mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-22 17:10:26 +03:00
Implemented loading of short URLs
This commit is contained in:
parent
e4356720d7
commit
c0203f1336
16 changed files with 191 additions and 33 deletions
|
@ -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",
|
||||
|
|
|
@ -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">
|
||||
|
|
70
src/api/ShlinkApiClient.js
Normal file
70
src/api/ShlinkApiClient.js
Normal 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
8
src/common/AsideMenu.js
Normal 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
15
src/common/AsideMenu.scss
Normal 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;
|
||||
}
|
|
@ -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}/> Add server
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
|
|
|
@ -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>
|
||||
<Switch>
|
||||
|
||||
</Switch>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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
8
src/reducers/types.js
Normal 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';
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export const loadServer = serverId => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export const listServers = () => {
|
||||
|
|
33
src/short-urls/ShortUrlsList.js
Normal file
33
src/short-urls/ShortUrlsList.js
Normal 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);
|
21
src/short-urls/reducers/shortUrlsList.js
Normal file
21
src/short-urls/reducers/shortUrlsList.js
Normal 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() });
|
||||
};
|
||||
};
|
Loading…
Reference in a new issue