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

View file

@ -9,7 +9,7 @@ import CreateServer from './servers/CreateServer';
export default class App extends React.Component {
render() {
return (
<div>
<div className="container-fluid">
<MainHeader/>
<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() {
return (
<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
</NavbarBrand>
<NavbarToggler onClick={() => this.toggle()}/>
<Collapse navbar isOpen={this.state.isOpen}>
<Nav navbar className="ml-auto">
<NavItem>
<NavLink tag={Link} to ="/server/create">
<NavLink tag={Link} to="/server/create">
<FontAwesomeIcon icon={plusIcon}/>&nbsp; Add server
</NavLink>
</NavItem>

View file

@ -1,13 +1,20 @@
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 (
<div>
<nav>Left menu</nav>
<div className="row">
<AsideMenu />
<div className="col-md-10 offset-md-2 col-sm-9 offset-sm-3">
<Switch>
<Route exact
path="/server/:serverId/list-short-urls/:page"
component={ShortUrlsList}
/>
</Switch>
</div>
</div>
);
}

View file

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

View file

@ -2,10 +2,10 @@ import { combineReducers } from 'redux';
import serversReducer from '../servers/reducers/server';
import selectedServerReducer from '../servers/reducers/selectedServer';
import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList';
const rootReducer = combineReducers({
export default combineReducers({
servers: serversReducer,
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 { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
import { isEmpty } from 'ramda';
import { listServers } from './reducers/server';
import { loadServer } from './reducers/selectedServer';
export class ServersDropdown extends React.Component {
renderServers = () => {
@ -16,17 +16,13 @@ export class ServersDropdown extends React.Component {
return Object.values(servers).map(({ name, id }) => (
<span key={id}>
<DropdownItem onClick={() => this.selectServer(id)}>
<DropdownItem tag={Link} to={`/server/${id}/list-short-urls/1`}>
{name}
</DropdownItem>
</span>
));
};
selectServer = serverId => {
this.props.loadServer(serverId);
};
componentDidMount() {
this.props.listServers();
}
@ -45,4 +41,4 @@ const mapStateToProps = state => ({
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';
const LOAD_SERVER = 'shlink/LOAD_SERVER';
import { LOAD_SERVER } from '../../reducers/types';
export default function selectedServerReducer(state = null, action) {
switch (action.type) {
case LOAD_SERVER:
return action.selectedServer;
}
default:
return state;
}
}
export const loadServer = serverId => {

View file

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