From 7516ca8dd9e373328446b40ae95025ebcd05ce9e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 18 Apr 2020 20:31:20 +0200 Subject: [PATCH 1/5] Created settings page and converted MainHeader into functional component --- src/App.js | 4 ++- src/common/MainHeader.js | 58 ++++++++++++++++------------------- src/common/NoMenuLayout.js | 13 ++++++++ src/common/NoMenuLayout.scss | 3 ++ src/servers/CreateServer.js | 7 +++-- src/servers/CreateServer.scss | 4 --- src/servers/EditServer.js | 5 +-- src/settings/Settings.js | 6 ++++ 8 files changed, 59 insertions(+), 41 deletions(-) create mode 100644 src/common/NoMenuLayout.js create mode 100644 src/common/NoMenuLayout.scss create mode 100644 src/settings/Settings.js diff --git a/src/App.js b/src/App.js index 4cecebff..fffc6705 100644 --- a/src/App.js +++ b/src/App.js @@ -1,7 +1,8 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; -import './App.scss'; import NotFound from './common/NotFound'; +import Settings from './settings/Settings'; +import './App.scss'; const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer) => () => (
@@ -12,6 +13,7 @@ const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer) => () => ( + diff --git a/src/common/MainHeader.js b/src/common/MainHeader.js index 5e582212..eecaf464 100644 --- a/src/common/MainHeader.js +++ b/src/common/MainHeader.js @@ -1,37 +1,28 @@ -import { faPlus as plusIcon, faChevronDown as arrowIcon } from '@fortawesome/free-solid-svg-icons'; +import { faPlus as plusIcon, faChevronDown as arrowIcon, faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import React from 'react'; +import React, { useEffect } from 'react'; import { Link } from 'react-router-dom'; import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap'; -import classnames from 'classnames'; +import classNames from 'classnames'; import PropTypes from 'prop-types'; +import { useToggle } from '../utils/helpers/hooks'; import shlinkLogo from './shlink-logo-white.png'; import './MainHeader.scss'; -const MainHeader = (ServersDropdown) => class MainHeader extends React.Component { - static propTypes = { - location: PropTypes.object, - }; +const propTypes = { + location: PropTypes.object, +}; - state = { isOpen: false }; - handleToggle = () => { - this.setState(({ isOpen }) => ({ - isOpen: !isOpen, - })); - }; +const MainHeader = (ServersDropdown) => { + const MainHeaderComp = ({ location }) => { + const [ isOpen, toggleOpen, , close ] = useToggle(); + const { pathname } = location; - componentDidUpdate(prevProps) { - if (this.props.location !== prevProps.location) { - this.setState({ isOpen: false }); - } - } + useEffect(close, [ location ]); - render() { - const { location } = this.props; const createServerPath = '/server/create'; - const toggleClass = classnames('main-header__toggle-icon', { - 'main-header__toggle-icon--opened': this.state.isOpen, - }); + const settingsPath = '/settings'; + const toggleClass = classNames('main-header__toggle-icon', { 'main-header__toggle-icon--opened': isOpen }); return ( @@ -39,18 +30,19 @@ const MainHeader = (ServersDropdown) => class MainHeader extends React.Component Shlink Shlink - + - +
)} - + ); }; diff --git a/src/servers/CreateServer.scss b/src/servers/CreateServer.scss index 764520c7..cfba848d 100644 --- a/src/servers/CreateServer.scss +++ b/src/servers/CreateServer.scss @@ -1,9 +1,5 @@ @import '../utils/base'; -.create-server { - padding: 40px 20px; -} - .create-server__label { font-weight: 700; cursor: pointer; diff --git a/src/servers/EditServer.js b/src/servers/EditServer.js index c4d4b0eb..6a6b89b6 100644 --- a/src/servers/EditServer.js +++ b/src/servers/EditServer.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import NoMenuLayout from '../common/NoMenuLayout'; import { ServerForm } from './helpers/ServerForm'; import { withSelectedServer } from './helpers/withSelectedServer'; import { serverType } from './prop-types'; @@ -20,11 +21,11 @@ export const EditServer = (ServerError) => { }; return ( -
+ -
+ ); }; diff --git a/src/settings/Settings.js b/src/settings/Settings.js new file mode 100644 index 00000000..cacd8581 --- /dev/null +++ b/src/settings/Settings.js @@ -0,0 +1,6 @@ +import React from 'react'; +import NoMenuLayout from '../common/NoMenuLayout'; + +const Settings = () => Settings; + +export default Settings; From 41f885d8ec89e86103f4c41b6f39b15477d8052d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 25 Apr 2020 09:49:54 +0200 Subject: [PATCH 2/5] Created settings page and reducers to handle real-time updates config --- src/App.js | 37 ++++++++++++++---------- src/container/index.js | 5 +++- src/reducers/index.js | 2 ++ src/settings/RealTimeUpdates.js | 31 ++++++++++++++++++++ src/settings/Settings.js | 6 +++- src/settings/reducers/realTimeUpdates.js | 32 ++++++++++++++++++++ src/settings/services/SettingsService.js | 14 +++++++++ src/settings/services/provideServices.js | 21 ++++++++++++++ test/App.test.js | 1 + 9 files changed, 131 insertions(+), 18 deletions(-) create mode 100644 src/settings/RealTimeUpdates.js create mode 100644 src/settings/reducers/realTimeUpdates.js create mode 100644 src/settings/services/SettingsService.js create mode 100644 src/settings/services/provideServices.js diff --git a/src/App.js b/src/App.js index fffc6705..fd6c859c 100644 --- a/src/App.js +++ b/src/App.js @@ -1,24 +1,29 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Route, Switch } from 'react-router-dom'; import NotFound from './common/NotFound'; -import Settings from './settings/Settings'; import './App.scss'; -const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer) => () => ( -
- +const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer, Settings) => ({ loadRealTimeUpdates }) => { + useEffect(() => { + loadRealTimeUpdates(); + }, []); -
- - - - - - - - + return ( +
+ + +
+ + + + + + + + +
-
-); + ); +}; export default App; diff --git a/src/container/index.js b/src/container/index.js index d634dffb..95383b13 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -10,6 +10,7 @@ import provideVisitsServices from '../visits/services/provideServices'; import provideTagsServices from '../tags/services/provideServices'; import provideUtilsServices from '../utils/services/provideServices'; import provideMercureServices from '../mercure/services/provideServices'; +import provideSettingsServices from '../settings/services/provideServices'; const bottle = new Bottle(); const { container } = bottle; @@ -27,7 +28,8 @@ const connect = (propsFromState, actionServiceNames = []) => actionServiceNames.reduce(mapActionService, {}) ); -bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer', 'EditServer'); +bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer', 'EditServer', 'Settings'); +bottle.decorator('App', connect(null, [ 'loadRealTimeUpdates' ])); provideCommonServices(bottle, connect, withRouter); provideShortUrlsServices(bottle, connect); @@ -36,5 +38,6 @@ provideTagsServices(bottle, connect); provideVisitsServices(bottle, connect); provideUtilsServices(bottle); provideMercureServices(bottle); +provideSettingsServices(bottle, connect); export default container; diff --git a/src/reducers/index.js b/src/reducers/index.js index 8b96ede8..8e81e18a 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -14,6 +14,7 @@ import tagsListReducer from '../tags/reducers/tagsList'; import tagDeleteReducer from '../tags/reducers/tagDelete'; import tagEditReducer from '../tags/reducers/tagEdit'; import mercureInfoReducer from '../mercure/reducers/mercureInfo'; +import realTimeUpdatesReducer from '../settings/reducers/realTimeUpdates'; export default combineReducers({ servers: serversReducer, @@ -31,4 +32,5 @@ export default combineReducers({ tagDelete: tagDeleteReducer, tagEdit: tagEditReducer, mercureInfo: mercureInfoReducer, + realTimeUpdates: realTimeUpdatesReducer, }); diff --git a/src/settings/RealTimeUpdates.js b/src/settings/RealTimeUpdates.js new file mode 100644 index 00000000..eccd585f --- /dev/null +++ b/src/settings/RealTimeUpdates.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { Card, CardBody, CardHeader, UncontrolledTooltip } from 'reactstrap'; +import PropTypes from 'prop-types'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; +import Checkbox from '../utils/Checkbox'; +import { RealTimeUpdatesType } from './reducers/realTimeUpdates'; + +const propTypes = { + realTimeUpdates: RealTimeUpdatesType, + setRealTimeUpdates: PropTypes.func, +}; + +const RealTimeUpdates = ({ realTimeUpdates, setRealTimeUpdates }) => ( + + Real-time updates + + + Enable real-time updates + + + + Enable or disable real-time updates, when using Shlink v2.2.0 or newer. + + + +); + +RealTimeUpdates.propTypes = propTypes; + +export default RealTimeUpdates; diff --git a/src/settings/Settings.js b/src/settings/Settings.js index cacd8581..81b662bd 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -1,6 +1,10 @@ import React from 'react'; import NoMenuLayout from '../common/NoMenuLayout'; -const Settings = () => Settings; +const Settings = (RealTimeUpdates) => () => ( + + + +); export default Settings; diff --git a/src/settings/reducers/realTimeUpdates.js b/src/settings/reducers/realTimeUpdates.js new file mode 100644 index 00000000..19fac094 --- /dev/null +++ b/src/settings/reducers/realTimeUpdates.js @@ -0,0 +1,32 @@ +import { handleActions } from 'redux-actions'; +import PropTypes from 'prop-types'; + +const LOAD_REAL_TIME_UPDATES = 'shlink/realTimeUpdates/LOAD_REAL_TIME_UPDATES'; + +export const RealTimeUpdatesType = PropTypes.shape({ + enabled: PropTypes.bool.isRequired, +}); + +const initialState = { + enabled: true, +}; + +export default handleActions({ + [LOAD_REAL_TIME_UPDATES]: (state, { enabled }) => ({ ...state, enabled }), +}, initialState); + +export const setRealTimeUpdates = ({ updateSettings }, loadRealTimeUpdatesAction) => (enabled) => { + updateSettings({ realTimeUpdates: { enabled } }); + + return loadRealTimeUpdatesAction(); +}; + +export const loadRealTimeUpdates = ({ loadSettings }) => () => { + const { realTimeUpdates = {} } = loadSettings(); + const { enabled = true } = realTimeUpdates; + + return { + type: LOAD_REAL_TIME_UPDATES, + enabled, + }; +}; diff --git a/src/settings/services/SettingsService.js b/src/settings/services/SettingsService.js new file mode 100644 index 00000000..a3a39446 --- /dev/null +++ b/src/settings/services/SettingsService.js @@ -0,0 +1,14 @@ +const SETTINGS_STORAGE_KEY = 'settings'; + +export default class SettingsService { + constructor(storage) { + this.storage = storage; + } + + loadSettings = () => this.storage.get(SETTINGS_STORAGE_KEY) || {}; + + updateSettings = (settingsToUpdate) => this.storage.set(SETTINGS_STORAGE_KEY, { + ...this.loadSettings(), + ...settingsToUpdate, + }) +} diff --git a/src/settings/services/provideServices.js b/src/settings/services/provideServices.js new file mode 100644 index 00000000..ef70b49f --- /dev/null +++ b/src/settings/services/provideServices.js @@ -0,0 +1,21 @@ +import RealTimeUpdates from '../RealTimeUpdates'; +import Settings from '../Settings'; +import { loadRealTimeUpdates, setRealTimeUpdates } from '../reducers/realTimeUpdates'; +import SettingsService from './SettingsService'; + +const provideServices = (bottle, connect) => { + // Components + bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates'); + + bottle.serviceFactory('RealTimeUpdates', () => RealTimeUpdates); + bottle.decorator('RealTimeUpdates', connect([ 'realTimeUpdates' ], [ 'setRealTimeUpdates' ])); + + // Services + bottle.service('SettingsService', SettingsService, 'Storage'); + + // Actions + bottle.serviceFactory('setRealTimeUpdates', setRealTimeUpdates, 'SettingsService', 'loadRealTimeUpdates'); + bottle.serviceFactory('loadRealTimeUpdates', loadRealTimeUpdates, 'SettingsService'); +}; + +export default provideServices; diff --git a/test/App.test.js b/test/App.test.js index 668239ca..ef67971c 100644 --- a/test/App.test.js +++ b/test/App.test.js @@ -21,6 +21,7 @@ describe('', () => { const routes = wrapper.find(Route); const expectedPaths = [ '/', + '/settings', '/server/create', '/server/:serverId/edit', '/server/:serverId', From 6a6c427b0ee268225120c4564b22dd9592c3c163 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 25 Apr 2020 10:22:17 +0200 Subject: [PATCH 3/5] Added unit tests for settings business logic elements --- src/settings/reducers/realTimeUpdates.js | 2 +- test/servers/reducers/server.test.js | 4 +- .../settings/reducers/realTimeUpdates.test.js | 47 +++++++++++++++++++ .../settings/services/SettingsService.test.js | 46 ++++++++++++++++++ 4 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 test/settings/reducers/realTimeUpdates.test.js create mode 100644 test/settings/services/SettingsService.test.js diff --git a/src/settings/reducers/realTimeUpdates.js b/src/settings/reducers/realTimeUpdates.js index 19fac094..c5cd726f 100644 --- a/src/settings/reducers/realTimeUpdates.js +++ b/src/settings/reducers/realTimeUpdates.js @@ -1,7 +1,7 @@ import { handleActions } from 'redux-actions'; import PropTypes from 'prop-types'; -const LOAD_REAL_TIME_UPDATES = 'shlink/realTimeUpdates/LOAD_REAL_TIME_UPDATES'; +export const LOAD_REAL_TIME_UPDATES = 'shlink/realTimeUpdates/LOAD_REAL_TIME_UPDATES'; export const RealTimeUpdatesType = PropTypes.shape({ enabled: PropTypes.bool.isRequired, diff --git a/test/servers/reducers/server.test.js b/test/servers/reducers/server.test.js index f6c55325..baa4e82f 100644 --- a/test/servers/reducers/server.test.js +++ b/test/servers/reducers/server.test.js @@ -4,7 +4,9 @@ import reducer, { deleteServer, listServers, createServers, - FETCH_SERVERS, FETCH_SERVERS_START, editServer, + editServer, + FETCH_SERVERS, + FETCH_SERVERS_START, } from '../../../src/servers/reducers/server'; describe('serverReducer', () => { diff --git a/test/settings/reducers/realTimeUpdates.test.js b/test/settings/reducers/realTimeUpdates.test.js new file mode 100644 index 00000000..cdd463e6 --- /dev/null +++ b/test/settings/reducers/realTimeUpdates.test.js @@ -0,0 +1,47 @@ +import reducer, { + LOAD_REAL_TIME_UPDATES, + loadRealTimeUpdates, + setRealTimeUpdates, +} from '../../../src/settings/reducers/realTimeUpdates'; + +describe('realTimeUpdatesReducer', () => { + const SettingsServiceMock = { + updateSettings: jest.fn(), + loadSettings: jest.fn(), + }; + + afterEach(jest.clearAllMocks); + + describe('reducer', () => { + it('returns realTimeUpdates when action is LOAD_REAL_TIME_UPDATES', () => { + expect(reducer({}, { type: LOAD_REAL_TIME_UPDATES, enabled: true })).toEqual({ enabled: true }); + }); + }); + + describe('loadRealTimeUpdates', () => { + it.each([ + [{}, true ], + [{ realTimeUpdates: {} }, true ], + [{ realTimeUpdates: { enabled: true } }, true ], + [{ realTimeUpdates: { enabled: false } }, false ], + ])('loads settings and returns LOAD_REAL_TIME_UPDATES action', (loadedSettings, expectedEnabled) => { + SettingsServiceMock.loadSettings.mockReturnValue(loadedSettings); + + const result = loadRealTimeUpdates(SettingsServiceMock)(); + + expect(result).toEqual({ type: LOAD_REAL_TIME_UPDATES, enabled: expectedEnabled }); + expect(SettingsServiceMock.loadSettings).toHaveBeenCalled(); + }); + }); + + describe('setRealTimeUpdates', () => { + it.each([[ true ], [ false ]])('updates settings with provided value and then loads updates again', (enabled) => { + const loadRealTimeUpdatesAction = jest.fn(); + + setRealTimeUpdates(SettingsServiceMock, loadRealTimeUpdatesAction)(enabled); + + expect(SettingsServiceMock.updateSettings).toHaveBeenCalledWith({ realTimeUpdates: { enabled } }); + expect(loadRealTimeUpdatesAction).toHaveBeenCalled(); + }); + }); +}); diff --git a/test/settings/services/SettingsService.test.js b/test/settings/services/SettingsService.test.js new file mode 100644 index 00000000..9e9419db --- /dev/null +++ b/test/settings/services/SettingsService.test.js @@ -0,0 +1,46 @@ +import SettingsService from '../../../src/settings/services/SettingsService'; + +describe('SettingsService', () => { + const settings = { foo: 'bar' }; + const createService = (withSettings = true) => { + const storageMock = { + set: jest.fn(), + get: jest.fn(() => withSettings ? settings : undefined), + }; + const service = new SettingsService(storageMock); + + return [ service, storageMock ]; + }; + + afterEach(jest.resetAllMocks); + + describe('loadSettings', () => { + it.each([ + [ false, {}], + [ true, settings ], + ])('returns result if found in storage', (withSettings, expectedResult) => { + const [ service, storageMock ] = createService(withSettings); + + const result = service.loadSettings(); + + expect(result).toEqual(expectedResult); + expect(storageMock.get).toHaveBeenCalledTimes(1); + expect(storageMock.set).not.toHaveBeenCalled(); + }); + }); + + describe('updateSettings', () => { + it.each([ + [ false, { hi: 'goodbye' }, { hi: 'goodbye' }], + [ true, { hi: 'goodbye' }, { foo: 'bar', hi: 'goodbye' }], + [ true, { foo: 'goodbye' }, { foo: 'goodbye' }], + ])('appends provided data to existing settings', (withSettings, settingsToUpdate, expectedResult) => { + const [ service, storageMock ] = createService(withSettings); + + service.updateSettings(settingsToUpdate); + + expect(storageMock.get).toHaveBeenCalledTimes(1); + expect(storageMock.set).toHaveBeenCalledWith(expect.anything(), expectedResult); + }); + }); +}); From 6fcf588bfd37ee00b1f85cf9f0646a72d76199df Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 25 Apr 2020 10:23:47 +0200 Subject: [PATCH 4/5] Updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad9b23ea..69b25f6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * If it works, it will setup the necessary `EventSource`s, dispatching redux actions when an event is pushed, which will in turn update the UI. * If it fails, it will assume it is either not configured or not supported by the Shlink version. +* [#253](https://github.com/shlinkio/shlink-web-client/issues/253) Created new settings page that will be used to define customizations in the app. + #### Changed * *Nothing* From 16dee3664bcfe1ee8e2c4e34b135c5dc73792a7d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 25 Apr 2020 10:37:50 +0200 Subject: [PATCH 5/5] Ensured mercure updates are not set even if supported, when they have been disabled --- src/mercure/helpers/index.js | 5 +++-- src/short-urls/ShortUrlsList.js | 5 ++++- src/short-urls/services/provideServices.js | 2 +- src/visits/ShortUrlVisits.js | 11 ++++++++++- src/visits/services/provideServices.js | 2 +- test/mercure/helpers/index.test.js | 13 +++++++------ 6 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/mercure/helpers/index.js b/src/mercure/helpers/index.js index 0cdd367c..fd69ce08 100644 --- a/src/mercure/helpers/index.js +++ b/src/mercure/helpers/index.js @@ -1,9 +1,10 @@ import { EventSourcePolyfill as EventSource } from 'event-source-polyfill'; -export const bindToMercureTopic = (mercureInfo, topic, onMessage, onTokenExpired) => () => { +export const bindToMercureTopic = (mercureInfo, realTimeUpdates, topic, onMessage, onTokenExpired) => () => { + const { enabled } = realTimeUpdates; const { mercureHubUrl, token, loading, error } = mercureInfo; - if (loading || error) { + if (!enabled || loading || error) { return undefined; } diff --git a/src/short-urls/ShortUrlsList.js b/src/short-urls/ShortUrlsList.js index 9df2250b..3b36d459 100644 --- a/src/short-urls/ShortUrlsList.js +++ b/src/short-urls/ShortUrlsList.js @@ -9,6 +9,7 @@ import SortingDropdown from '../utils/SortingDropdown'; import { determineOrderDir } from '../utils/utils'; import { MercureInfoType } from '../mercure/reducers/mercureInfo'; import { bindToMercureTopic } from '../mercure/helpers'; +import { RealTimeUpdatesType } from '../settings/reducers/realTimeUpdates'; import { shortUrlType } from './reducers/shortUrlsList'; import { shortUrlsListParamsType } from './reducers/shortUrlsListParams'; import './ShortUrlsList.scss'; @@ -33,6 +34,7 @@ const propTypes = { createNewVisit: PropTypes.func, loadMercureInfo: PropTypes.func, mercureInfo: MercureInfoType, + realTimeUpdates: RealTimeUpdatesType, }; // FIXME Replace with typescript: (ShortUrlsRow component) @@ -50,6 +52,7 @@ const ShortUrlsList = (ShortUrlsRow) => { createNewVisit, loadMercureInfo, mercureInfo, + realTimeUpdates, }) => { const { orderBy } = shortUrlsListParams; const [ order, setOrder ] = useState({ @@ -117,7 +120,7 @@ const ShortUrlsList = (ShortUrlsRow) => { return resetShortUrlParams; }, []); useEffect( - bindToMercureTopic(mercureInfo, 'https://shlink.io/new-visit', createNewVisit, loadMercureInfo), + bindToMercureTopic(mercureInfo, realTimeUpdates, 'https://shlink.io/new-visit', createNewVisit, loadMercureInfo), [ mercureInfo ] ); diff --git a/src/short-urls/services/provideServices.js b/src/short-urls/services/provideServices.js index 385b0a1c..6a35ac3d 100644 --- a/src/short-urls/services/provideServices.js +++ b/src/short-urls/services/provideServices.js @@ -31,7 +31,7 @@ const provideServices = (bottle, connect) => { bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow'); bottle.decorator('ShortUrlsList', connect( - [ 'selectedServer', 'shortUrlsListParams', 'mercureInfo' ], + [ 'selectedServer', 'shortUrlsListParams', 'mercureInfo', 'realTimeUpdates' ], [ 'listShortUrls', 'resetShortUrlParams', 'createNewVisit', 'loadMercureInfo' ] )); diff --git a/src/visits/ShortUrlVisits.js b/src/visits/ShortUrlVisits.js index d253b486..410d390b 100644 --- a/src/visits/ShortUrlVisits.js +++ b/src/visits/ShortUrlVisits.js @@ -12,6 +12,7 @@ import { formatDate } from '../utils/helpers/date'; import { useToggle } from '../utils/helpers/hooks'; import { MercureInfoType } from '../mercure/reducers/mercureInfo'; import { bindToMercureTopic } from '../mercure/helpers'; +import { RealTimeUpdatesType } from '../settings/reducers/realTimeUpdates'; import SortableBarGraph from './SortableBarGraph'; import { shortUrlVisitsType } from './reducers/shortUrlVisits'; import VisitsHeader from './VisitsHeader'; @@ -35,6 +36,7 @@ const propTypes = { createNewVisit: PropTypes.func, loadMercureInfo: PropTypes.func, mercureInfo: MercureInfoType, + realTimeUpdates: RealTimeUpdatesType, }; const highlightedVisitsToStats = (highlightedVisits, prop) => highlightedVisits.reduce((acc, highlightedVisit) => { @@ -62,6 +64,7 @@ const ShortUrlVisits = ({ processStatsFromVisits, normalizeVisits }, OpenMapModa createNewVisit, loadMercureInfo, mercureInfo, + realTimeUpdates, }) => { const [ startDate, setStartDate ] = useState(undefined); const [ endDate, setEndDate ] = useState(undefined); @@ -117,7 +120,13 @@ const ShortUrlVisits = ({ processStatsFromVisits, normalizeVisits }, OpenMapModa loadVisits(); }, [ startDate, endDate ]); useEffect( - bindToMercureTopic(mercureInfo, `https://shlink.io/new-visit/${shortCode}`, createNewVisit, loadMercureInfo), + bindToMercureTopic( + mercureInfo, + realTimeUpdates, + `https://shlink.io/new-visit/${shortCode}`, + createNewVisit, + loadMercureInfo + ), [ mercureInfo ], ); diff --git a/src/visits/services/provideServices.js b/src/visits/services/provideServices.js index f2363f67..f9f31b80 100644 --- a/src/visits/services/provideServices.js +++ b/src/visits/services/provideServices.js @@ -11,7 +11,7 @@ const provideServices = (bottle, connect) => { bottle.serviceFactory('MapModal', () => MapModal); bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'VisitsParser', 'OpenMapModalBtn'); bottle.decorator('ShortUrlVisits', connect( - [ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo' ], + [ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'realTimeUpdates' ], [ 'getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisit', 'loadMercureInfo' ] )); diff --git a/test/mercure/helpers/index.test.js b/test/mercure/helpers/index.test.js index 1e6fd3df..3ee91a3c 100644 --- a/test/mercure/helpers/index.test.js +++ b/test/mercure/helpers/index.test.js @@ -11,11 +11,12 @@ describe('helpers', () => { const onTokenExpired = jest.fn(); it.each([ - [{ loading: true, error: false }], - [{ loading: false, error: true }], - [{ loading: true, error: true }], - ])('does not bind an EventSource when loading or error', (mercureInfo) => { - bindToMercureTopic(mercureInfo)(); + [{ loading: true, error: false }, { enabled: true }], + [{ loading: false, error: true }, { enabled: true }], + [{ loading: true, error: true }, { enabled: true }], + [{ loading: false, error: false }, { enabled: false }], + ])('does not bind an EventSource when disabled, loading or error', (mercureInfo, realTimeUpdates) => { + bindToMercureTopic(mercureInfo, realTimeUpdates)(); expect(EventSource).not.toHaveBeenCalled(); expect(onMessage).not.toHaveBeenCalled(); @@ -35,7 +36,7 @@ describe('helpers', () => { error: false, mercureHubUrl, token, - }, topic, onMessage, onTokenExpired)(); + }, { enabled: true }, topic, onMessage, onTokenExpired)(); expect(EventSource).toHaveBeenCalledWith(hubUrl, { headers: {