mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-08 17:27:32 +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/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",
|
||||||
|
|
|
@ -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">
|
||||||
|
|
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() {
|
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}/> Add server
|
<FontAwesomeIcon icon={plusIcon}/> Add server
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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
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 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);
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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 = () => {
|
||||||
|
|
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