diff --git a/src/common/Home.js b/src/common/Home.js index e670e20e..79bdc57a 100644 --- a/src/common/Home.js +++ b/src/common/Home.js @@ -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 (

Welcome to Shlink

-
+ {!loading && hasServers && Please, select a server.} {!loading && !hasServers && Please, add a server.} {loading && Trying to load servers...} -
- - {!loading && hasServers && ( - - {servers.map(({ name, id }) => ( - - {name} - - - ))} - - )} +
); } diff --git a/src/common/Home.scss b/src/common/Home.scss index a57f033b..ee360009 100644 --- a/src/common/Home.scss +++ b/src/common/Home.scss @@ -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; -} diff --git a/src/common/MenuLayout.js b/src/common/MenuLayout.js index 6b5f5057..7a0a6ca7 100644 --- a/src/common/MenuLayout.js +++ b/src/common/MenuLayout.js @@ -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 ; + return ; } if (selectedServer.serverNotFound) { - return Could not find a server with id "{serverId}" in this host.; + return Could not find this Shlink server in this host.; } if (selectedServer.serverNotReachable) { return ( - - Oops! Could not connect to Shlink server with ID "{serverId}". Make sure you have internet - connection, the server is properly configured and it is on-line. - + +

Oops! Could not connect to this Shlink server.

+ Make sure you have internet connection, and the server is properly configured and on-line. +
); } diff --git a/src/servers/ServersListGroup.js b/src/servers/ServersListGroup.js new file mode 100644 index 00000000..764ca583 --- /dev/null +++ b/src/servers/ServersListGroup.js @@ -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 }) => ( + + {name} + + +); + +ServerListItem.propTypes = { + id: PropTypes.string, + name: PropTypes.string, +}; + +const ServersListGroup = ({ servers, children }) => ( + +
{children}
+ {servers.length && ( + + {servers.map(({ id, name }) => )} + + )} +
+); + +ServersListGroup.propTypes = propTypes; + +export default ServersListGroup; diff --git a/src/servers/ServersListGroup.scss b/src/servers/ServersListGroup.scss new file mode 100644 index 00000000..2b33ab3f --- /dev/null +++ b/src/servers/ServersListGroup.scss @@ -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; +} diff --git a/src/tags/TagsList.js b/src/tags/TagsList.js index 9cee6953..914a1ba8 100644 --- a/src/tags/TagsList.js +++ b/src/tags/TagsList.js @@ -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 ; + return ; } if (tagsList.error) { @@ -43,7 +43,7 @@ const TagsList = (TagCard) => class TagsList extends React.Component { const tagsCount = tagsList.filteredTags.length; if (tagsCount < 1) { - return No tags found; + return No tags found; } const tagsGroups = splitEvery(ceil(tagsCount / TAGS_GROUPS_AMOUNT), tagsList.filteredTags); diff --git a/src/utils/MutedMessage.js b/src/utils/Message.js similarity index 53% rename from src/utils/MutedMessage.js rename to src/utils/Message.js index 61e5c90c..368141c6 100644 --- a/src/utils/MutedMessage.js +++ b/src/utils/Message.js @@ -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 (
-

+

{loading && } {loading && !children && Loading...} {children} @@ -29,6 +43,6 @@ const MutedMessage = ({ children, loading = false, noMargin = false }) => { ); }; -MutedMessage.propTypes = propTypes; +Message.propTypes = propTypes; -export default MutedMessage; +export default Message; diff --git a/src/visits/ShortUrlVisits.js b/src/visits/ShortUrlVisits.js index a0875b0b..7330cc12 100644 --- a/src/visits/ShortUrlVisits.js +++ b/src/visits/ShortUrlVisits.js @@ -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 {message}; + return {message}; } if (error) { @@ -76,7 +76,7 @@ const ShortUrlVisits = ( } if (isEmpty(visits)) { - return There are no visits matching current filter :(; + return There are no visits matching current filter :(; } const { os, browsers, referrers, countries, cities, citiesForMap } = processStatsFromVisits( diff --git a/test/common/Home.test.js b/test/common/Home.test.js index 249b3e5d..c5812e4f 100644 --- a/test/common/Home.test.js +++ b/test/common/Home.test.js @@ -1,12 +1,11 @@ import { shallow } from 'enzyme'; -import { values } from 'ramda'; import React from 'react'; import Home from '../../src/common/Home'; describe('', () => { let wrapped; const defaultProps = { - resetSelectedServer: () => '', + resetSelectedServer: jest.fn(), servers: { loading: false, list: {} }, }; const createComponent = (props) => { @@ -17,12 +16,7 @@ describe('', () => { 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('', () => { 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('', () => { 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); }); }); diff --git a/test/servers/ServersListGroup.test.js b/test/servers/ServersListGroup.test.js new file mode 100644 index 00000000..b2871bbe --- /dev/null +++ b/test/servers/ServersListGroup.test.js @@ -0,0 +1,34 @@ +import { shallow } from 'enzyme'; +import React from 'react'; +import { ListGroup } from 'reactstrap'; +import ServersListGroup from '../../src/servers/ServersListGroup'; + +describe('', () => { + let wrapped; + const createComponent = (servers) => { + wrapped = shallow(The list of servers); + + 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); + }); +}); diff --git a/test/tags/TagsList.test.js b/test/tags/TagsList.test.js index 5a2ce549..bba25539 100644 --- a/test/tags/TagsList.test.js +++ b/test/tags/TagsList.test.js @@ -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('', () => { 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('', () => { 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'); diff --git a/test/visits/ShortUrlVisits.test.js b/test/visits/ShortUrlVisits.test.js index 004019cf..785f216a 100644 --- a/test/visits/ShortUrlVisits.test.js +++ b/test/visits/ShortUrlVisits.test.js @@ -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('', () => { 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('', () => { 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('', () => { 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 :(');