mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-10 18:27:25 +03:00
Extracted servers list group from home component to a reusable component
This commit is contained in:
parent
6395e4e00b
commit
99042c0979
12 changed files with 138 additions and 92 deletions
|
@ -1,11 +1,9 @@
|
|||
import { faChevronRight as chevronIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { isEmpty, values } from 'ramda';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ListGroup, ListGroupItem } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Home.scss';
|
||||
import ServersListGroup from '../servers/ServersListGroup';
|
||||
|
||||
export default class Home extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -25,27 +23,11 @@ export default class Home extends React.Component {
|
|||
return (
|
||||
<div className="home">
|
||||
<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, <Link to="/server/create">add a server</Link>.</span>}
|
||||
{loading && <span>Trying to load servers...</span>}
|
||||
</h5>
|
||||
|
||||
{!loading && hasServers && (
|
||||
<ListGroup className="home__servers-list">
|
||||
{servers.map(({ name, id }) => (
|
||||
<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>
|
||||
)}
|
||||
</ServersListGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
@import '../utils/base';
|
||||
@import '../utils/mixins/vertical-align';
|
||||
|
||||
.home {
|
||||
text-align: center;
|
||||
|
@ -17,21 +16,3 @@
|
|||
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 * as PropTypes from 'prop-types';
|
||||
import { serverType } from '../servers/prop-types';
|
||||
import MutedMessage from '../utils/MutedMessage';
|
||||
import Message from '../utils/Message';
|
||||
import NotFound from './NotFound';
|
||||
import './MenuLayout.scss';
|
||||
|
||||
|
@ -28,19 +28,19 @@ const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisi
|
|||
useEffect(() => setShowSidebar(false), [ location ]);
|
||||
|
||||
if (!selectedServer) {
|
||||
return <MutedMessage loading />;
|
||||
return <Message loading />;
|
||||
}
|
||||
|
||||
if (selectedServer.serverNotFound) {
|
||||
return <MutedMessage>Could not find a server with id <b>"{serverId}"</b> in this host.</MutedMessage>;
|
||||
return <Message type="error">Could not find this Shlink server in this host.</Message>;
|
||||
}
|
||||
|
||||
if (selectedServer.serverNotReachable) {
|
||||
return (
|
||||
<MutedMessage>
|
||||
Oops! Could not connect to Shlink server with ID <b>"{serverId}"</b>. Make sure you have internet
|
||||
connection, the server is properly configured and it is on-line.
|
||||
</MutedMessage>
|
||||
<Message type="error">
|
||||
<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.
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
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 && (
|
||||
<ListGroup className="servers-list__list-group">
|
||||
{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 {
|
||||
margin-top: 1rem;
|
||||
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;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { splitEvery } from 'ramda';
|
||||
import PropTypes from 'prop-types';
|
||||
import MutedMessage from '../utils/MutedMessage';
|
||||
import Message from '../utils/Message';
|
||||
import SearchField from '../utils/SearchField';
|
||||
|
||||
const { ceil } = Math;
|
||||
|
@ -29,7 +29,7 @@ const TagsList = (TagCard) => class TagsList extends React.Component {
|
|||
const { tagsList, match } = this.props;
|
||||
|
||||
if (tagsList.loading) {
|
||||
return <MutedMessage noMargin loading />;
|
||||
return <Message noMargin loading />;
|
||||
}
|
||||
|
||||
if (tagsList.error) {
|
||||
|
@ -43,7 +43,7 @@ const TagsList = (TagCard) => class TagsList extends React.Component {
|
|||
const tagsCount = tagsList.filteredTags.length;
|
||||
|
||||
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);
|
||||
|
|
|
@ -5,21 +5,35 @@ import PropTypes from 'prop-types';
|
|||
import { faCircleNotch as preloader } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
const getClassForType = (type) => {
|
||||
const map = {
|
||||
error: 'bg-danger',
|
||||
};
|
||||
|
||||
return map[type] || 'bg-light';
|
||||
};
|
||||
const getTextClassForType = (type) => {
|
||||
const map = {
|
||||
error: 'text-white',
|
||||
};
|
||||
|
||||
return map[type] || 'text-muted';
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
noMargin: PropTypes.bool,
|
||||
loading: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
type: PropTypes.oneOf([ 'default', 'error' ]),
|
||||
};
|
||||
|
||||
const MutedMessage = ({ children, loading = false, noMargin = false }) => {
|
||||
const cardClasses = classNames('bg-light', {
|
||||
'mt-4': !noMargin,
|
||||
});
|
||||
const Message = ({ children, loading = false, noMargin = false, type = 'default' }) => {
|
||||
const cardClasses = classNames(getClassForType(type), { 'mt-4': !noMargin });
|
||||
|
||||
return (
|
||||
<div className="col-md-10 offset-md-1">
|
||||
<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 && !children && <span className="ml-2">Loading...</span>}
|
||||
{children}
|
||||
|
@ -29,6 +43,6 @@ const MutedMessage = ({ children, loading = false, noMargin = false }) => {
|
|||
);
|
||||
};
|
||||
|
||||
MutedMessage.propTypes = propTypes;
|
||||
Message.propTypes = propTypes;
|
||||
|
||||
export default MutedMessage;
|
||||
export default Message;
|
|
@ -4,7 +4,7 @@ import { Card } from 'reactstrap';
|
|||
import PropTypes from 'prop-types';
|
||||
import qs from 'qs';
|
||||
import DateRangeRow from '../utils/DateRangeRow';
|
||||
import MutedMessage from '../utils/MutedMessage';
|
||||
import Message from '../utils/Message';
|
||||
import { formatDate } from '../utils/utils';
|
||||
import SortableBarGraph from './SortableBarGraph';
|
||||
import { shortUrlVisitsType } from './reducers/shortUrlVisits';
|
||||
|
@ -64,7 +64,7 @@ const ShortUrlVisits = (
|
|||
if (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) {
|
||||
|
@ -76,7 +76,7 @@ const ShortUrlVisits = (
|
|||
}
|
||||
|
||||
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(
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { shallow } from 'enzyme';
|
||||
import { values } from 'ramda';
|
||||
import React from 'react';
|
||||
import Home from '../../src/common/Home';
|
||||
|
||||
describe('<Home />', () => {
|
||||
let wrapped;
|
||||
const defaultProps = {
|
||||
resetSelectedServer: () => '',
|
||||
resetSelectedServer: jest.fn(),
|
||||
servers: { loading: false, list: {} },
|
||||
};
|
||||
const createComponent = (props) => {
|
||||
|
@ -17,12 +16,7 @@ describe('<Home />', () => {
|
|||
return wrapped;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapped !== undefined) {
|
||||
wrapped.unmount();
|
||||
wrapped = undefined;
|
||||
}
|
||||
});
|
||||
afterEach(() => wrapped && wrapped.unmount());
|
||||
|
||||
it('resets selected server when mounted', () => {
|
||||
const resetSelectedServer = jest.fn();
|
||||
|
@ -36,7 +30,6 @@ describe('<Home />', () => {
|
|||
const wrapped = createComponent();
|
||||
|
||||
expect(wrapped.find('Link')).toHaveLength(1);
|
||||
expect(wrapped.find('ListGroup')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('shows message when loading servers', () => {
|
||||
|
@ -45,21 +38,5 @@ describe('<Home />', () => {
|
|||
|
||||
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 = {
|
||||
loading: false,
|
||||
list: {
|
||||
1: { name: 'foo', id: '123' },
|
||||
2: { name: 'bar', id: '456' },
|
||||
},
|
||||
};
|
||||
const wrapped = createComponent({ servers });
|
||||
|
||||
expect(wrapped.find('Link')).toHaveLength(0);
|
||||
expect(wrapped.find('ListGroup')).toHaveLength(1);
|
||||
expect(wrapped.find('ListGroupItem')).toHaveLength(values(servers).length);
|
||||
});
|
||||
});
|
||||
|
|
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);
|
||||
});
|
||||
});
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { shallow } from 'enzyme';
|
||||
import { identity } from 'ramda';
|
||||
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 { rangeOf } from '../../src/utils/utils';
|
||||
|
||||
|
@ -28,7 +28,7 @@ describe('<TagsList />', () => {
|
|||
|
||||
it('shows a loading message when tags are being loaded', () => {
|
||||
const wrapper = createWrapper({ loading: true });
|
||||
const loadingMsg = wrapper.find(MutedMessage);
|
||||
const loadingMsg = wrapper.find(Message);
|
||||
|
||||
expect(loadingMsg).toHaveLength(1);
|
||||
expect(loadingMsg.html()).toContain('Loading...');
|
||||
|
@ -44,7 +44,7 @@ describe('<TagsList />', () => {
|
|||
|
||||
it('shows a message when the list of tags is empty', () => {
|
||||
const wrapper = createWrapper({ filteredTags: [] });
|
||||
const msg = wrapper.find(MutedMessage);
|
||||
const msg = wrapper.find(Message);
|
||||
|
||||
expect(msg).toHaveLength(1);
|
||||
expect(msg.html()).toContain('No tags found');
|
||||
|
|
|
@ -3,7 +3,7 @@ import { shallow } from 'enzyme';
|
|||
import { identity } from 'ramda';
|
||||
import { Card } from 'reactstrap';
|
||||
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 SortableBarGraph from '../../src/visits/SortableBarGraph';
|
||||
import DateRangeRow from '../../src/utils/DateRangeRow';
|
||||
|
@ -44,7 +44,7 @@ describe('<ShortUrlVisits />', () => {
|
|||
|
||||
it('renders a preloader when visits are loading', () => {
|
||||
const wrapper = createComponent({ loading: true });
|
||||
const loadingMessage = wrapper.find(MutedMessage);
|
||||
const loadingMessage = wrapper.find(Message);
|
||||
|
||||
expect(loadingMessage).toHaveLength(1);
|
||||
expect(loadingMessage.html()).toContain('Loading...');
|
||||
|
@ -52,7 +52,7 @@ describe('<ShortUrlVisits />', () => {
|
|||
|
||||
it('renders a warning when loading large amounts of visits', () => {
|
||||
const wrapper = createComponent({ loading: true, loadingLarge: true });
|
||||
const loadingMessage = wrapper.find(MutedMessage);
|
||||
const loadingMessage = wrapper.find(Message);
|
||||
|
||||
expect(loadingMessage).toHaveLength(1);
|
||||
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', () => {
|
||||
const wrapper = createComponent({ loading: false, error: false, visits: [] });
|
||||
const message = wrapper.find(MutedMessage);
|
||||
const message = wrapper.find(Message);
|
||||
|
||||
expect(message).toHaveLength(1);
|
||||
expect(message.html()).toContain('There are no visits matching current filter :(');
|
||||
|
|
Loading…
Reference in a new issue