mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-03 23:07:26 +03:00
Merge pull request #222 from acelaya-forks/feature/server-not-found
Feature/server not found
This commit is contained in:
commit
01672b88e1
29 changed files with 1076 additions and 261 deletions
|
@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
#### Added
|
#### Added
|
||||||
|
|
||||||
* [#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.
|
* [#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.
|
||||||
|
|
||||||
#### Changed
|
#### Changed
|
||||||
|
|
||||||
|
|
831
package-lock.json
generated
831
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -85,8 +85,8 @@
|
||||||
"css-loader": "^3.2.0",
|
"css-loader": "^3.2.0",
|
||||||
"dotenv": "^8.1.0",
|
"dotenv": "^8.1.0",
|
||||||
"dotenv-expand": "^5.1.0",
|
"dotenv-expand": "^5.1.0",
|
||||||
"enzyme": "^3.10.0",
|
"enzyme": "^3.11.0",
|
||||||
"enzyme-adapter-react-16": "^1.14.0",
|
"enzyme-adapter-react-16": "^1.15.2",
|
||||||
"eslint": "^5.11.1",
|
"eslint": "^5.11.1",
|
||||||
"eslint-config-adidas-babel": "^1.1.0",
|
"eslint-config-adidas-babel": "^1.1.0",
|
||||||
"eslint-config-adidas-env": "^1.1.0",
|
"eslint-config-adidas-env": "^1.1.0",
|
||||||
|
|
|
@ -1,52 +1,35 @@
|
||||||
import { faChevronRight as chevronIcon } from '@fortawesome/free-solid-svg-icons';
|
import React, { useEffect } from 'react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { isEmpty, values } from 'ramda';
|
import { isEmpty, values } from 'ramda';
|
||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { ListGroup, ListGroupItem } from 'reactstrap';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './Home.scss';
|
import './Home.scss';
|
||||||
|
import ServersListGroup from '../servers/ServersListGroup';
|
||||||
|
|
||||||
export default class Home extends React.Component {
|
const propTypes = {
|
||||||
static propTypes = {
|
resetSelectedServer: PropTypes.func,
|
||||||
resetSelectedServer: PropTypes.func,
|
servers: PropTypes.object,
|
||||||
servers: PropTypes.object,
|
};
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
const Home = ({ resetSelectedServer, servers: { list, loading } }) => {
|
||||||
this.props.resetSelectedServer();
|
const servers = values(list);
|
||||||
}
|
const hasServers = !isEmpty(servers);
|
||||||
|
|
||||||
render() {
|
useEffect(() => {
|
||||||
const { servers: { list, loading } } = this.props;
|
resetSelectedServer();
|
||||||
const servers = values(list);
|
}, []);
|
||||||
const hasServers = !isEmpty(servers);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="home">
|
<div className="home">
|
||||||
<h1 className="home__title">Welcome to Shlink</h1>
|
<h1 className="home__title">Welcome to Shlink</h1>
|
||||||
<h5 className="home__intro">
|
<ServersListGroup servers={servers}>
|
||||||
{!loading && hasServers && <span>Please, select a server.</span>}
|
{!loading && hasServers && <span>Please, select a server.</span>}
|
||||||
{!loading && !hasServers && <span>Please, <Link to="/server/create">add a server</Link>.</span>}
|
{!loading && !hasServers && <span>Please, <Link to="/server/create">add a server</Link>.</span>}
|
||||||
{loading && <span>Trying to load servers...</span>}
|
{loading && <span>Trying to load servers...</span>}
|
||||||
</h5>
|
</ServersListGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
{!loading && hasServers && (
|
Home.propTypes = propTypes;
|
||||||
<ListGroup className="home__servers-list">
|
|
||||||
{servers.map(({ name, id }) => (
|
export default Home;
|
||||||
<ListGroupItem
|
|
||||||
key={id}
|
|
||||||
tag={Link}
|
|
||||||
to={`/server/${id}/list-short-urls/1`}
|
|
||||||
className="home__servers-item"
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
<FontAwesomeIcon icon={chevronIcon} className="home__servers-item-icon" />
|
|
||||||
</ListGroupItem>
|
|
||||||
))}
|
|
||||||
</ListGroup>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
@import '../utils/base';
|
@import '../utils/base';
|
||||||
@import '../utils/mixins/vertical-align';
|
|
||||||
|
|
||||||
.home {
|
.home {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -17,21 +16,3 @@
|
||||||
font-size: 2.2rem;
|
font-size: 2.2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.home__servers-list {
|
|
||||||
margin-top: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home__servers-item.home__servers-item {
|
|
||||||
text-align: left;
|
|
||||||
position: relative;
|
|
||||||
padding: .75rem 2.5rem .75rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home__servers-item-icon {
|
|
||||||
@include vertical-align();
|
|
||||||
|
|
||||||
right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import * as PropTypes from 'prop-types';
|
import * as PropTypes from 'prop-types';
|
||||||
import { serverType } from '../servers/prop-types';
|
import { serverType } from '../servers/prop-types';
|
||||||
import MutedMessage from '../utils/MutedMessage';
|
import Message from '../utils/Message';
|
||||||
import NotFound from './NotFound';
|
import NotFound from './NotFound';
|
||||||
import './MenuLayout.scss';
|
import './MenuLayout.scss';
|
||||||
|
|
||||||
|
@ -17,22 +17,28 @@ const propTypes = {
|
||||||
selectedServer: serverType,
|
selectedServer: serverType,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisits, ShlinkVersions) => {
|
const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisits, ShlinkVersions, ServerError) => {
|
||||||
const MenuLayoutComp = ({ match, location, selectedServer, selectServer }) => {
|
const MenuLayoutComp = ({ match, location, selectedServer, selectServer }) => {
|
||||||
const [ showSideBar, setShowSidebar ] = useState(false);
|
const [ showSideBar, setShowSidebar ] = useState(false);
|
||||||
|
const { params: { serverId } } = match;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { params: { serverId } } = match;
|
|
||||||
|
|
||||||
selectServer(serverId);
|
selectServer(serverId);
|
||||||
}, []);
|
}, [ serverId ]);
|
||||||
useEffect(() => setShowSidebar(false), [ location ]);
|
useEffect(() => setShowSidebar(false), [ location ]);
|
||||||
|
|
||||||
if (!selectedServer) {
|
if (!selectedServer) {
|
||||||
return <MutedMessage loading />;
|
return <Message loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedServer.serverNotFound) {
|
||||||
|
return <ServerError type="not-found" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedServer.serverNotReachable) {
|
||||||
|
return <ServerError type="not-reachable" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { params: { serverId } } = match;
|
|
||||||
const burgerClasses = classNames('menu-layout__burger-icon', {
|
const burgerClasses = classNames('menu-layout__burger-icon', {
|
||||||
'menu-layout__burger-icon--active': showSideBar,
|
'menu-layout__burger-icon--active': showSideBar,
|
||||||
});
|
});
|
||||||
|
@ -68,7 +74,7 @@ const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisi
|
||||||
<Route exact path="/server/:serverId/short-code/:shortCode/visits" component={ShortUrlVisits} />
|
<Route exact path="/server/:serverId/short-code/:shortCode/visits" component={ShortUrlVisits} />
|
||||||
<Route exact path="/server/:serverId/manage-tags" component={TagsList} />
|
<Route exact path="/server/:serverId/manage-tags" component={TagsList} />
|
||||||
<Route
|
<Route
|
||||||
render={() => <NotFound to={`/server/${serverId}/list-short-urls/1`} btnText="List short URLs" />}
|
render={() => <NotFound to={`/server/${serverId}/list-short-urls/1`}>List short URLs</NotFound>}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,17 +4,18 @@ import * as PropTypes from 'prop-types';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
to: PropTypes.string,
|
to: PropTypes.string,
|
||||||
btnText: PropTypes.string,
|
children: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
const NotFound = ({ to = '/', btnText = 'Home' }) => (
|
const NotFound = ({ to = '/', children = 'Home' }) => (
|
||||||
<div className="home">
|
<div className="home">
|
||||||
<h2>Oops! We could not find requested route.</h2>
|
<h2>Oops! We could not find requested route.</h2>
|
||||||
<p>
|
<p>
|
||||||
Use your browser{'\''}s back button to navigate to the page you have previously come from, or just press this button.
|
Use your browser's back button to navigate to the page you have previously come from, or just press this
|
||||||
|
button.
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
<Link to={to} className="btn btn-outline-primary btn-lg">{btnText}</Link>
|
<Link to={to} className="btn btn-outline-primary btn-lg">{children}</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,8 @@ const provideServices = (bottle, connect, withRouter) => {
|
||||||
'AsideMenu',
|
'AsideMenu',
|
||||||
'CreateShortUrl',
|
'CreateShortUrl',
|
||||||
'ShortUrlVisits',
|
'ShortUrlVisits',
|
||||||
'ShlinkVersions'
|
'ShlinkVersions',
|
||||||
|
'ServerError'
|
||||||
);
|
);
|
||||||
bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ]));
|
bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ]));
|
||||||
bottle.decorator('MenuLayout', withRouter);
|
bottle.decorator('MenuLayout', withRouter);
|
||||||
|
|
|
@ -8,7 +8,6 @@ const ServersDropdown = (serversExporter) => class ServersDropdown extends React
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
servers: PropTypes.object,
|
servers: PropTypes.object,
|
||||||
selectedServer: serverType,
|
selectedServer: serverType,
|
||||||
selectServer: PropTypes.func,
|
|
||||||
listServers: PropTypes.func,
|
listServers: PropTypes.func,
|
||||||
history: PropTypes.shape({
|
history: PropTypes.shape({
|
||||||
push: PropTypes.func,
|
push: PropTypes.func,
|
||||||
|
@ -16,14 +15,10 @@ const ServersDropdown = (serversExporter) => class ServersDropdown extends React
|
||||||
};
|
};
|
||||||
|
|
||||||
renderServers = () => {
|
renderServers = () => {
|
||||||
const { servers: { list, loading }, selectedServer, selectServer } = this.props;
|
const { servers: { list, loading }, selectedServer } = this.props;
|
||||||
const servers = values(list);
|
const servers = values(list);
|
||||||
const { push } = this.props.history;
|
const { push } = this.props.history;
|
||||||
const loadServer = (id) => {
|
const loadServer = (id) => push(`/server/${id}/list-short-urls/1`);
|
||||||
selectServer(id)
|
|
||||||
.then(() => push(`/server/${id}/list-short-urls/1`))
|
|
||||||
.catch(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <DropdownItem disabled><i>Trying to load servers...</i></DropdownItem>;
|
return <DropdownItem disabled><i>Trying to load servers...</i></DropdownItem>;
|
||||||
|
@ -41,10 +36,7 @@ const ServersDropdown = (serversExporter) => class ServersDropdown extends React
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
))}
|
))}
|
||||||
<DropdownItem divider />
|
<DropdownItem divider />
|
||||||
<DropdownItem
|
<DropdownItem className="servers-dropdown__export-item" onClick={() => serversExporter.exportServers()}>
|
||||||
className="servers-dropdown__export-item"
|
|
||||||
onClick={() => serversExporter.exportServers()}
|
|
||||||
>
|
|
||||||
Export servers
|
Export servers
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
40
src/servers/ServersListGroup.js
Normal file
40
src/servers/ServersListGroup.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { ListGroup, ListGroupItem } from 'reactstrap';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faChevronRight as chevronIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { serverType } from './prop-types';
|
||||||
|
import './ServersListGroup.scss';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
servers: PropTypes.arrayOf(serverType).isRequired,
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ServerListItem = ({ id, name }) => (
|
||||||
|
<ListGroupItem tag={Link} to={`/server/${id}/list-short-urls/1`} className="servers-list__server-item">
|
||||||
|
{name}
|
||||||
|
<FontAwesomeIcon icon={chevronIcon} className="servers-list__server-item-icon" />
|
||||||
|
</ListGroupItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
ServerListItem.propTypes = {
|
||||||
|
id: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ServersListGroup = ({ servers, children }) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<h5>{children}</h5>
|
||||||
|
{servers.length > 0 && (
|
||||||
|
<ListGroup className="servers-list__list-group mt-md-3">
|
||||||
|
{servers.map(({ id, name }) => <ServerListItem key={id} id={id} name={name} />)}
|
||||||
|
</ListGroup>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
ServersListGroup.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ServersListGroup;
|
18
src/servers/ServersListGroup.scss
Normal file
18
src/servers/ServersListGroup.scss
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
@import '../utils/mixins/vertical-align';
|
||||||
|
|
||||||
|
.servers-list__list-group {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.servers-list__server-item.servers-list__server-item {
|
||||||
|
text-align: left;
|
||||||
|
position: relative;
|
||||||
|
padding: .75rem 2.5rem .75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.servers-list__server-item-icon {
|
||||||
|
@include vertical-align();
|
||||||
|
|
||||||
|
right: 1rem;
|
||||||
|
}
|
33
src/servers/helpers/ServerError.js
Normal file
33
src/servers/helpers/ServerError.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import Message from '../../utils/Message';
|
||||||
|
import ServersListGroup from '../ServersListGroup';
|
||||||
|
import './ServerError.scss';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
servers: PropTypes.object,
|
||||||
|
type: PropTypes.oneOf([ 'not-found', 'not-reachable' ]).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ServerError = ({ type, servers: { list } }) => (
|
||||||
|
<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.'}
|
||||||
|
{type === 'not-reachable' && (
|
||||||
|
<React.Fragment>
|
||||||
|
<p>Oops! Could not connect to this Shlink server.</p>
|
||||||
|
Make sure you have internet connection, and the server is properly configured and on-line.
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</Message>
|
||||||
|
</div>
|
||||||
|
<ServersListGroup servers={Object.values(list)}>
|
||||||
|
These are the {type === 'not-reachable' ? 'other' : ''} Shlink servers currently configured. Choose one of
|
||||||
|
them or <Link to="/server/create">add a new one</Link>.
|
||||||
|
</ServersListGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
ServerError.propTypes = propTypes;
|
6
src/servers/helpers/ServerError.scss
Normal file
6
src/servers/helpers/ServerError.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
.server-error-container {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
export const serverType = PropTypes.shape({
|
const regularServerType = PropTypes.shape({
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
url: PropTypes.string,
|
url: PropTypes.string,
|
||||||
|
@ -8,3 +8,17 @@ export const serverType = PropTypes.shape({
|
||||||
version: PropTypes.string,
|
version: PropTypes.string,
|
||||||
printableVersion: PropTypes.string,
|
printableVersion: PropTypes.string,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const notFoundServerType = PropTypes.shape({
|
||||||
|
serverNotFound: PropTypes.bool.isRequired,
|
||||||
|
});
|
||||||
|
|
||||||
|
const notReachableServerType = PropTypes.shape({
|
||||||
|
serverNotReachable: PropTypes.bool.isRequired,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const serverType = PropTypes.oneOfType([
|
||||||
|
regularServerType,
|
||||||
|
notFoundServerType,
|
||||||
|
notReachableServerType,
|
||||||
|
]);
|
||||||
|
|
|
@ -21,20 +21,37 @@ const versionToSemVer = pipe(
|
||||||
export const resetSelectedServer = createAction(RESET_SELECTED_SERVER);
|
export const resetSelectedServer = createAction(RESET_SELECTED_SERVER);
|
||||||
|
|
||||||
export const selectServer = ({ findServerById }, buildShlinkApiClient) => (serverId) => async (dispatch) => {
|
export const selectServer = ({ findServerById }, buildShlinkApiClient) => (serverId) => async (dispatch) => {
|
||||||
|
dispatch(resetSelectedServer());
|
||||||
dispatch(resetShortUrlParams());
|
dispatch(resetShortUrlParams());
|
||||||
|
|
||||||
const selectedServer = findServerById(serverId);
|
const selectedServer = findServerById(serverId);
|
||||||
const { health } = buildShlinkApiClient(selectedServer);
|
|
||||||
const { version } = await health().catch(() => MIN_FALLBACK_VERSION);
|
|
||||||
|
|
||||||
dispatch({
|
if (!selectedServer) {
|
||||||
type: SELECT_SERVER,
|
dispatch({
|
||||||
selectedServer: {
|
type: SELECT_SERVER,
|
||||||
...selectedServer,
|
selectedServer: { serverNotFound: true },
|
||||||
version: versionToSemVer(version),
|
});
|
||||||
printableVersion: versionToPrintable(version),
|
|
||||||
},
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { health } = buildShlinkApiClient(selectedServer);
|
||||||
|
const { version } = await health();
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: SELECT_SERVER,
|
||||||
|
selectedServer: {
|
||||||
|
...selectedServer,
|
||||||
|
version: versionToSemVer(version),
|
||||||
|
printableVersion: versionToPrintable(version),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
dispatch({
|
||||||
|
type: SELECT_SERVER,
|
||||||
|
selectedServer: { serverNotReachable: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handleActions({
|
export default handleActions({
|
||||||
|
|
|
@ -7,6 +7,7 @@ import ImportServersBtn from '../helpers/ImportServersBtn';
|
||||||
import { resetSelectedServer, selectServer } from '../reducers/selectedServer';
|
import { resetSelectedServer, selectServer } from '../reducers/selectedServer';
|
||||||
import { createServer, createServers, deleteServer, listServers } from '../reducers/server';
|
import { createServer, createServers, deleteServer, listServers } from '../reducers/server';
|
||||||
import ForServerVersion from '../helpers/ForServerVersion';
|
import ForServerVersion from '../helpers/ForServerVersion';
|
||||||
|
import { ServerError } from '../helpers/ServerError';
|
||||||
import ServersImporter from './ServersImporter';
|
import ServersImporter from './ServersImporter';
|
||||||
import ServersService from './ServersService';
|
import ServersService from './ServersService';
|
||||||
import ServersExporter from './ServersExporter';
|
import ServersExporter from './ServersExporter';
|
||||||
|
@ -18,7 +19,7 @@ const provideServices = (bottle, connect, withRouter) => {
|
||||||
|
|
||||||
bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter');
|
bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter');
|
||||||
bottle.decorator('ServersDropdown', withRouter);
|
bottle.decorator('ServersDropdown', withRouter);
|
||||||
bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ], [ 'listServers', 'selectServer' ]));
|
bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ], [ 'listServers' ]));
|
||||||
|
|
||||||
bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal);
|
bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal);
|
||||||
bottle.decorator('DeleteServerModal', withRouter);
|
bottle.decorator('DeleteServerModal', withRouter);
|
||||||
|
@ -32,6 +33,9 @@ const provideServices = (bottle, connect, withRouter) => {
|
||||||
bottle.serviceFactory('ForServerVersion', () => ForServerVersion);
|
bottle.serviceFactory('ForServerVersion', () => ForServerVersion);
|
||||||
bottle.decorator('ForServerVersion', connect([ 'selectedServer' ]));
|
bottle.decorator('ForServerVersion', connect([ 'selectedServer' ]));
|
||||||
|
|
||||||
|
bottle.serviceFactory('ServerError', () => ServerError);
|
||||||
|
bottle.decorator('ServerError', connect([ 'servers' ]));
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
bottle.constant('csvjson', csvjson);
|
bottle.constant('csvjson', csvjson);
|
||||||
bottle.service('ServersImporter', ServersImporter, 'csvjson');
|
bottle.service('ServersImporter', ServersImporter, 'csvjson');
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Paginator from './Paginator';
|
import Paginator from './Paginator';
|
||||||
|
|
||||||
|
@ -14,9 +14,13 @@ const ShortUrls = (SearchBar, ShortUrlsList) => {
|
||||||
const { match: { params }, shortUrlsList } = props;
|
const { match: { params }, shortUrlsList } = props;
|
||||||
const { page, serverId } = params;
|
const { page, serverId } = params;
|
||||||
const { data = [], pagination } = shortUrlsList;
|
const { data = [], pagination } = shortUrlsList;
|
||||||
|
const [ urlsListKey, setUrlsListKey ] = useState(`${serverId}_${page}`);
|
||||||
|
|
||||||
// Using a key on a component makes react to create a new instance every time the key changes
|
// Using a key on a component makes react to create a new instance every time the key changes
|
||||||
const urlsListKey = `${serverId}_${page}`;
|
// Without it, pagination on the URL will not make the component to be refreshed
|
||||||
|
useEffect(() => {
|
||||||
|
setUrlsListKey(`${serverId}_${page}`);
|
||||||
|
}, [ serverId, page ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
|
|
@ -112,9 +112,9 @@ const ShortUrlsList = (ShortUrlsRow) => class ShortUrlsList extends React.Compon
|
||||||
|
|
||||||
return shortUrlsList.map((shortUrl) => (
|
return shortUrlsList.map((shortUrl) => (
|
||||||
<ShortUrlsRow
|
<ShortUrlsRow
|
||||||
|
key={shortUrl.shortUrl}
|
||||||
shortUrl={shortUrl}
|
shortUrl={shortUrl}
|
||||||
selectedServer={selectedServer}
|
selectedServer={selectedServer}
|
||||||
key={shortUrl.shortCode}
|
|
||||||
refreshList={this.refreshList}
|
refreshList={this.refreshList}
|
||||||
shortUrlsListParams={shortUrlsListParams}
|
shortUrlsListParams={shortUrlsListParams}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { splitEvery } from 'ramda';
|
import { splitEvery } from 'ramda';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import MutedMessage from '../utils/MutedMessage';
|
import Message from '../utils/Message';
|
||||||
import SearchField from '../utils/SearchField';
|
import SearchField from '../utils/SearchField';
|
||||||
|
|
||||||
const { ceil } = Math;
|
const { ceil } = Math;
|
||||||
|
@ -29,7 +29,7 @@ const TagsList = (TagCard) => class TagsList extends React.Component {
|
||||||
const { tagsList, match } = this.props;
|
const { tagsList, match } = this.props;
|
||||||
|
|
||||||
if (tagsList.loading) {
|
if (tagsList.loading) {
|
||||||
return <MutedMessage noMargin loading />;
|
return <Message noMargin loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagsList.error) {
|
if (tagsList.error) {
|
||||||
|
@ -43,7 +43,7 @@ const TagsList = (TagCard) => class TagsList extends React.Component {
|
||||||
const tagsCount = tagsList.filteredTags.length;
|
const tagsCount = tagsList.filteredTags.length;
|
||||||
|
|
||||||
if (tagsCount < 1) {
|
if (tagsCount < 1) {
|
||||||
return <MutedMessage>No tags found</MutedMessage>;
|
return <Message>No tags found</Message>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagsGroups = splitEvery(ceil(tagsCount / TAGS_GROUPS_AMOUNT), tagsList.filteredTags);
|
const tagsGroups = splitEvery(ceil(tagsCount / TAGS_GROUPS_AMOUNT), tagsList.filteredTags);
|
||||||
|
|
|
@ -5,21 +5,35 @@ import PropTypes from 'prop-types';
|
||||||
import { faCircleNotch as preloader } from '@fortawesome/free-solid-svg-icons';
|
import { faCircleNotch as preloader } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
const getClassForType = (type) => {
|
||||||
|
const map = {
|
||||||
|
error: 'border-danger',
|
||||||
|
};
|
||||||
|
|
||||||
|
return map[type] || '';
|
||||||
|
};
|
||||||
|
const getTextClassForType = (type) => {
|
||||||
|
const map = {
|
||||||
|
error: 'text-danger',
|
||||||
|
};
|
||||||
|
|
||||||
|
return map[type] || 'text-muted';
|
||||||
|
};
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
noMargin: PropTypes.bool,
|
noMargin: PropTypes.bool,
|
||||||
loading: PropTypes.bool,
|
loading: PropTypes.bool,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
|
type: PropTypes.oneOf([ 'default', 'error' ]),
|
||||||
};
|
};
|
||||||
|
|
||||||
const MutedMessage = ({ children, loading = false, noMargin = false }) => {
|
const Message = ({ children, loading = false, noMargin = false, type = 'default' }) => {
|
||||||
const cardClasses = classNames('bg-light', {
|
const cardClasses = classNames('bg-light', getClassForType(type), { 'mt-4': !noMargin });
|
||||||
'mt-4': !noMargin,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="col-md-10 offset-md-1">
|
<div className="col-md-10 offset-md-1">
|
||||||
<Card className={cardClasses} body>
|
<Card className={cardClasses} body>
|
||||||
<h3 className="text-center text-muted mb-0">
|
<h3 className={classNames('text-center mb-0', getTextClassForType(type))}>
|
||||||
{loading && <FontAwesomeIcon icon={preloader} spin />}
|
{loading && <FontAwesomeIcon icon={preloader} spin />}
|
||||||
{loading && !children && <span className="ml-2">Loading...</span>}
|
{loading && !children && <span className="ml-2">Loading...</span>}
|
||||||
{children}
|
{children}
|
||||||
|
@ -29,6 +43,6 @@ const MutedMessage = ({ children, loading = false, noMargin = false }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
MutedMessage.propTypes = propTypes;
|
Message.propTypes = propTypes;
|
||||||
|
|
||||||
export default MutedMessage;
|
export default Message;
|
|
@ -1,4 +1,4 @@
|
||||||
@mixin vertical-align($extraTransforms: '') {
|
@mixin vertical-align($extraTransforms: null) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%) $extraTransforms;
|
transform: translateY(-50%) $extraTransforms;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Card } from 'reactstrap';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import DateRangeRow from '../utils/DateRangeRow';
|
import DateRangeRow from '../utils/DateRangeRow';
|
||||||
import MutedMessage from '../utils/MutedMessage';
|
import Message from '../utils/Message';
|
||||||
import { formatDate } from '../utils/utils';
|
import { formatDate } from '../utils/utils';
|
||||||
import SortableBarGraph from './SortableBarGraph';
|
import SortableBarGraph from './SortableBarGraph';
|
||||||
import { shortUrlVisitsType } from './reducers/shortUrlVisits';
|
import { shortUrlVisitsType } from './reducers/shortUrlVisits';
|
||||||
|
@ -64,7 +64,7 @@ const ShortUrlVisits = (
|
||||||
if (loading) {
|
if (loading) {
|
||||||
const message = loadingLarge ? 'This is going to take a while... :S' : 'Loading...';
|
const message = loadingLarge ? 'This is going to take a while... :S' : 'Loading...';
|
||||||
|
|
||||||
return <MutedMessage loading>{message}</MutedMessage>;
|
return <Message loading>{message}</Message>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -76,7 +76,7 @@ const ShortUrlVisits = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEmpty(visits)) {
|
if (isEmpty(visits)) {
|
||||||
return <MutedMessage>There are no visits matching current filter :(</MutedMessage>;
|
return <Message>There are no visits matching current filter :(</Message>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { os, browsers, referrers, countries, cities, citiesForMap } = processStatsFromVisits(
|
const { os, browsers, referrers, countries, cities, citiesForMap } = processStatsFromVisits(
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { values } from 'ramda';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Home from '../../src/common/Home';
|
import Home from '../../src/common/Home';
|
||||||
|
|
||||||
describe('<Home />', () => {
|
describe('<Home />', () => {
|
||||||
let wrapped;
|
let wrapped;
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
resetSelectedServer: () => '',
|
resetSelectedServer: jest.fn(),
|
||||||
servers: { loading: false, list: {} },
|
servers: { loading: false, list: {} },
|
||||||
};
|
};
|
||||||
const createComponent = (props) => {
|
const createComponent = (props) => {
|
||||||
|
@ -17,26 +16,12 @@ describe('<Home />', () => {
|
||||||
return wrapped;
|
return wrapped;
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => wrapped && wrapped.unmount());
|
||||||
if (wrapped !== undefined) {
|
|
||||||
wrapped.unmount();
|
|
||||||
wrapped = undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('resets selected server when mounted', () => {
|
|
||||||
const resetSelectedServer = jest.fn();
|
|
||||||
|
|
||||||
expect(resetSelectedServer).not.toHaveBeenCalled();
|
|
||||||
createComponent({ resetSelectedServer });
|
|
||||||
expect(resetSelectedServer).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows link to create server when no servers exist', () => {
|
it('shows link to create server when no servers exist', () => {
|
||||||
const wrapped = createComponent();
|
const wrapped = createComponent();
|
||||||
|
|
||||||
expect(wrapped.find('Link')).toHaveLength(1);
|
expect(wrapped.find('Link')).toHaveLength(1);
|
||||||
expect(wrapped.find('ListGroup')).toHaveLength(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows message when loading servers', () => {
|
it('shows message when loading servers', () => {
|
||||||
|
@ -45,21 +30,17 @@ describe('<Home />', () => {
|
||||||
|
|
||||||
expect(span).toHaveLength(1);
|
expect(span).toHaveLength(1);
|
||||||
expect(span.text()).toContain('Trying to load servers...');
|
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', () => {
|
it('Asks to select a server when not loadign and servers exist', () => {
|
||||||
const servers = {
|
const list = [
|
||||||
loading: false,
|
{ name: 'foo', id: '1' },
|
||||||
list: {
|
{ name: 'bar', id: '2' },
|
||||||
1: { name: 'foo', id: '123' },
|
];
|
||||||
2: { name: 'bar', id: '456' },
|
const wrapped = createComponent({ servers: { list } });
|
||||||
},
|
const span = wrapped.find('span');
|
||||||
};
|
|
||||||
const wrapped = createComponent({ servers });
|
|
||||||
|
|
||||||
expect(wrapped.find('Link')).toHaveLength(0);
|
expect(span).toHaveLength(1);
|
||||||
expect(wrapped.find('ListGroup')).toHaveLength(1);
|
expect(span.text()).toContain('Please, select a server.');
|
||||||
expect(wrapped.find('ListGroupItem')).toHaveLength(values(servers).length);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -38,7 +38,7 @@ describe('<NotFound />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows a link with provided props', () => {
|
it('shows a link with provided props', () => {
|
||||||
const { wrapper } = createWrapper({ to: '/foo/bar', btnText: 'Hello' });
|
const { wrapper } = createWrapper({ to: '/foo/bar', children: 'Hello' });
|
||||||
const link = wrapper.find(Link);
|
const link = wrapper.find(Link);
|
||||||
|
|
||||||
expect(link.prop('to')).toEqual('/foo/bar');
|
expect(link.prop('to')).toEqual('/foo/bar');
|
||||||
|
|
34
test/servers/ServersListGroup.test.js
Normal file
34
test/servers/ServersListGroup.test.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import { ListGroup } from 'reactstrap';
|
||||||
|
import ServersListGroup from '../../src/servers/ServersListGroup';
|
||||||
|
|
||||||
|
describe('<ServersListGroup />', () => {
|
||||||
|
let wrapped;
|
||||||
|
const createComponent = (servers) => {
|
||||||
|
wrapped = shallow(<ServersListGroup servers={servers}>The list of servers</ServersListGroup>);
|
||||||
|
|
||||||
|
return wrapped;
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => wrapped && wrapped.unmount());
|
||||||
|
|
||||||
|
it('Renders title', () => {
|
||||||
|
const wrapped = createComponent([]);
|
||||||
|
const title = wrapped.find('h5');
|
||||||
|
|
||||||
|
expect(title).toHaveLength(1);
|
||||||
|
expect(title.text()).toEqual('The list of servers');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows servers list', () => {
|
||||||
|
const servers = [
|
||||||
|
{ name: 'foo', id: '123' },
|
||||||
|
{ name: 'bar', id: '456' },
|
||||||
|
];
|
||||||
|
const wrapped = createComponent(servers);
|
||||||
|
|
||||||
|
expect(wrapped.find(ListGroup)).toHaveLength(1);
|
||||||
|
expect(wrapped.find('ServerListItem')).toHaveLength(servers.length);
|
||||||
|
});
|
||||||
|
});
|
35
test/servers/helpers/ServerError.test.js
Normal file
35
test/servers/helpers/ServerError.test.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import { ServerError } from '../../../src/servers/helpers/ServerError';
|
||||||
|
|
||||||
|
describe('<ServerError />', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
afterEach(() => wrapper && wrapper.unmount());
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[
|
||||||
|
'not-found',
|
||||||
|
[ 'Could not find this Shlink server.' ],
|
||||||
|
'These are the Shlink servers',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'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.',
|
||||||
|
],
|
||||||
|
'These are the other Shlink servers',
|
||||||
|
],
|
||||||
|
])('renders expected information based on type', (type, expectedTitleParts, expectedBody) => {
|
||||||
|
wrapper = shallow(<BrowserRouter><ServerError type={type} servers={{ list: [] }} /></BrowserRouter>);
|
||||||
|
const wrapperText = wrapper.html();
|
||||||
|
const textsToFind = [ ...expectedTitleParts, ...expectedBody ];
|
||||||
|
|
||||||
|
expect.assertions(textsToFind.length);
|
||||||
|
textsToFind.forEach((text) => {
|
||||||
|
expect(wrapperText).toContain(text);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -58,9 +58,10 @@ describe('selectedServerReducer', () => {
|
||||||
|
|
||||||
await selectServer(ServersServiceMock, buildApiClient)(serverId)(dispatch);
|
await selectServer(ServersServiceMock, buildApiClient)(serverId)(dispatch);
|
||||||
|
|
||||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
expect(dispatch).toHaveBeenCalledTimes(3);
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: RESET_SHORT_URL_PARAMS });
|
expect(dispatch).toHaveBeenNthCalledWith(1, { type: RESET_SELECTED_SERVER });
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
|
expect(dispatch).toHaveBeenNthCalledWith(2, { type: RESET_SHORT_URL_PARAMS });
|
||||||
|
expect(dispatch).toHaveBeenNthCalledWith(3, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('invokes dependencies', async () => {
|
it('invokes dependencies', async () => {
|
||||||
|
@ -70,17 +71,27 @@ describe('selectedServerReducer', () => {
|
||||||
expect(buildApiClient).toHaveBeenCalledTimes(1);
|
expect(buildApiClient).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to min version when health endpoint fails', async () => {
|
it('dispatches error when health endpoint fails', async () => {
|
||||||
const expectedSelectedServer = {
|
const expectedSelectedServer = { serverNotReachable: true };
|
||||||
...selectedServer,
|
|
||||||
version: MIN_FALLBACK_VERSION,
|
|
||||||
};
|
|
||||||
|
|
||||||
apiClientMock.health.mockRejectedValue({});
|
apiClientMock.health.mockRejectedValue({});
|
||||||
|
|
||||||
await selectServer(ServersServiceMock, buildApiClient)(serverId)(dispatch);
|
await selectServer(ServersServiceMock, buildApiClient)(serverId)(dispatch);
|
||||||
|
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
|
expect(apiClientMock.health).toHaveBeenCalled();
|
||||||
|
expect(dispatch).toHaveBeenNthCalledWith(3, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches error when server is not found', async () => {
|
||||||
|
const expectedSelectedServer = { serverNotFound: true };
|
||||||
|
|
||||||
|
ServersServiceMock.findServerById.mockReturnValue(undefined);
|
||||||
|
|
||||||
|
await selectServer(ServersServiceMock, buildApiClient)(serverId)(dispatch);
|
||||||
|
|
||||||
|
expect(ServersServiceMock.findServerById).toHaveBeenCalled();
|
||||||
|
expect(apiClientMock.health).not.toHaveBeenCalled();
|
||||||
|
expect(dispatch).toHaveBeenNthCalledWith(3, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { identity } from 'ramda';
|
import { identity } from 'ramda';
|
||||||
import createTagsList from '../../src/tags/TagsList';
|
import createTagsList from '../../src/tags/TagsList';
|
||||||
import MutedMessage from '../../src/utils/MutedMessage';
|
import Message from '../../src/utils/Message';
|
||||||
import SearchField from '../../src/utils/SearchField';
|
import SearchField from '../../src/utils/SearchField';
|
||||||
import { rangeOf } from '../../src/utils/utils';
|
import { rangeOf } from '../../src/utils/utils';
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ describe('<TagsList />', () => {
|
||||||
|
|
||||||
it('shows a loading message when tags are being loaded', () => {
|
it('shows a loading message when tags are being loaded', () => {
|
||||||
const wrapper = createWrapper({ loading: true });
|
const wrapper = createWrapper({ loading: true });
|
||||||
const loadingMsg = wrapper.find(MutedMessage);
|
const loadingMsg = wrapper.find(Message);
|
||||||
|
|
||||||
expect(loadingMsg).toHaveLength(1);
|
expect(loadingMsg).toHaveLength(1);
|
||||||
expect(loadingMsg.html()).toContain('Loading...');
|
expect(loadingMsg.html()).toContain('Loading...');
|
||||||
|
@ -44,7 +44,7 @@ describe('<TagsList />', () => {
|
||||||
|
|
||||||
it('shows a message when the list of tags is empty', () => {
|
it('shows a message when the list of tags is empty', () => {
|
||||||
const wrapper = createWrapper({ filteredTags: [] });
|
const wrapper = createWrapper({ filteredTags: [] });
|
||||||
const msg = wrapper.find(MutedMessage);
|
const msg = wrapper.find(Message);
|
||||||
|
|
||||||
expect(msg).toHaveLength(1);
|
expect(msg).toHaveLength(1);
|
||||||
expect(msg.html()).toContain('No tags found');
|
expect(msg.html()).toContain('No tags found');
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { shallow } from 'enzyme';
|
||||||
import { identity } from 'ramda';
|
import { identity } from 'ramda';
|
||||||
import { Card } from 'reactstrap';
|
import { Card } from 'reactstrap';
|
||||||
import createShortUrlVisits from '../../src/visits/ShortUrlVisits';
|
import createShortUrlVisits from '../../src/visits/ShortUrlVisits';
|
||||||
import MutedMessage from '../../src/utils/MutedMessage';
|
import Message from '../../src/utils/Message';
|
||||||
import GraphCard from '../../src/visits/GraphCard';
|
import GraphCard from '../../src/visits/GraphCard';
|
||||||
import SortableBarGraph from '../../src/visits/SortableBarGraph';
|
import SortableBarGraph from '../../src/visits/SortableBarGraph';
|
||||||
import DateRangeRow from '../../src/utils/DateRangeRow';
|
import DateRangeRow from '../../src/utils/DateRangeRow';
|
||||||
|
@ -44,7 +44,7 @@ describe('<ShortUrlVisits />', () => {
|
||||||
|
|
||||||
it('renders a preloader when visits are loading', () => {
|
it('renders a preloader when visits are loading', () => {
|
||||||
const wrapper = createComponent({ loading: true });
|
const wrapper = createComponent({ loading: true });
|
||||||
const loadingMessage = wrapper.find(MutedMessage);
|
const loadingMessage = wrapper.find(Message);
|
||||||
|
|
||||||
expect(loadingMessage).toHaveLength(1);
|
expect(loadingMessage).toHaveLength(1);
|
||||||
expect(loadingMessage.html()).toContain('Loading...');
|
expect(loadingMessage.html()).toContain('Loading...');
|
||||||
|
@ -52,7 +52,7 @@ describe('<ShortUrlVisits />', () => {
|
||||||
|
|
||||||
it('renders a warning when loading large amounts of visits', () => {
|
it('renders a warning when loading large amounts of visits', () => {
|
||||||
const wrapper = createComponent({ loading: true, loadingLarge: true });
|
const wrapper = createComponent({ loading: true, loadingLarge: true });
|
||||||
const loadingMessage = wrapper.find(MutedMessage);
|
const loadingMessage = wrapper.find(Message);
|
||||||
|
|
||||||
expect(loadingMessage).toHaveLength(1);
|
expect(loadingMessage).toHaveLength(1);
|
||||||
expect(loadingMessage.html()).toContain('This is going to take a while... :S');
|
expect(loadingMessage.html()).toContain('This is going to take a while... :S');
|
||||||
|
@ -68,7 +68,7 @@ describe('<ShortUrlVisits />', () => {
|
||||||
|
|
||||||
it('renders a message when visits are loaded but the list is empty', () => {
|
it('renders a message when visits are loaded but the list is empty', () => {
|
||||||
const wrapper = createComponent({ loading: false, error: false, visits: [] });
|
const wrapper = createComponent({ loading: false, error: false, visits: [] });
|
||||||
const message = wrapper.find(MutedMessage);
|
const message = wrapper.find(Message);
|
||||||
|
|
||||||
expect(message).toHaveLength(1);
|
expect(message).toHaveLength(1);
|
||||||
expect(message.html()).toContain('There are no visits matching current filter :(');
|
expect(message.html()).toContain('There are no visits matching current filter :(');
|
||||||
|
|
Loading…
Reference in a new issue