From 20820c47d460843344fc51096aaf9495a026e39c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 28 Apr 2019 12:07:09 +0200 Subject: [PATCH 1/6] Updated list servers action so that it tries to fetch servers from the servers.json file when no local servers are found --- package.json | 2 +- src/common/Home.js | 10 +++-- src/servers/ServersDropdown.js | 27 ++++++------ src/servers/helpers/ImportServersBtn.js | 4 -- src/servers/reducers/selectedServer.js | 4 +- src/servers/reducers/server.js | 55 ++++++++++++++++++++----- src/servers/services/ServersImporter.js | 6 --- src/servers/services/ServersService.js | 12 +++--- src/servers/services/provideServices.js | 2 +- 9 files changed, 74 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index b1582502..ee0b7d7e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "A React-based progressive web application for shlink", "version": "1.0.0", "private": false, - "homepage": "https://shlink.io", + "homepage": "", "scripts": { "lint": "npm run lint:js && npm run lint:css", "lint:js": "eslint src test scripts config", diff --git a/src/common/Home.js b/src/common/Home.js index 7cb44dc8..f021de55 100644 --- a/src/common/Home.js +++ b/src/common/Home.js @@ -18,18 +18,20 @@ export default class Home extends React.Component { } render() { - const servers = values(this.props.servers); + const { servers: { list, loading } } = this.props; + const servers = values(list); const hasServers = !isEmpty(servers); return (

Welcome to Shlink

- {hasServers && Please, select a server.} - {!hasServers && Please, add a server.} + {!loading && hasServers && Please, select a server.} + {!loading && !hasServers && Please, add a server.} + {loading && Trying to load servers....}
- {hasServers && ( + {!loading && hasServers && ( {servers.map(({ name, id }) => ( class ServersDropdown extends React }; renderServers = () => { - const { servers, selectedServer, selectServer } = this.props; + const { servers: { list, loading }, selectedServer, selectServer } = this.props; + const servers = values(list); + + if (loading) { + return Trying to load servers...; + } if (isEmpty(servers)) { return Add a server first...; @@ -22,7 +27,7 @@ const ServersDropdown = (serversExporter) => class ServersDropdown extends React return ( - {values(servers).map(({ name, id }) => ( + {servers.map(({ name, id }) => ( class ServersDropdown extends React ); }; - componentDidMount() { - this.props.listServers(); - } + componentDidMount = this.props.listServers; - render() { - return ( - - Servers - {this.renderServers()} - - ); - } + render = () => ( + + Servers + {this.renderServers()} + + ); }; export default ServersDropdown; diff --git a/src/servers/helpers/ImportServersBtn.js b/src/servers/helpers/ImportServersBtn.js index 4d687976..ec56cb9e 100644 --- a/src/servers/helpers/ImportServersBtn.js +++ b/src/servers/helpers/ImportServersBtn.js @@ -1,7 +1,5 @@ import React from 'react'; import { UncontrolledTooltip } from 'reactstrap'; -import { assoc, map } from 'ramda'; -import { v4 as uuid } from 'uuid'; import PropTypes from 'prop-types'; const ImportServersBtn = (serversImporter) => class ImportServersBtn extends React.Component { @@ -22,10 +20,8 @@ const ImportServersBtn = (serversImporter) => class ImportServersBtn extends Rea render() { const { importServersFromFile } = serversImporter; const { onImport, createServers } = this.props; - const assocId = (server) => assoc('id', uuid(), server); const onChange = ({ target }) => importServersFromFile(target.files[0]) - .then(map(assocId)) .then(createServers) .then(onImport) .then(() => { diff --git a/src/servers/reducers/selectedServer.js b/src/servers/reducers/selectedServer.js index 2d35987f..bd14e80f 100644 --- a/src/servers/reducers/selectedServer.js +++ b/src/servers/reducers/selectedServer.js @@ -10,10 +10,10 @@ const initialState = null; export const resetSelectedServer = createAction(RESET_SELECTED_SERVER); -export const selectServer = (serversService) => (serverId) => (dispatch) => { +export const selectServer = ({ findServerById }) => (serverId) => (dispatch) => { dispatch(resetShortUrlParams()); - const selectedServer = serversService.findServerById(serverId); + const selectedServer = findServerById(serverId); dispatch({ type: SELECT_SERVER, diff --git a/src/servers/reducers/server.js b/src/servers/reducers/server.js index 1a3955f6..a5d3b3b4 100644 --- a/src/servers/reducers/server.js +++ b/src/servers/reducers/server.js @@ -1,16 +1,51 @@ -import { createAction, handleActions } from 'redux-actions'; -import { pipe } from 'ramda'; +import { handleActions } from 'redux-actions'; +import { pipe, isEmpty, assoc, map } from 'ramda'; +import { v4 as uuid } from 'uuid'; +import { homepage } from '../../../package.json'; +/* eslint-disable padding-line-between-statements */ +export const FETCH_SERVERS_START = 'shlink/servers/FETCH_SERVERS_START'; export const FETCH_SERVERS = 'shlink/servers/FETCH_SERVERS'; +/* eslint-enable padding-line-between-statements */ -export const listServers = ({ listServers }) => createAction(FETCH_SERVERS, () => listServers()); +const initialState = { + list: {}, + loading: false, +}; -export const createServer = ({ createServer }, listServers) => pipe(createServer, listServers); - -export const deleteServer = ({ deleteServer }, listServers) => pipe(deleteServer, listServers); - -export const createServers = ({ createServers }, listServers) => pipe(createServers, listServers); +const assocId = (server) => assoc('id', uuid(), server); export default handleActions({ - [FETCH_SERVERS]: (state, { payload }) => payload, -}, {}); + [FETCH_SERVERS_START]: (state) => ({ ...state, loading: true }), + [FETCH_SERVERS]: (state, { list }) => ({ list, loading: false }), +}, initialState); + +export const listServers = ({ listServers, createServers }, { get }) => () => async (dispatch) => { + dispatch({ type: FETCH_SERVERS_START }); + + // Fetch list from local storage. + const localList = listServers(); + + if (!isEmpty(localList)) { + dispatch({ type: FETCH_SERVERS, list: localList }); + + return; + } + + // If local list is empty, try to fetch it remotely, calculate IDs for every server, and use it + const { data: remoteList } = await get(`${homepage}/servers.json`); + const listWithIds = map(assocId, remoteList); + + createServers(listWithIds); + dispatch({ type: FETCH_SERVERS, list: listWithIds }); +}; + +export const createServer = ({ createServer }, listServersAction) => pipe(createServer, listServersAction); + +export const deleteServer = ({ deleteServer }, listServersAction) => pipe(deleteServer, listServersAction); + +export const createServers = ({ createServers }, listServersAction) => pipe( + map(assocId), + createServers, + listServersAction +); diff --git a/src/servers/services/ServersImporter.js b/src/servers/services/ServersImporter.js index af855a64..35ce8ef9 100644 --- a/src/servers/services/ServersImporter.js +++ b/src/servers/services/ServersImporter.js @@ -1,9 +1,3 @@ -import PropTypes from 'prop-types'; - -export const serversImporterType = PropTypes.shape({ - importServersFromFile: PropTypes.func, -}); - export default class ServersImporter { constructor(csvjson) { this.csvjson = csvjson; diff --git a/src/servers/services/ServersService.js b/src/servers/services/ServersService.js index 52bb8fed..aee850d5 100644 --- a/src/servers/services/ServersService.js +++ b/src/servers/services/ServersService.js @@ -1,10 +1,11 @@ -import { assoc, dissoc, reduce } from 'ramda'; +import { assoc, curry, dissoc, reduce } from 'ramda'; const SERVERS_STORAGE_KEY = 'servers'; export default class ServersService { constructor(storage) { this.storage = storage; + this.setServers = curry(this.storage.set)(SERVERS_STORAGE_KEY); } listServers = () => this.storage.get(SERVERS_STORAGE_KEY) || {}; @@ -20,12 +21,9 @@ export default class ServersService { servers ); - this.storage.set(SERVERS_STORAGE_KEY, allServers); + this.setServers(allServers); }; - deleteServer = (server) => - this.storage.set( - SERVERS_STORAGE_KEY, - dissoc(server.id, this.listServers()) - ); + deleteServer = ({ id }) => + this.setServers(dissoc(id, this.listServers())); } diff --git a/src/servers/services/provideServices.js b/src/servers/services/provideServices.js index 7821e168..0b135491 100644 --- a/src/servers/services/provideServices.js +++ b/src/servers/services/provideServices.js @@ -38,7 +38,7 @@ const provideServices = (bottle, connect, withRouter) => { bottle.serviceFactory('createServer', createServer, 'ServersService', 'listServers'); bottle.serviceFactory('createServers', createServers, 'ServersService', 'listServers'); bottle.serviceFactory('deleteServer', deleteServer, 'ServersService', 'listServers'); - bottle.serviceFactory('listServers', listServers, 'ServersService'); + bottle.serviceFactory('listServers', listServers, 'ServersService', 'axios'); bottle.serviceFactory('resetSelectedServer', () => resetSelectedServer); }; From 78c34a342d8ff9e700872fc5af68f8b0c46191f2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 28 Apr 2019 12:40:50 +0200 Subject: [PATCH 2/6] Added tests for new use cases --- src/common/Home.js | 2 +- src/servers/reducers/server.js | 8 ++--- src/servers/services/ServersService.js | 7 ++-- test/common/Home.test.js | 22 +++++++++---- test/servers/ServersDropdown.test.js | 24 ++++++++++---- test/servers/reducers/server.test.js | 44 ++++++++++++++++++++------ 6 files changed, 76 insertions(+), 31 deletions(-) diff --git a/src/common/Home.js b/src/common/Home.js index f021de55..e670e20e 100644 --- a/src/common/Home.js +++ b/src/common/Home.js @@ -28,7 +28,7 @@ export default class Home extends React.Component {
{!loading && hasServers && Please, select a server.} {!loading && !hasServers && Please, add a server.} - {loading && Trying to load servers....} + {loading && Trying to load servers...}
{!loading && hasServers && ( diff --git a/src/servers/reducers/server.js b/src/servers/reducers/server.js index a5d3b3b4..17ac2662 100644 --- a/src/servers/reducers/server.js +++ b/src/servers/reducers/server.js @@ -13,7 +13,7 @@ const initialState = { loading: false, }; -const assocId = (server) => assoc('id', uuid(), server); +const assocId = (server) => assoc('id', server.id || uuid(), server); export default handleActions({ [FETCH_SERVERS_START]: (state) => ({ ...state, loading: true }), @@ -22,8 +22,6 @@ export default handleActions({ export const listServers = ({ listServers, createServers }, { get }) => () => async (dispatch) => { dispatch({ type: FETCH_SERVERS_START }); - - // Fetch list from local storage. const localList = listServers(); if (!isEmpty(localList)) { @@ -32,12 +30,12 @@ export const listServers = ({ listServers, createServers }, { get }) => () => as return; } - // If local list is empty, try to fetch it remotely, calculate IDs for every server, and use it + // If local list is empty, try to fetch it remotely and calculate IDs for every server const { data: remoteList } = await get(`${homepage}/servers.json`); const listWithIds = map(assocId, remoteList); createServers(listWithIds); - dispatch({ type: FETCH_SERVERS, list: listWithIds }); + dispatch({ type: FETCH_SERVERS, list: listWithIds.reduce((map, server) => ({ ...map, [server.id]: server }), {}) }); }; export const createServer = ({ createServer }, listServersAction) => pipe(createServer, listServersAction); diff --git a/src/servers/services/ServersService.js b/src/servers/services/ServersService.js index aee850d5..e8ffdb68 100644 --- a/src/servers/services/ServersService.js +++ b/src/servers/services/ServersService.js @@ -1,11 +1,10 @@ -import { assoc, curry, dissoc, reduce } from 'ramda'; +import { assoc, dissoc, reduce } from 'ramda'; const SERVERS_STORAGE_KEY = 'servers'; export default class ServersService { constructor(storage) { this.storage = storage; - this.setServers = curry(this.storage.set)(SERVERS_STORAGE_KEY); } listServers = () => this.storage.get(SERVERS_STORAGE_KEY) || {}; @@ -21,9 +20,9 @@ export default class ServersService { servers ); - this.setServers(allServers); + this.storage.set(SERVERS_STORAGE_KEY, allServers); }; deleteServer = ({ id }) => - this.setServers(dissoc(id, this.listServers())); + this.storage.set(SERVERS_STORAGE_KEY, dissoc(id, this.listServers())); } diff --git a/test/common/Home.test.js b/test/common/Home.test.js index 6fb094a5..249b3e5d 100644 --- a/test/common/Home.test.js +++ b/test/common/Home.test.js @@ -6,10 +6,8 @@ import Home from '../../src/common/Home'; describe('', () => { let wrapped; const defaultProps = { - resetSelectedServer() { - return ''; - }, - servers: {}, + resetSelectedServer: () => '', + servers: { loading: false, list: {} }, }; const createComponent = (props) => { const actualProps = { ...defaultProps, ...props }; @@ -41,10 +39,22 @@ describe('', () => { expect(wrapped.find('ListGroup')).toHaveLength(0); }); + it('shows message when loading servers', () => { + const wrapped = createComponent({ servers: { loading: true } }); + const span = wrapped.find('span'); + + expect(span).toHaveLength(1); + expect(span.text()).toContain('Trying to load servers...'); + expect(wrapped.find('ListGroup')).toHaveLength(0); + }); + it('shows servers list when list of servers is not empty', () => { const servers = { - 1: { name: 'foo', id: '123' }, - 2: { name: 'bar', id: '456' }, + loading: false, + list: { + 1: { name: 'foo', id: '123' }, + 2: { name: 'bar', id: '456' }, + }, }; const wrapped = createComponent({ servers }); diff --git a/test/servers/ServersDropdown.test.js b/test/servers/ServersDropdown.test.js index 0f194f3e..c5bc0dd4 100644 --- a/test/servers/ServersDropdown.test.js +++ b/test/servers/ServersDropdown.test.js @@ -8,9 +8,12 @@ describe('', () => { let wrapped; let ServersDropdown; const servers = { - '1a': { name: 'foo', id: 1 }, - '2b': { name: 'bar', id: 2 }, - '3c': { name: 'baz', id: 3 }, + list: { + '1a': { name: 'foo', id: 1 }, + '2b': { name: 'bar', id: 2 }, + '3c': { name: 'baz', id: 3 }, + }, + loading: false, }; beforeEach(() => { @@ -20,7 +23,7 @@ describe('', () => { afterEach(() => wrapped.unmount()); it('contains the list of servers', () => - expect(wrapped.find(DropdownItem).filter('[to]')).toHaveLength(values(servers).length)); + expect(wrapped.find(DropdownItem).filter('[to]')).toHaveLength(values(servers.list).length)); it('contains a toggle with proper title', () => expect(wrapped.find(DropdownToggle)).toHaveLength(1)); @@ -32,12 +35,21 @@ describe('', () => { expect(items.filter('.servers-dropdown__export-item')).toHaveLength(1); }); - it('contains a message when no servers exist yet', () => { - wrapped = shallow(); + it('shows a message when no servers exist yet', () => { + wrapped = shallow(); const item = wrapped.find(DropdownItem); expect(item).toHaveLength(1); expect(item.prop('disabled')).toEqual(true); expect(item.find('i').text()).toEqual('Add a server first...'); }); + + it('shows a message when loading', () => { + wrapped = shallow(); + const item = wrapped.find(DropdownItem); + + expect(item).toHaveLength(1); + expect(item.prop('disabled')).toEqual(true); + expect(item.find('i').text()).toEqual('Trying to load servers...'); + }); }); diff --git a/test/servers/reducers/server.test.js b/test/servers/reducers/server.test.js index 6645f9e0..14d204bb 100644 --- a/test/servers/reducers/server.test.js +++ b/test/servers/reducers/server.test.js @@ -4,17 +4,17 @@ import reducer, { deleteServer, listServers, createServers, - FETCH_SERVERS, + FETCH_SERVERS, FETCH_SERVERS_START, } from '../../../src/servers/reducers/server'; describe('serverReducer', () => { - const payload = { + const list = { abc123: { id: 'abc123' }, def456: { id: 'def456' }, }; - const expectedFetchServersResult = { type: FETCH_SERVERS, payload }; + const expectedFetchServersResult = { type: FETCH_SERVERS, list }; const ServersServiceMock = { - listServers: jest.fn(() => payload), + listServers: jest.fn(() => list), createServer: jest.fn(), deleteServer: jest.fn(), createServers: jest.fn(), @@ -22,7 +22,7 @@ describe('serverReducer', () => { describe('reducer', () => { it('returns servers when action is FETCH_SERVERS', () => - expect(reducer({}, { type: FETCH_SERVERS, payload })).toEqual(payload)); + expect(reducer({}, { type: FETCH_SERVERS, list })).toEqual({ loading: false, list })); }); describe('action creators', () => { @@ -34,14 +34,40 @@ describe('serverReducer', () => { }); describe('listServers', () => { - it('fetches servers and returns them as part of the action', () => { - const result = listServers(ServersServiceMock)(); + const axios = { get: jest.fn().mockResolvedValue({ data: [] }) }; + const dispatch = jest.fn(); - expect(result).toEqual(expectedFetchServersResult); + beforeEach(() => { + axios.get.mockClear(); + dispatch.mockReset(); + }); + + it('fetches servers from local storage when found', async () => { + await listServers(ServersServiceMock, axios)()(dispatch); + + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(1, { type: FETCH_SERVERS_START }); + expect(dispatch).toHaveBeenNthCalledWith(2, expectedFetchServersResult); expect(ServersServiceMock.listServers).toHaveBeenCalledTimes(1); expect(ServersServiceMock.createServer).not.toHaveBeenCalled(); expect(ServersServiceMock.deleteServer).not.toHaveBeenCalled(); expect(ServersServiceMock.createServers).not.toHaveBeenCalled(); + expect(axios.get).not.toHaveBeenCalled(); + }); + + it('tries to fetch servers from remote when not found locally', async () => { + const NoListServersServiceMock = { ...ServersServiceMock, listServers: jest.fn(() => ({})) }; + + await listServers(NoListServersServiceMock, axios)()(dispatch); + + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(1, { type: FETCH_SERVERS_START }); + expect(dispatch).toHaveBeenNthCalledWith(2, { type: FETCH_SERVERS, list: {} }); + expect(NoListServersServiceMock.listServers).toHaveBeenCalledTimes(1); + expect(NoListServersServiceMock.createServer).not.toHaveBeenCalled(); + expect(NoListServersServiceMock.deleteServer).not.toHaveBeenCalled(); + expect(NoListServersServiceMock.createServers).toHaveBeenCalledTimes(1); + expect(axios.get).toHaveBeenCalledTimes(1); }); }); @@ -75,7 +101,7 @@ describe('serverReducer', () => { describe('createServer', () => { it('creates multiple servers and then fetches servers again', () => { - const serversToCreate = values(payload); + const serversToCreate = values(list); const result = createServers(ServersServiceMock, () => expectedFetchServersResult)(serversToCreate); expect(result).toEqual(expectedFetchServersResult); From 4c11d9c6d5d5c2dcad5e402d6dcf02c2683af9cd Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 28 Apr 2019 13:07:55 +0200 Subject: [PATCH 3/6] Catched error when loading servers from a servers.json file --- .gitignore | 1 + README.md | 7 ++++--- src/servers/reducers/server.js | 12 +++++++----- src/utils/utils.js | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 19285d6a..1b558dfb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ npm-debug.log* docker-compose.override.yml home +public/servers.json* diff --git a/README.md b/README.md index 79c8f16a..2c2442ad 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,13 @@ Official distributable files have been build so that they are served from the ro If you need to host shlink-web-client yourself and serve it from a subpath, follow these steps: * Download [node](https://nodejs.org/en/download/package-manager/) 10.15 or later (if you don't have it yet). -* Download shlink-web-client source files for the version you want to build. +* Download shlink-web-client source code for the version you want to build. * For example, if you want to build `v1.0.1`, use this link https://github.com/shlinkio/shlink-web-client/archive/v1.0.1.zip * Replace the `v1.0.1` part in the link with the one of the version you want to build. * Decompress the file and `cd` into the resulting folder. * Install project dependencies by running `npm install`. * Open the `package.json` file in the root of the project, locate the `homepage` property and replace the value (which should be an empty string) by the path from which you want to serve shlink-web-client. * For example: `"homepage": "/my-projects/shlink-web-client",`. -* Build the distributable contents by running `npm run build`. -* Once the command finishes, you will have a `build` folder with all the static assets you need to run shlink-web-client. Just place them wherever you want them to be served from. +* Build the project: + * For a static distributable file, run `npm run build`. Once the command finishes, you will have a `build` folder with all the static assets you need to run shlink-web-client. Just place them wherever you want them to be served from. + * For a docker image, run `docker build . -t shlink-web-client`. Once the command finishes, you will have an image with the name `shlink-web-client`. diff --git a/src/servers/reducers/server.js b/src/servers/reducers/server.js index 17ac2662..f1624027 100644 --- a/src/servers/reducers/server.js +++ b/src/servers/reducers/server.js @@ -1,5 +1,5 @@ import { handleActions } from 'redux-actions'; -import { pipe, isEmpty, assoc, map } from 'ramda'; +import { pipe, isEmpty, assoc, map, prop } from 'ramda'; import { v4 as uuid } from 'uuid'; import { homepage } from '../../../package.json'; @@ -31,11 +31,13 @@ export const listServers = ({ listServers, createServers }, { get }) => () => as } // If local list is empty, try to fetch it remotely and calculate IDs for every server - const { data: remoteList } = await get(`${homepage}/servers.json`); - const listWithIds = map(assocId, remoteList); + const remoteList = await get(`${homepage}/servers.json`) + .then(prop('data')) + .then(map(assocId)) + .catch(() => []); - createServers(listWithIds); - dispatch({ type: FETCH_SERVERS, list: listWithIds.reduce((map, server) => ({ ...map, [server.id]: server }), {}) }); + createServers(remoteList); + dispatch({ type: FETCH_SERVERS, list: remoteList.reduce((map, server) => ({ ...map, [server.id]: server }), {}) }); }; export const createServer = ({ createServer }, listServersAction) => pipe(createServer, listServersAction); diff --git a/src/utils/utils.js b/src/utils/utils.js index 5f416bca..b1daf53b 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -52,4 +52,4 @@ export const useToggle = (initialValue = false) => { return [ flag, () => setFlag(!flag) ]; }; -export const wait = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds)); +export const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)); From a1a0b935c731da03ddf3be802af144cbd21c70fe Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 28 Apr 2019 17:41:01 +0200 Subject: [PATCH 4/6] Improved documentation mentioning how to pre-configure servers --- README.md | 54 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2c2442ad..ed3418fa 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ A ReactJS-based progressive web application for [Shlink](https://shlink.io). There are three ways in which you can use this application. -* The easiest way to use shlink-web-client is by just going to https://app.shlink.io. +* The easiest way to use shlink-web-client is by just going to . - The application runs 100% in the browser, so you can use that instance and access any shlink instance from it. + The application runs 100% in the browser, so you can safely access any shlink instance from there. * Self hosting the application yourself. @@ -24,28 +24,60 @@ There are three ways in which you can use this application. The package contains static files only, so just put it in a folder and serve it with the web server of your choice. - Provided dist files are configured to be served from the root of your domain. If you need to serve shlink-web-client from a subpath, you will have to build it yourself following [these simple steps](#serve-shlink-in-subpath). + Provided dist files are configured to be served from the root of your domain. If you need to serve shlink-web-client from a subpath, you will have to build it yourself following [these steps](#serve-shlink-in-subpath). -* Use the official [docker image](https://hub.docker.com/r/shlinkio/shlink-web-client/) +* Using the official [docker image](https://hub.docker.com/r/shlinkio/shlink-web-client/) - If you want to deploy shlink-web-client in a container-based cluster (kubernetes, docker swarm, etc), just pick the image and do it. + If you want to deploy shlink-web-client in a container-based cluster (kubernetes, docker swarm, etc), just pick the `shlinkio/shlink-web-client` image and do it. - It's a lightweight [nginx:alpine](https://hub.docker.com/r/library/nginx/) image serving the assets on port 80. + It's a lightweight [nginx:alpine](https://hub.docker.com/r/library/nginx/) image serving the static app on port 80. + +## Pre-configuring servers + +The first time you access shlink-web-client from a browser, you will have to configure the list of shlink servers you want to manage, and they will be saved in the local storage. + +Those servers can be exported and imported in other browsers, but if for some reason you need some servers to be there from the beginning, you can provide a `servers.json` file in the project root folder (the same containing the `index.html`, `favicon.ico`, etc) with a structure like this: + +```json +[ + { + "name": "Main server", + "url": "https://doma.in", + "apiKey": "09c972b7-506b-49f1-a19a-d729e22e599c" + }, + { + "name": "Local", + "url": "http://localhost:8080", + "apiKey": "580d0b42-4dea-419a-96bf-6c876b901451" + } +] +``` + +> The list can contain as many servers as you need. + +If you are using the shlink-web-client docker image, you can mount the `servers.json` file in a volume inside `/usr/share/nginx/html`, which is the app's document root inside the container. + + docker run --name shlink-web-client -p 8000:80 -v ${PWD}/servers.json:/usr/share/nginx/html/servers.json shlinkio/shlink-web-client ## Serve project in subpath -Official distributable files have been build so that they are served from the root of a domain. +Official distributable files have been built so that they are served from the root of a domain. If you need to host shlink-web-client yourself and serve it from a subpath, follow these steps: -* Download [node](https://nodejs.org/en/download/package-manager/) 10.15 or later (if you don't have it yet). * Download shlink-web-client source code for the version you want to build. * For example, if you want to build `v1.0.1`, use this link https://github.com/shlinkio/shlink-web-client/archive/v1.0.1.zip * Replace the `v1.0.1` part in the link with the one of the version you want to build. * Decompress the file and `cd` into the resulting folder. -* Install project dependencies by running `npm install`. * Open the `package.json` file in the root of the project, locate the `homepage` property and replace the value (which should be an empty string) by the path from which you want to serve shlink-web-client. * For example: `"homepage": "/my-projects/shlink-web-client",`. * Build the project: - * For a static distributable file, run `npm run build`. Once the command finishes, you will have a `build` folder with all the static assets you need to run shlink-web-client. Just place them wherever you want them to be served from. - * For a docker image, run `docker build . -t shlink-web-client`. Once the command finishes, you will have an image with the name `shlink-web-client`. + * For classic hosting: + * Download [node](https://nodejs.org/en/download/package-manager/) 10.15 or later. + * Install project dependencies by running `npm install`. + * Build the project by running `npm run build`. + * Once the command finishes, you will have a `build` folder with all the static assets you need to run shlink-web-client. Just place them wherever you want them to be served from. + * For docker image: + * Download [docker](https://docs.docker.com/install/). + * Build the docker image by running `docker build . -t shlink-web-client`. + * Once the command finishes, you will have an image with the name `shlink-web-client`. From a09b661b510821048fb6b5bfce3e57ad4e3ed92f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 28 Apr 2019 17:42:20 +0200 Subject: [PATCH 5/6] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd0a497c..bdea2c2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), #### Added * [#101](https://github.com/shlinkio/shlink-web-client/issues/101) Added checkbox to short URL creation form that allows to determine the value of the `findIfExists` flag introduced in Shlink v1.16.0. +* [#105](https://github.com/shlinkio/shlink-web-client/issues/105) Added support to pre-configure servers. #### Changed From 258330f98586a5f378ff6425d3f65c3dbfb7979a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 28 Apr 2019 17:53:35 +0200 Subject: [PATCH 6/6] Mentioned that pre-configured servers won't work on versions previous to 2.1.0 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed3418fa..e7a1f2d3 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ There are three ways in which you can use this application. The first time you access shlink-web-client from a browser, you will have to configure the list of shlink servers you want to manage, and they will be saved in the local storage. -Those servers can be exported and imported in other browsers, but if for some reason you need some servers to be there from the beginning, you can provide a `servers.json` file in the project root folder (the same containing the `index.html`, `favicon.ico`, etc) with a structure like this: +Those servers can be exported and imported in other browsers, but if for some reason you need some servers to be there from the beginning, starting with shlink-web-client 2.1.0, you can provide a `servers.json` file in the project root folder (the same containing the `index.html`, `favicon.ico`, etc) with a structure like this: ```json [