mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Merge pull request #227 from acelaya-forks/feature/edit-servers
Feature/edit servers
This commit is contained in:
commit
25c67f1c3e
30 changed files with 501 additions and 213 deletions
|
@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||
|
||||
* [#213](https://github.com/shlinkio/shlink-web-client/issues/213) The versions of both shlink-web-client and currently consumed Shlink server are now displayed in the footer.
|
||||
* [#221](https://github.com/shlinkio/shlink-web-client/issues/221) Improved how servers are handled, displaying meaningful errors when a not-found or a not-reachable server is tried to be loaded.
|
||||
* [#226](https://github.com/shlinkio/shlink-web-client/issues/226) Created servers can now be edited.
|
||||
|
||||
#### Changed
|
||||
|
||||
|
|
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -5340,7 +5340,7 @@
|
|||
},
|
||||
"discontinuous-range": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz",
|
||||
"resolved": "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz",
|
||||
"integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=",
|
||||
"dev": true
|
||||
},
|
||||
|
@ -9346,7 +9346,7 @@
|
|||
},
|
||||
"is-subset": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz",
|
||||
"resolved": "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz",
|
||||
"integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=",
|
||||
"dev": true
|
||||
},
|
||||
|
@ -10557,13 +10557,13 @@
|
|||
},
|
||||
"lodash.escape": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz",
|
||||
"resolved": "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz",
|
||||
"integrity": "sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.flattendeep": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
|
||||
"resolved": "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
|
||||
"integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
|
||||
"dev": true
|
||||
},
|
||||
|
@ -10575,7 +10575,7 @@
|
|||
},
|
||||
"lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"resolved": "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
|
||||
"dev": true
|
||||
},
|
||||
|
@ -13817,7 +13817,7 @@
|
|||
},
|
||||
"railroad-diagrams": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
|
||||
"resolved": "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
|
||||
"integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=",
|
||||
"dev": true
|
||||
},
|
||||
|
@ -14977,7 +14977,7 @@
|
|||
},
|
||||
"rst-selector-parser": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz",
|
||||
"resolved": "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz",
|
||||
"integrity": "sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
|
|
@ -3,14 +3,15 @@ import { Route, Switch } from 'react-router-dom';
|
|||
import './App.scss';
|
||||
import NotFound from './common/NotFound';
|
||||
|
||||
const App = (MainHeader, Home, MenuLayout, CreateServer) => () => (
|
||||
const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer) => () => (
|
||||
<div className="container-fluid app-container">
|
||||
<MainHeader />
|
||||
|
||||
<div className="app">
|
||||
<Switch>
|
||||
<Route exact path="/server/create" component={CreateServer} />
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route exact path="/server/create" component={CreateServer} />
|
||||
<Route exact path="/server/:serverId/edit" component={EditServer} />
|
||||
<Route path="/server/:serverId" component={MenuLayout} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { faList as listIcon, faLink as createIcon, faTags as tagsIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import {
|
||||
faList as listIcon,
|
||||
faLink as createIcon,
|
||||
faTags as tagsIcon,
|
||||
faPen as editIcon,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
@ -7,8 +12,13 @@ import classNames from 'classnames';
|
|||
import { serverType } from '../servers/prop-types';
|
||||
import './AsideMenu.scss';
|
||||
|
||||
const AsideMenuItem = ({ children, to, ...rest }) => (
|
||||
<NavLink className="aside-menu__item" activeClassName="aside-menu__item--selected" to={to} {...rest}>
|
||||
const AsideMenuItem = ({ children, to, className, ...rest }) => (
|
||||
<NavLink
|
||||
className={classNames('aside-menu__item', className)}
|
||||
activeClassName="aside-menu__item--selected"
|
||||
to={to}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</NavLink>
|
||||
);
|
||||
|
@ -16,6 +26,7 @@ const AsideMenuItem = ({ children, to, ...rest }) => (
|
|||
AsideMenuItem.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
to: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
|
@ -48,8 +59,15 @@ const AsideMenu = (DeleteServerButton) => {
|
|||
<FontAwesomeIcon icon={tagsIcon} />
|
||||
<span className="aside-menu__item-text">Manage tags</span>
|
||||
</AsideMenuItem>
|
||||
|
||||
<DeleteServerButton className="aside-menu__item aside-menu__item--danger" server={selectedServer} />
|
||||
<AsideMenuItem to={buildPath('/edit')} className="aside-menu__item--push">
|
||||
<FontAwesomeIcon icon={editIcon} />
|
||||
<span className="aside-menu__item-text">Edit this server</span>
|
||||
</AsideMenuItem>
|
||||
<DeleteServerButton
|
||||
className="aside-menu__item aside-menu__item--danger"
|
||||
textClassName="aside-menu__item-text"
|
||||
server={selectedServer}
|
||||
/>
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
|
|
|
@ -67,6 +67,9 @@ $asideMenuMobileWidth: 280px;
|
|||
|
||||
.aside-menu__item--danger {
|
||||
color: $dangerColor;
|
||||
}
|
||||
|
||||
.aside-menu__item--push {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,35 +6,23 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||
import classNames from 'classnames';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { serverType } from '../servers/prop-types';
|
||||
import Message from '../utils/Message';
|
||||
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
|
||||
import NotFound from './NotFound';
|
||||
import './MenuLayout.scss';
|
||||
|
||||
const propTypes = {
|
||||
match: PropTypes.object,
|
||||
selectServer: PropTypes.func,
|
||||
location: PropTypes.object,
|
||||
selectedServer: serverType,
|
||||
};
|
||||
|
||||
const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisits, ShlinkVersions, ServerError) => {
|
||||
const MenuLayoutComp = ({ match, location, selectedServer, selectServer }) => {
|
||||
const MenuLayoutComp = ({ match, location, selectedServer }) => {
|
||||
const [ showSideBar, setShowSidebar ] = useState(false);
|
||||
const { params: { serverId } } = match;
|
||||
|
||||
useEffect(() => {
|
||||
selectServer(serverId);
|
||||
}, [ serverId ]);
|
||||
useEffect(() => setShowSidebar(false), [ location ]);
|
||||
|
||||
if (!selectedServer) {
|
||||
return <Message loading />;
|
||||
}
|
||||
|
||||
if (selectedServer.serverNotFound) {
|
||||
return <ServerError type="not-found" />;
|
||||
}
|
||||
|
||||
if (selectedServer.serverNotReachable) {
|
||||
return <ServerError type="not-reachable" />;
|
||||
}
|
||||
|
@ -91,7 +79,7 @@ const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisi
|
|||
|
||||
MenuLayoutComp.propTypes = propTypes;
|
||||
|
||||
return MenuLayoutComp;
|
||||
return withSelectedServer(MenuLayoutComp, ServerError);
|
||||
};
|
||||
|
||||
export default MenuLayout;
|
||||
|
|
|
@ -26,7 +26,7 @@ const connect = (propsFromState, actionServiceNames = []) =>
|
|||
actionServiceNames.reduce(mapActionService, {})
|
||||
);
|
||||
|
||||
bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer');
|
||||
bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer', 'EditServer');
|
||||
|
||||
provideCommonServices(bottle, connect, withRouter);
|
||||
provideShortUrlsServices(bottle, connect);
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { assoc, dissoc, pipe } from 'ramda';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import PropTypes from 'prop-types';
|
||||
import './CreateServer.scss';
|
||||
import { ServerForm } from './helpers/ServerForm';
|
||||
|
||||
const SHOW_IMPORT_MSG_TIME = 4000;
|
||||
|
||||
const CreateServer = (ImportServersBtn, stateFlagTimeout) => class CreateServer extends React.Component {
|
||||
static propTypes = {
|
||||
const propTypes = {
|
||||
createServer: PropTypes.func,
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func,
|
||||
|
@ -15,65 +13,29 @@ const CreateServer = (ImportServersBtn, stateFlagTimeout) => class CreateServer
|
|||
resetSelectedServer: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
name: '',
|
||||
url: '',
|
||||
apiKey: '',
|
||||
serversImported: false,
|
||||
};
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { createServer, history: { push } } = this.props;
|
||||
const server = pipe(
|
||||
assoc('id', uuid()),
|
||||
dissoc('serversImported')
|
||||
)(this.state);
|
||||
const CreateServer = (ImportServersBtn, useStateFlagTimeout) => {
|
||||
const CreateServerComp = ({ createServer, history: { push }, resetSelectedServer }) => {
|
||||
const [ serversImported, setServersImported ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
|
||||
const handleSubmit = (serverData) => {
|
||||
const id = uuid();
|
||||
const server = { id, ...serverData };
|
||||
|
||||
createServer(server);
|
||||
push(`/server/${server.id}/list-short-urls/1`);
|
||||
push(`/server/${id}/list-short-urls/1`);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.resetSelectedServer();
|
||||
}
|
||||
|
||||
render() {
|
||||
const renderInputGroup = (id, placeholder, type = 'text') => (
|
||||
<div className="form-group row">
|
||||
<label htmlFor={id} className="col-lg-1 col-md-2 col-form-label create-server__label">
|
||||
{placeholder}:
|
||||
</label>
|
||||
<div className="col-lg-11 col-md-10">
|
||||
<input
|
||||
type={type}
|
||||
className="form-control"
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
value={this.state[id]}
|
||||
required
|
||||
onChange={(e) => this.setState({ [id]: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
useEffect(() => {
|
||||
resetSelectedServer();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="create-server">
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
{renderInputGroup('name', 'Name')}
|
||||
{renderInputGroup('url', 'URL', 'url')}
|
||||
{renderInputGroup('apiKey', 'API key')}
|
||||
|
||||
<div className="text-right">
|
||||
<ImportServersBtn
|
||||
onImport={() => stateFlagTimeout(this.setState.bind(this), 'serversImported', true, SHOW_IMPORT_MSG_TIME)}
|
||||
/>
|
||||
<ServerForm onSubmit={handleSubmit}>
|
||||
<ImportServersBtn onImport={setServersImported} />
|
||||
<button className="btn btn-outline-primary">Create server</button>
|
||||
</div>
|
||||
</ServerForm>
|
||||
|
||||
{this.state.serversImported && (
|
||||
{serversImported && (
|
||||
<div className="row create-server__import-success-msg">
|
||||
<div className="col-md-10 offset-md-1">
|
||||
<div className="p-2 mt-3 bg-main text-white text-center">
|
||||
|
@ -82,10 +44,13 @@ const CreateServer = (ImportServersBtn, stateFlagTimeout) => class CreateServer
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CreateServerComp.propTypes = propTypes;
|
||||
|
||||
return CreateServerComp;
|
||||
};
|
||||
|
||||
export default CreateServer;
|
||||
|
|
|
@ -7,25 +7,22 @@ import { serverType } from './prop-types';
|
|||
const propTypes = {
|
||||
server: serverType,
|
||||
className: PropTypes.string,
|
||||
textClassName: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
const DeleteServerButton = (DeleteServerModal) => {
|
||||
const DeleteServerButtonComp = ({ server, className }) => {
|
||||
const DeleteServerButtonComp = ({ server, className, children, textClassName }) => {
|
||||
const [ isModalOpen, setModalOpen ] = useState(false);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span className={className} key="deleteServerBtn" onClick={() => setModalOpen(true)}>
|
||||
<FontAwesomeIcon icon={deleteIcon} />
|
||||
<span className="aside-menu__item-text">Remove this server</span>
|
||||
<span className={className} onClick={() => setModalOpen(true)}>
|
||||
{!children && <FontAwesomeIcon icon={deleteIcon} />}
|
||||
<span className={textClassName}>{children || 'Remove this server'}</span>
|
||||
</span>
|
||||
|
||||
<DeleteServerModal
|
||||
isOpen={isModalOpen}
|
||||
toggle={() => setModalOpen(!isModalOpen)}
|
||||
server={server}
|
||||
key="deleteServerModal"
|
||||
/>
|
||||
<DeleteServerModal server={server} isOpen={isModalOpen} toggle={() => setModalOpen(!isModalOpen)} />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
|
34
src/servers/EditServer.js
Normal file
34
src/servers/EditServer.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ServerForm } from './helpers/ServerForm';
|
||||
import { withSelectedServer } from './helpers/withSelectedServer';
|
||||
import { serverType } from './prop-types';
|
||||
|
||||
const propTypes = {
|
||||
editServer: PropTypes.func,
|
||||
selectedServer: serverType,
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func,
|
||||
}),
|
||||
};
|
||||
|
||||
export const EditServer = (ServerError) => {
|
||||
const EditServerComp = ({ editServer, selectedServer, history: { push } }) => {
|
||||
const handleSubmit = (serverData) => {
|
||||
editServer(selectedServer.id, serverData);
|
||||
push(`/server/${selectedServer.id}/list-short-urls/1`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="create-server">
|
||||
<ServerForm initialValues={selectedServer} onSubmit={handleSubmit}>
|
||||
<button className="btn btn-outline-primary">Save</button>
|
||||
</ServerForm>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
EditServerComp.propTypes = propTypes;
|
||||
|
||||
return withSelectedServer(EditServerComp, ServerError);
|
||||
};
|
|
@ -26,7 +26,9 @@ ServerListItem.propTypes = {
|
|||
|
||||
const ServersListGroup = ({ servers, children }) => (
|
||||
<React.Fragment>
|
||||
<div className="container">
|
||||
<h5>{children}</h5>
|
||||
</div>
|
||||
{servers.length > 0 && (
|
||||
<ListGroup className="servers-list__list-group mt-md-3">
|
||||
{servers.map(({ id, name }) => <ServerListItem key={id} id={id} name={name} />)}
|
||||
|
|
|
@ -3,15 +3,18 @@ import PropTypes from 'prop-types';
|
|||
import { Link } from 'react-router-dom';
|
||||
import Message from '../../utils/Message';
|
||||
import ServersListGroup from '../ServersListGroup';
|
||||
import { serverType } from '../prop-types';
|
||||
import './ServerError.scss';
|
||||
|
||||
const propTypes = {
|
||||
servers: PropTypes.object,
|
||||
selectedServer: serverType,
|
||||
type: PropTypes.oneOf([ 'not-found', 'not-reachable' ]).isRequired,
|
||||
};
|
||||
|
||||
export const ServerError = ({ type, servers: { list } }) => (
|
||||
<div className="server-error-container flex-column">
|
||||
export const ServerError = (DeleteServerButton) => {
|
||||
const ServerErrorComp = ({ type, servers: { list }, selectedServer }) => (
|
||||
<div className="server-error__container flex-column">
|
||||
<div className="row w-100 mb-3 mb-md-5">
|
||||
<Message type="error">
|
||||
{type === 'not-found' && 'Could not find this Shlink server.'}
|
||||
|
@ -23,11 +26,25 @@ export const ServerError = ({ type, servers: { list } }) => (
|
|||
)}
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<ServersListGroup servers={Object.values(list)}>
|
||||
These are the {type === 'not-reachable' ? 'other' : ''} Shlink servers currently configured. Choose one of
|
||||
These are the Shlink servers currently configured. Choose one of
|
||||
them or <Link to="/server/create">add a new one</Link>.
|
||||
</ServersListGroup>
|
||||
|
||||
{type === 'not-reachable' && (
|
||||
<div className="container mt-3 mt-md-5">
|
||||
<h5>
|
||||
Alternatively, if you think you may have miss-configured this server, you
|
||||
can <DeleteServerButton server={selectedServer} className="server-error__delete-btn">remove it</DeleteServerButton> or
|
||||
<Link to={`/server/${selectedServer.id}/edit`}>edit it</Link>.
|
||||
</h5>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
ServerError.propTypes = propTypes;
|
||||
ServerErrorComp.propTypes = propTypes;
|
||||
|
||||
return ServerErrorComp;
|
||||
};
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
.server-error-container {
|
||||
@import '../../utils/base';
|
||||
|
||||
.server-error__container {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.server-error__delete-btn {
|
||||
color: $dangerColor;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.server-error__delete-btn:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
41
src/servers/helpers/ServerForm.js
Normal file
41
src/servers/helpers/ServerForm.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { HorizontalFormGroup } from '../../utils/HorizontalFormGroup';
|
||||
|
||||
const propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
initialValues: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
apiKey: PropTypes.string.isRequired,
|
||||
}),
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export const ServerForm = ({ onSubmit, initialValues, children }) => {
|
||||
const [ name, setName ] = useState('');
|
||||
const [ url, setUrl ] = useState('');
|
||||
const [ apiKey, setApiKey ] = useState('');
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onSubmit({ name, url, apiKey });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initialValues && setName(initialValues.name);
|
||||
initialValues && setUrl(initialValues.url);
|
||||
initialValues && setApiKey(initialValues.apiKey);
|
||||
}, [ initialValues ]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<HorizontalFormGroup value={name} onChange={setName}>Name</HorizontalFormGroup>
|
||||
<HorizontalFormGroup type="url" value={url} onChange={setUrl}>URL</HorizontalFormGroup>
|
||||
<HorizontalFormGroup value={apiKey} onChange={setApiKey}>API key</HorizontalFormGroup>
|
||||
|
||||
<div className="text-right">{children}</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
ServerForm.propTypes = propTypes;
|
35
src/servers/helpers/withSelectedServer.js
Normal file
35
src/servers/helpers/withSelectedServer.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Message from '../../utils/Message';
|
||||
import { serverType } from '../prop-types';
|
||||
|
||||
const propTypes = {
|
||||
selectServer: PropTypes.func,
|
||||
selectedServer: serverType,
|
||||
match: PropTypes.object,
|
||||
};
|
||||
|
||||
export const withSelectedServer = (WrappedComponent, ServerError) => {
|
||||
const Component = (props) => {
|
||||
const { selectServer, selectedServer, match } = props;
|
||||
const { params: { serverId } } = match;
|
||||
|
||||
useEffect(() => {
|
||||
selectServer(serverId);
|
||||
}, [ serverId ]);
|
||||
|
||||
if (!selectedServer) {
|
||||
return <Message loading />;
|
||||
}
|
||||
|
||||
if (selectedServer.serverNotFound) {
|
||||
return <ServerError type="not-found" />;
|
||||
}
|
||||
|
||||
return <WrappedComponent {...props} />;
|
||||
};
|
||||
|
||||
Component.propTypes = propTypes;
|
||||
|
||||
return Component;
|
||||
};
|
|
@ -7,18 +7,14 @@ const regularServerType = PropTypes.shape({
|
|||
apiKey: PropTypes.string,
|
||||
version: PropTypes.string,
|
||||
printableVersion: PropTypes.string,
|
||||
serverNotReachable: PropTypes.bool,
|
||||
});
|
||||
|
||||
const notFoundServerType = PropTypes.shape({
|
||||
serverNotFound: PropTypes.bool.isRequired,
|
||||
});
|
||||
|
||||
const notReachableServerType = PropTypes.shape({
|
||||
serverNotReachable: PropTypes.bool.isRequired,
|
||||
});
|
||||
|
||||
export const serverType = PropTypes.oneOfType([
|
||||
regularServerType,
|
||||
notFoundServerType,
|
||||
notReachableServerType,
|
||||
]);
|
||||
|
|
|
@ -49,7 +49,7 @@ export const selectServer = ({ findServerById }, buildShlinkApiClient) => (serve
|
|||
} catch (e) {
|
||||
dispatch({
|
||||
type: SELECT_SERVER,
|
||||
selectedServer: { serverNotReachable: true },
|
||||
selectedServer: { ...selectedServer, serverNotReachable: true },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -52,6 +52,8 @@ export const listServers = ({ listServers, createServers }, { get }) => () => as
|
|||
|
||||
export const createServer = ({ createServer }, listServersAction) => pipe(createServer, listServersAction);
|
||||
|
||||
export const editServer = ({ editServer }, listServersAction) => pipe(editServer, listServersAction);
|
||||
|
||||
export const deleteServer = ({ deleteServer }, listServersAction) => pipe(deleteServer, listServersAction);
|
||||
|
||||
export const createServers = ({ createServers }, listServersAction) => pipe(
|
||||
|
|
|
@ -25,4 +25,14 @@ export default class ServersService {
|
|||
|
||||
deleteServer = ({ id }) =>
|
||||
this.storage.set(SERVERS_STORAGE_KEY, dissoc(id, this.listServers()));
|
||||
|
||||
editServer = (id, serverData) => {
|
||||
const allServers = this.listServers();
|
||||
|
||||
if (!allServers[id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.storage.set(SERVERS_STORAGE_KEY, assoc(id, { ...allServers[id], ...serverData }, allServers));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,10 @@ import CreateServer from '../CreateServer';
|
|||
import ServersDropdown from '../ServersDropdown';
|
||||
import DeleteServerModal from '../DeleteServerModal';
|
||||
import DeleteServerButton from '../DeleteServerButton';
|
||||
import { EditServer } from '../EditServer';
|
||||
import ImportServersBtn from '../helpers/ImportServersBtn';
|
||||
import { resetSelectedServer, selectServer } from '../reducers/selectedServer';
|
||||
import { createServer, createServers, deleteServer, listServers } from '../reducers/server';
|
||||
import { createServer, createServers, deleteServer, editServer, listServers } from '../reducers/server';
|
||||
import ForServerVersion from '../helpers/ForServerVersion';
|
||||
import { ServerError } from '../helpers/ServerError';
|
||||
import ServersImporter from './ServersImporter';
|
||||
|
@ -14,9 +15,12 @@ import ServersExporter from './ServersExporter';
|
|||
|
||||
const provideServices = (bottle, connect, withRouter) => {
|
||||
// Components
|
||||
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'stateFlagTimeout');
|
||||
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useStateFlagTimeout');
|
||||
bottle.decorator('CreateServer', connect([ 'selectedServer' ], [ 'createServer', 'resetSelectedServer' ]));
|
||||
|
||||
bottle.serviceFactory('EditServer', EditServer, 'ServerError');
|
||||
bottle.decorator('EditServer', connect([ 'selectedServer' ], [ 'editServer', 'selectServer' ]));
|
||||
|
||||
bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter');
|
||||
bottle.decorator('ServersDropdown', withRouter);
|
||||
bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ], [ 'listServers' ]));
|
||||
|
@ -33,8 +37,8 @@ const provideServices = (bottle, connect, withRouter) => {
|
|||
bottle.serviceFactory('ForServerVersion', () => ForServerVersion);
|
||||
bottle.decorator('ForServerVersion', connect([ 'selectedServer' ]));
|
||||
|
||||
bottle.serviceFactory('ServerError', () => ServerError);
|
||||
bottle.decorator('ServerError', connect([ 'servers' ]));
|
||||
bottle.serviceFactory('ServerError', ServerError, 'DeleteServerButton');
|
||||
bottle.decorator('ServerError', connect([ 'servers', 'selectedServer' ]));
|
||||
|
||||
// Services
|
||||
bottle.constant('csvjson', csvjson);
|
||||
|
@ -47,6 +51,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('editServer', editServer, 'ServersService', 'listServers');
|
||||
bottle.serviceFactory('listServers', listServers, 'ServersService', 'axios');
|
||||
|
||||
bottle.serviceFactory('resetSelectedServer', () => resetSelectedServer);
|
||||
|
|
32
src/utils/HorizontalFormGroup.js
Normal file
32
src/utils/HorizontalFormGroup.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
id: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
};
|
||||
|
||||
export const HorizontalFormGroup = ({ children, value, onChange, id = uuid(), type = 'text', required = true }) => (
|
||||
<div className="form-group row">
|
||||
<label htmlFor={id} className="col-lg-1 col-md-2 col-form-label create-server__label">
|
||||
{children}:
|
||||
</label>
|
||||
<div className="col-lg-11 col-md-10">
|
||||
<input
|
||||
className="form-control"
|
||||
type={type}
|
||||
id={id}
|
||||
value={value}
|
||||
required={required}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
HorizontalFormGroup.propTypes = propTypes;
|
|
@ -9,7 +9,7 @@ describe('<App />', () => {
|
|||
const MainHeader = () => '';
|
||||
|
||||
beforeEach(() => {
|
||||
const App = appFactory(MainHeader, identity, identity, identity);
|
||||
const App = appFactory(MainHeader, identity, identity, identity, identity);
|
||||
|
||||
wrapper = shallow(<App />);
|
||||
});
|
||||
|
@ -20,13 +20,14 @@ describe('<App />', () => {
|
|||
it('renders app main routes', () => {
|
||||
const routes = wrapper.find(Route);
|
||||
const expectedPaths = [
|
||||
'/server/create',
|
||||
'/',
|
||||
'/server/create',
|
||||
'/server/:serverId/edit',
|
||||
'/server/:serverId',
|
||||
];
|
||||
|
||||
expect.assertions(expectedPaths.length + 1);
|
||||
expect(routes).toHaveLength(4);
|
||||
expect(routes).toHaveLength(expectedPaths.length + 1);
|
||||
expectedPaths.forEach((path, index) => {
|
||||
expect(routes.at(index).prop('path')).toEqual(path);
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ describe('<AsideMenu />', () => {
|
|||
it('contains links to different sections', () => {
|
||||
const links = wrapped.find('[to]');
|
||||
|
||||
expect(links).toHaveLength(3);
|
||||
expect(links).toHaveLength(4);
|
||||
links.forEach((link) => expect(link.prop('to')).toContain('abc123'));
|
||||
});
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import { shallow } from 'enzyme';
|
||||
import { identity } from 'ramda';
|
||||
import createServerConstruct from '../../src/servers/CreateServer';
|
||||
import { ServerForm } from '../../src/servers/helpers/ServerForm';
|
||||
|
||||
describe('<CreateServer />', () => {
|
||||
let wrapper;
|
||||
|
@ -10,53 +11,41 @@ describe('<CreateServer />', () => {
|
|||
const historyMock = {
|
||||
push: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createServerMock.mockReset();
|
||||
|
||||
const CreateServer = createServerConstruct(ImportServersBtn);
|
||||
const createWrapper = (serversImported = false) => {
|
||||
const CreateServer = createServerConstruct(ImportServersBtn, () => [ serversImported, () => '' ]);
|
||||
|
||||
wrapper = shallow(
|
||||
<CreateServer createServer={createServerMock} resetSelectedServer={identity} history={historyMock} />
|
||||
);
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
wrapper && wrapper.unmount();
|
||||
});
|
||||
afterEach(() => wrapper.unmount());
|
||||
|
||||
it('renders components', () => {
|
||||
expect(wrapper.find('#name')).toHaveLength(1);
|
||||
expect(wrapper.find('#url')).toHaveLength(1);
|
||||
expect(wrapper.find('#apiKey')).toHaveLength(1);
|
||||
expect(wrapper.find(ImportServersBtn)).toHaveLength(1);
|
||||
const wrapper = createWrapper();
|
||||
|
||||
expect(wrapper.find(ServerForm)).toHaveLength(1);
|
||||
expect(wrapper.find('.create-server__import-success-msg')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('shows success message when imported is true', () => {
|
||||
wrapper.setState({ serversImported: true });
|
||||
const wrapper = createWrapper(true);
|
||||
|
||||
expect(wrapper.find('.create-server__import-success-msg')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('creates server and redirects to it when form is submitted', () => {
|
||||
const form = wrapper.find('form');
|
||||
const wrapper = createWrapper();
|
||||
const form = wrapper.find(ServerForm);
|
||||
|
||||
form.simulate('submit', { preventDefault() {
|
||||
return '';
|
||||
} });
|
||||
form.simulate('submit', {});
|
||||
|
||||
expect(createServerMock).toHaveBeenCalledTimes(1);
|
||||
expect(historyMock.push).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('updates state when inputs are changed', () => {
|
||||
const nameInput = wrapper.find('#name');
|
||||
const urlInput = wrapper.find('#url');
|
||||
const apiKeyInput = wrapper.find('#apiKey');
|
||||
|
||||
nameInput.simulate('change', { target: { value: 'the_name' } });
|
||||
urlInput.simulate('change', { target: { value: 'the_url' } });
|
||||
apiKeyInput.simulate('change', { target: { value: 'the_api_key' } });
|
||||
|
||||
expect(wrapper.state('name')).toEqual('the_name');
|
||||
expect(wrapper.state('url')).toEqual('the_url');
|
||||
expect(wrapper.state('apiKey')).toEqual('the_api_key');
|
||||
});
|
||||
});
|
||||
|
|
52
test/servers/EditServer.test.js
Normal file
52
test/servers/EditServer.test.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { EditServer as editServerConstruct } from '../../src/servers/EditServer';
|
||||
import { ServerForm } from '../../src/servers/helpers/ServerForm';
|
||||
|
||||
describe('<EditServer />', () => {
|
||||
let wrapper;
|
||||
const ServerError = jest.fn();
|
||||
const editServerMock = jest.fn();
|
||||
const historyMock = { push: jest.fn() };
|
||||
const match = {
|
||||
params: { serverId: 'abc123' },
|
||||
};
|
||||
const selectedServer = {
|
||||
id: 'abc123',
|
||||
name: 'name',
|
||||
url: 'url',
|
||||
apiKey: 'apiKey',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const EditServer = editServerConstruct(ServerError);
|
||||
|
||||
wrapper = mount(
|
||||
<EditServer
|
||||
editServer={editServerMock}
|
||||
history={historyMock}
|
||||
match={match}
|
||||
selectedServer={selectedServer}
|
||||
selectServer={jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
wrapper && wrapper.unmount();
|
||||
});
|
||||
|
||||
it('renders components', () => {
|
||||
expect(wrapper.find(ServerForm)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('edits server and redirects to it when form is submitted', () => {
|
||||
const form = wrapper.find(ServerForm);
|
||||
|
||||
form.simulate('submit', {});
|
||||
|
||||
expect(editServerMock).toHaveBeenCalledTimes(1);
|
||||
expect(historyMock.push).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -1,35 +1,49 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { ServerError } from '../../../src/servers/helpers/ServerError';
|
||||
import { ServerError as createServerError } from '../../../src/servers/helpers/ServerError';
|
||||
|
||||
describe('<ServerError />', () => {
|
||||
let wrapper;
|
||||
const selectedServer = { id: '' };
|
||||
const ServerError = createServerError(() => '');
|
||||
|
||||
afterEach(() => wrapper && wrapper.unmount());
|
||||
|
||||
it.each([
|
||||
[
|
||||
'not-found',
|
||||
[ 'Could not find this Shlink server.' ],
|
||||
'These are the Shlink servers',
|
||||
{
|
||||
'Could not find this Shlink server.': true,
|
||||
'Oops! Could not connect to this Shlink server.': false,
|
||||
'Make sure you have internet connection, and the server is properly configured and on-line.': false,
|
||||
'Alternatively, if you think you may have miss-configured this server': false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'not-reachable',
|
||||
[
|
||||
'Oops! Could not connect to this Shlink server.',
|
||||
'Make sure you have internet connection, and the server is properly configured and on-line.',
|
||||
{
|
||||
'Could not find this Shlink server.': false,
|
||||
'Oops! Could not connect to this Shlink server.': true,
|
||||
'Make sure you have internet connection, and the server is properly configured and on-line.': true,
|
||||
'Alternatively, if you think you may have miss-configured this server': true,
|
||||
},
|
||||
],
|
||||
'These are the other Shlink servers',
|
||||
],
|
||||
])('renders expected information based on type', (type, expectedTitleParts, expectedBody) => {
|
||||
wrapper = shallow(<BrowserRouter><ServerError type={type} servers={{ list: [] }} /></BrowserRouter>);
|
||||
])('renders expected information for type "%s"', (type, textsToFind) => {
|
||||
wrapper = shallow(
|
||||
<BrowserRouter>
|
||||
<ServerError type={type} servers={{ list: [] }} selectedServer={selectedServer} />
|
||||
</BrowserRouter>
|
||||
);
|
||||
const wrapperText = wrapper.html();
|
||||
const textsToFind = [ ...expectedTitleParts, ...expectedBody ];
|
||||
const textPairs = Object.entries(textsToFind);
|
||||
|
||||
expect.assertions(textsToFind.length);
|
||||
textsToFind.forEach((text) => {
|
||||
textPairs.forEach(([ text, shouldBeFound ]) => {
|
||||
if (shouldBeFound) {
|
||||
expect(wrapperText).toContain(text);
|
||||
} else {
|
||||
expect(wrapperText).not.toContain(text);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
33
test/servers/helpers/ServerForm.test.js
Normal file
33
test/servers/helpers/ServerForm.test.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { ServerForm } from '../../../src/servers/helpers/ServerForm';
|
||||
import { HorizontalFormGroup } from '../../../src/utils/HorizontalFormGroup';
|
||||
|
||||
describe('<ServerForm />', () => {
|
||||
let wrapper;
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<ServerForm onSubmit={onSubmit}><span>Something</span></ServerForm>);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
wrapper && wrapper.unmount();
|
||||
});
|
||||
|
||||
it('renders components', () => {
|
||||
expect(wrapper.find(HorizontalFormGroup)).toHaveLength(3);
|
||||
expect(wrapper.find('span')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('invokes submit callback when submit event is triggered', () => {
|
||||
const form = wrapper.find('form');
|
||||
const preventDefault = jest.fn();
|
||||
|
||||
form.simulate('submit', { preventDefault });
|
||||
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
expect(onSubmit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -72,7 +72,7 @@ describe('selectedServerReducer', () => {
|
|||
});
|
||||
|
||||
it('dispatches error when health endpoint fails', async () => {
|
||||
const expectedSelectedServer = { serverNotReachable: true };
|
||||
const expectedSelectedServer = { ...selectedServer, serverNotReachable: true };
|
||||
|
||||
apiClientMock.health.mockRejectedValue({});
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import reducer, {
|
|||
deleteServer,
|
||||
listServers,
|
||||
createServers,
|
||||
FETCH_SERVERS, FETCH_SERVERS_START,
|
||||
FETCH_SERVERS, FETCH_SERVERS_START, editServer,
|
||||
} from '../../../src/servers/reducers/server';
|
||||
|
||||
describe('serverReducer', () => {
|
||||
|
@ -16,6 +16,7 @@ describe('serverReducer', () => {
|
|||
const ServersServiceMock = {
|
||||
listServers: jest.fn(() => list),
|
||||
createServer: jest.fn(),
|
||||
editServer: jest.fn(),
|
||||
deleteServer: jest.fn(),
|
||||
createServers: jest.fn(),
|
||||
};
|
||||
|
@ -41,6 +42,7 @@ describe('serverReducer', () => {
|
|||
expect(dispatch).toHaveBeenNthCalledWith(2, expectedFetchServersResult);
|
||||
expect(ServersServiceMock.listServers).toHaveBeenCalledTimes(1);
|
||||
expect(ServersServiceMock.createServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.editServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.deleteServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServers).not.toHaveBeenCalled();
|
||||
expect(axios.get).not.toHaveBeenCalled();
|
||||
|
@ -91,6 +93,7 @@ describe('serverReducer', () => {
|
|||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: FETCH_SERVERS, list: expectedList });
|
||||
expect(NoListServersServiceMock.listServers).toHaveBeenCalledTimes(1);
|
||||
expect(NoListServersServiceMock.createServer).not.toHaveBeenCalled();
|
||||
expect(NoListServersServiceMock.editServer).not.toHaveBeenCalled();
|
||||
expect(NoListServersServiceMock.deleteServer).not.toHaveBeenCalled();
|
||||
expect(NoListServersServiceMock.createServers).toHaveBeenCalledTimes(1);
|
||||
expect(axios.get).toHaveBeenCalledTimes(1);
|
||||
|
@ -103,9 +106,25 @@ describe('serverReducer', () => {
|
|||
const result = createServer(ServersServiceMock, () => expectedFetchServersResult)(serverToCreate);
|
||||
|
||||
expect(result).toEqual(expectedFetchServersResult);
|
||||
expect(ServersServiceMock.listServers).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServer).toHaveBeenCalledTimes(1);
|
||||
expect(ServersServiceMock.createServer).toHaveBeenCalledWith(serverToCreate);
|
||||
expect(ServersServiceMock.editServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.deleteServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServers).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('editServer', () => {
|
||||
it('edits existing server and then fetches servers again', () => {
|
||||
const serverToEdit = { name: 'edited' };
|
||||
const result = editServer(ServersServiceMock, () => expectedFetchServersResult)('123', serverToEdit);
|
||||
|
||||
expect(result).toEqual(expectedFetchServersResult);
|
||||
expect(ServersServiceMock.listServers).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.editServer).toHaveBeenCalledTimes(1);
|
||||
expect(ServersServiceMock.editServer).toHaveBeenCalledWith('123', serverToEdit);
|
||||
expect(ServersServiceMock.deleteServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServers).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -120,12 +139,13 @@ describe('serverReducer', () => {
|
|||
expect(ServersServiceMock.listServers).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServers).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.editServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.deleteServer).toHaveBeenCalledTimes(1);
|
||||
expect(ServersServiceMock.deleteServer).toHaveBeenCalledWith(serverToDelete);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createServer', () => {
|
||||
describe('createServers', () => {
|
||||
it('creates multiple servers and then fetches servers again', () => {
|
||||
const serversToCreate = values(list);
|
||||
const result = createServers(ServersServiceMock, () => expectedFetchServersResult)(serversToCreate);
|
||||
|
@ -133,9 +153,10 @@ describe('serverReducer', () => {
|
|||
expect(result).toEqual(expectedFetchServersResult);
|
||||
expect(ServersServiceMock.listServers).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.editServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.deleteServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServers).toHaveBeenCalledTimes(1);
|
||||
expect(ServersServiceMock.createServers).toHaveBeenCalledWith(serversToCreate);
|
||||
expect(ServersServiceMock.deleteServer).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,15 +5,19 @@ describe('ServersService', () => {
|
|||
abc123: { id: 'abc123' },
|
||||
def456: { id: 'def456' },
|
||||
};
|
||||
const createStorageMock = (returnValue) => ({
|
||||
const createService = (withServers = true) => {
|
||||
const storageMock = {
|
||||
set: jest.fn(),
|
||||
get: jest.fn(() => returnValue),
|
||||
});
|
||||
get: jest.fn(() => withServers ? servers : undefined),
|
||||
};
|
||||
const service = new ServersService(storageMock);
|
||||
|
||||
return [ service, storageMock ];
|
||||
};
|
||||
|
||||
describe('listServers', () => {
|
||||
it('returns an empty object when servers are not found in storage', () => {
|
||||
const storageMock = createStorageMock();
|
||||
const service = new ServersService(storageMock);
|
||||
const [ service, storageMock ] = createService(false);
|
||||
|
||||
const result = service.listServers();
|
||||
|
||||
|
@ -23,8 +27,7 @@ describe('ServersService', () => {
|
|||
});
|
||||
|
||||
it('returns value from storage when found', () => {
|
||||
const storageMock = createStorageMock(servers);
|
||||
const service = new ServersService(storageMock);
|
||||
const [ service, storageMock ] = createService();
|
||||
|
||||
const result = service.listServers();
|
||||
|
||||
|
@ -36,8 +39,7 @@ describe('ServersService', () => {
|
|||
|
||||
describe('findServerById', () => {
|
||||
it('returns undefined when requested server is not found', () => {
|
||||
const storageMock = createStorageMock(servers);
|
||||
const service = new ServersService(storageMock);
|
||||
const [ service, storageMock ] = createService();
|
||||
|
||||
const result = service.findServerById('ghi789');
|
||||
|
||||
|
@ -47,8 +49,7 @@ describe('ServersService', () => {
|
|||
});
|
||||
|
||||
it('returns server from list when found', () => {
|
||||
const storageMock = createStorageMock(servers);
|
||||
const service = new ServersService(storageMock);
|
||||
const [ service, storageMock ] = createService();
|
||||
|
||||
const result = service.findServerById('abc123');
|
||||
|
||||
|
@ -60,8 +61,7 @@ describe('ServersService', () => {
|
|||
|
||||
describe('createServer', () => {
|
||||
it('adds one server to the list', () => {
|
||||
const storageMock = createStorageMock(servers);
|
||||
const service = new ServersService(storageMock);
|
||||
const [ service, storageMock ] = createService();
|
||||
|
||||
service.createServer({ id: 'ghi789' });
|
||||
|
||||
|
@ -77,8 +77,7 @@ describe('ServersService', () => {
|
|||
|
||||
describe('createServers', () => {
|
||||
it('adds multiple servers to the list', () => {
|
||||
const storageMock = createStorageMock(servers);
|
||||
const service = new ServersService(storageMock);
|
||||
const [ service, storageMock ] = createService();
|
||||
|
||||
service.createServers([{ id: 'ghi789' }, { id: 'jkl123' }]);
|
||||
|
||||
|
@ -95,8 +94,7 @@ describe('ServersService', () => {
|
|||
|
||||
describe('deleteServer', () => {
|
||||
it('removes one server from the list', () => {
|
||||
const storageMock = createStorageMock(servers);
|
||||
const service = new ServersService(storageMock);
|
||||
const [ service, storageMock ] = createService();
|
||||
|
||||
service.deleteServer({ id: 'abc123' });
|
||||
|
||||
|
@ -107,4 +105,26 @@ describe('ServersService', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('editServer', () => {
|
||||
it('dos nothing is provided server does not exist', () => {
|
||||
const [ service, storageMock ] = createService();
|
||||
|
||||
service.editServer('notFound', {});
|
||||
|
||||
expect(storageMock.set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates the list with provided server data', () => {
|
||||
const [ service, storageMock ] = createService();
|
||||
const serverData = { name: 'foo', apiKey: 'bar' };
|
||||
|
||||
service.editServer('abc123', serverData);
|
||||
|
||||
expect(storageMock.set).toHaveBeenCalledWith(expect.anything(), {
|
||||
abc123: { id: 'abc123', ...serverData },
|
||||
def456: { id: 'def456' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue