Implemented loading of short URLs

This commit is contained in:
Alejandro Celaya 2018-06-15 21:49:25 +02:00
parent e4356720d7
commit c0203f1336
16 changed files with 191 additions and 33 deletions

View file

@ -8,6 +8,7 @@
"@fortawesome/fontawesome-free-solid": "^5.0.13", "@fortawesome/fontawesome-free-solid": "^5.0.13",
"@fortawesome/react-fontawesome": "0.0.19", "@fortawesome/react-fontawesome": "0.0.19",
"autoprefixer": "7.1.6", "autoprefixer": "7.1.6",
"axios": "^0.18.0",
"babel-core": "6.26.0", "babel-core": "6.26.0",
"babel-eslint": "7.2.3", "babel-eslint": "7.2.3",
"babel-jest": "20.0.3", "babel-jest": "20.0.3",
@ -46,6 +47,7 @@
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"reactstrap": "^6.0.1", "reactstrap": "^6.0.1",
"redux": "^4.0.0", "redux": "^4.0.0",
"redux-thunk": "^2.3.0",
"resolve": "1.6.0", "resolve": "1.6.0",
"style-loader": "0.19.0", "style-loader": "0.19.0",
"sw-precache-webpack-plugin": "0.11.4", "sw-precache-webpack-plugin": "0.11.4",
@ -74,7 +76,7 @@
"<rootDir>/config/setupEnzyme.js" "<rootDir>/config/setupEnzyme.js"
], ],
"testMatch": [ "testMatch": [
"<rootDir>/tests/**/*.test.{js,jsx,mjs}" "<rootDir>/test/**/*.test.{js,jsx,mjs}"
], ],
"testEnvironment": "node", "testEnvironment": "node",
"testURL": "http://localhost", "testURL": "http://localhost",

View file

@ -9,7 +9,7 @@ import CreateServer from './servers/CreateServer';
export default class App extends React.Component { export default class App extends React.Component {
render() { render() {
return ( return (
<div> <div className="container-fluid">
<MainHeader/> <MainHeader/>
<div className="app"> <div className="app">

View file

@ -0,0 +1,70 @@
import axios from 'axios';
import { isEmpty } from 'ramda';
export class ShlinkApiClient {
constructor(axios) {
this.axios = axios;
this._baseUrl = '';
this._apiKey = '';
this._token = '';
}
/**
* Sets the base URL to be used on any request
* @param {String} baseUrl
* @param {String} apiKey
*/
setConfig = ({ url, apiKey }) => {
this._baseUrl = url;
this._apiKey = apiKey;
};
/**
* Returns the list of short URLs
* @param params
* @returns {Promise<Array>}
*/
listShortUrls = (params = {}) => {
const { page = 1 } = params;
return this._performRequest(`/rest/short-codes?page=${page}`)
.then(resp => resp.data.shortUrls.data)
.catch(e => {
// If auth failed, reset token to force it to be regenerated, and perform a new request
if (e.response.status === 401) {
this._token = '';
return this.listShortUrls(params);
}
// Otherwise, let caller handle the rejection
return Promise.reject(e);
});
};
_performRequest = async (url, method = 'GET') => {
if (isEmpty(this._token)) {
this._token = await this._handleAuth();
}
return await this.axios({
method,
url: `${this._baseUrl}${url}`,
headers: { 'Authorization': `Bearer ${this._token}` }
}).then(resp => {
// Save new token
const { authorization = '' } = resp.headers;
this._token = authorization.substr('Bearer '.length);
return resp;
});
};
_handleAuth = async () => {
const resp = await this.axios({
method: 'POST',
url: `${this._baseUrl}/rest/authenticate`,
data: { apiKey: this._apiKey }
});
return resp.data.token;
};
}
export default new ShlinkApiClient(axios);

8
src/common/AsideMenu.js Normal file
View file

@ -0,0 +1,8 @@
import React from 'react';
import './AsideMenu.scss';
export default function AsideMenu() {
return (
<aside className="aside-menu col-md-2 col-sm-2">Aside menu</aside>
);
}

15
src/common/AsideMenu.scss Normal file
View file

@ -0,0 +1,15 @@
@import "../utils/base";
.aside-menu {
position: fixed !important;
top: $headerHeight;
bottom: 0;
left: 0;
z-index: 1000;
display: block;
padding: 20px;
overflow-x: hidden;
overflow-y: auto;
background-color: #f7f7f7;
border-right: 1px solid #eee;
}

View file

@ -25,14 +25,14 @@ export default class MainHeader extends React.Component {
render() { render() {
return ( return (
<Navbar color="primary" dark fixed="top" className="main-header" expand="md"> <Navbar color="primary" dark fixed="top" className="main-header" expand="md">
<NavbarBrand href="https://shlink.io" target="_blank"> <NavbarBrand tag={Link} to="/">
<img src={shlinkLogo} alt="Shlink" className="main-header__brand-logo"/> Shlink <img src={shlinkLogo} alt="Shlink" className="main-header__brand-logo"/> Shlink
</NavbarBrand> </NavbarBrand>
<NavbarToggler onClick={() => this.toggle()}/> <NavbarToggler onClick={() => this.toggle()}/>
<Collapse navbar isOpen={this.state.isOpen}> <Collapse navbar isOpen={this.state.isOpen}>
<Nav navbar className="ml-auto"> <Nav navbar className="ml-auto">
<NavItem> <NavItem>
<NavLink tag={Link} to ="/server/create"> <NavLink tag={Link} to="/server/create">
<FontAwesomeIcon icon={plusIcon}/>&nbsp; Add server <FontAwesomeIcon icon={plusIcon}/>&nbsp; Add server
</NavLink> </NavLink>
</NavItem> </NavItem>

View file

@ -1,13 +1,20 @@
import React from 'react'; import React from 'react';
import { Switch } from 'react-router-dom'; import { Route, Switch } from 'react-router-dom';
import ShortUrlsList from '../short-urls/ShortUrlsList';
import AsideMenu from './AsideMenu';
export default () => { export default function MenuLayout() {
return ( return (
<div> <div className="row">
<nav>Left menu</nav> <AsideMenu />
<Switch> <div className="col-md-10 offset-md-2 col-sm-9 offset-sm-3">
<Switch>
</Switch> <Route exact
path="/server/:serverId/list-short-urls/:page"
component={ShortUrlsList}
/>
</Switch>
</div>
</div> </div>
); );
} }

View file

@ -4,13 +4,14 @@ import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { applyMiddleware, createStore } from 'redux'; import { applyMiddleware, createStore } from 'redux';
import ReduxThunk from 'redux-thunk';
import App from './App'; import App from './App';
import './index.scss'; import './index.scss';
import reducers from './reducers'; import reducers from './reducers';
import registerServiceWorker from './registerServiceWorker'; import registerServiceWorker from './registerServiceWorker';
// const store = createStore(reducers, {}, applyMiddleware()); const store = createStore(reducers, applyMiddleware(ReduxThunk));
const store = createStore(reducers, applyMiddleware());
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>

View file

@ -2,10 +2,10 @@ import { combineReducers } from 'redux';
import serversReducer from '../servers/reducers/server'; import serversReducer from '../servers/reducers/server';
import selectedServerReducer from '../servers/reducers/selectedServer'; import selectedServerReducer from '../servers/reducers/selectedServer';
import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList';
const rootReducer = combineReducers({ export default combineReducers({
servers: serversReducer, servers: serversReducer,
selectedServer: selectedServerReducer, selectedServer: selectedServerReducer,
shortUrlsList: shortUrlsListReducer,
}); });
export default rootReducer;

8
src/reducers/types.js Normal file
View file

@ -0,0 +1,8 @@
// Servers
export const LOAD_SERVER = 'shlink/LOAD_SERVER';
export const FETCH_SERVERS = 'shlink/FETCH_SERVERS';
export const CREATE_SERVER = 'shlink/CREATE_SERVER';
// Short URLs
export const LIST_SHORT_URLS = 'shlink/LIST_SHORT_URLS';

View file

@ -1,10 +1,10 @@
import { isEmpty } from 'ramda';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap'; import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
import { isEmpty } from 'ramda';
import { listServers } from './reducers/server'; import { listServers } from './reducers/server';
import { loadServer } from './reducers/selectedServer';
export class ServersDropdown extends React.Component { export class ServersDropdown extends React.Component {
renderServers = () => { renderServers = () => {
@ -16,17 +16,13 @@ export class ServersDropdown extends React.Component {
return Object.values(servers).map(({ name, id }) => ( return Object.values(servers).map(({ name, id }) => (
<span key={id}> <span key={id}>
<DropdownItem onClick={() => this.selectServer(id)}> <DropdownItem tag={Link} to={`/server/${id}/list-short-urls/1`}>
{name} {name}
</DropdownItem> </DropdownItem>
</span> </span>
)); ));
}; };
selectServer = serverId => {
this.props.loadServer(serverId);
};
componentDidMount() { componentDidMount() {
this.props.listServers(); this.props.listServers();
} }
@ -45,4 +41,4 @@ const mapStateToProps = state => ({
servers: state.servers servers: state.servers
}); });
export default connect(mapStateToProps, { listServers, loadServer })(ServersDropdown); export default connect(mapStateToProps, { listServers })(ServersDropdown);

View file

@ -1,14 +1,13 @@
import ServersService from '../services'; import ServersService from '../services';
import { LOAD_SERVER } from '../../reducers/types';
const LOAD_SERVER = 'shlink/LOAD_SERVER';
export default function selectedServerReducer(state = null, action) { export default function selectedServerReducer(state = null, action) {
switch (action.type) { switch (action.type) {
case LOAD_SERVER: case LOAD_SERVER:
return action.selectedServer; return action.selectedServer;
default:
return state;
} }
return state;
} }
export const loadServer = serverId => { export const loadServer = serverId => {

View file

@ -1,7 +1,5 @@
import ServersService from '../services'; import ServersService from '../services';
import { FETCH_SERVERS, CREATE_SERVER } from '../../reducers/types';
const FETCH_SERVERS = 'shlink/FETCH_SERVERS';
const CREATE_SERVER = 'shlink/CREATE_SERVER';
export default function serversReducer(state = {}, action) { export default function serversReducer(state = {}, action) {
switch (action.type) { switch (action.type) {
@ -9,9 +7,9 @@ export default function serversReducer(state = {}, action) {
return action.servers; return action.servers;
case CREATE_SERVER: case CREATE_SERVER:
return [ ...state, action.server ]; return [ ...state, action.server ];
default:
return state;
} }
return state;
} }
export const listServers = () => { export const listServers = () => {

View file

@ -0,0 +1,33 @@
import React from 'react';
import { connect } from 'react-redux';
import { listShortUrls } from './reducers/shortUrlsList';
export class ShortUrlsList extends React.Component {
componentDidMount() {
const { match } = this.props;
this.props.listShortUrls(match.params.serverId);
}
render() {
return (
<ul>
{this.renderShortUrls()}
</ul>
);
}
renderShortUrls() {
const { shortUrlsList } = this.props;
if (! shortUrlsList) {
return '<li><i>Loading...</i></li>';
}
return shortUrlsList.map(shortUrl => (
<li key={shortUrl.shortCode}>{`${shortUrl.shortCode}`}</li>
));
}
}
export default connect(state => ({
shortUrlsList: state.shortUrlsList
}), { listShortUrls })(ShortUrlsList);

View file

@ -0,0 +1,21 @@
import { LIST_SHORT_URLS } from '../../reducers/types';
import ServersService from '../../servers/services';
import ShlinkApiClient from '../../api/ShlinkApiClient';
export default function shortUrlsListReducer(state = [], action) {
switch (action.type) {
case LIST_SHORT_URLS:
return action.shortUrls;
default:
return state;
}
}
export const listShortUrls = (serverId) => {
return async dispatch => {
const selectedServer = ServersService.findServerById(serverId);
ShlinkApiClient.setConfig(selectedServer);
dispatch({ type: LIST_SHORT_URLS, shortUrls: await ShlinkApiClient.listShortUrls() });
};
};