From ab7718e3350fdfe656b0244b2a1b543be375af8e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 5 Mar 2020 10:03:38 +0100 Subject: [PATCH 01/11] Removed duplicated code from AsideMenu by creating an AsideMenuItem helper component --- src/common/AsideMenu.js | 50 +++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/src/common/AsideMenu.js b/src/common/AsideMenu.js index 7850488b..56e7d10c 100644 --- a/src/common/AsideMenu.js +++ b/src/common/AsideMenu.js @@ -3,14 +3,21 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React from 'react'; import { NavLink } from 'react-router-dom'; import PropTypes from 'prop-types'; -import classnames from 'classnames'; +import classNames from 'classnames'; import { serverType } from '../servers/prop-types'; import './AsideMenu.scss'; -const defaultProps = { - className: '', - showOnMobile: false, +const AsideMenuItem = ({ children, to, ...rest }) => ( + + {children} + +); + +AsideMenuItem.propTypes = { + children: PropTypes.node.isRequired, + to: PropTypes.string.isRequired, }; + const propTypes = { selectedServer: serverType, className: PropTypes.string, @@ -20,51 +27,34 @@ const propTypes = { const AsideMenu = (DeleteServerButton) => { const AsideMenu = ({ selectedServer, className, showOnMobile }) => { const serverId = selectedServer ? selectedServer.id : ''; - const asideClass = classnames('aside-menu', className, { + const asideClass = classNames('aside-menu', className, { 'aside-menu--hidden': !showOnMobile, }); const shortUrlsIsActive = (match, location) => location.pathname.match('/list-short-urls'); + const buildPath = (suffix) => `/server/${serverId}${suffix}`; return ( ); }; - AsideMenu.defaultProps = defaultProps; AsideMenu.propTypes = propTypes; return AsideMenu; From b02dcf6c53d2ab858a0eae6f7b95e94bb4b7e397 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 5 Mar 2020 10:18:33 +0100 Subject: [PATCH 02/11] Refactored delete server components --- src/common/AsideMenu.js | 3 ++- src/common/ShlinkVersions.js | 16 ++++++++++++ src/common/services/provideServices.js | 6 ++++- src/servers/DeleteServerButton.js | 36 ++++++++++++-------------- src/servers/DeleteServerModal.js | 10 ++++--- 5 files changed, 46 insertions(+), 25 deletions(-) create mode 100644 src/common/ShlinkVersions.js diff --git a/src/common/AsideMenu.js b/src/common/AsideMenu.js index 56e7d10c..80f0cd2c 100644 --- a/src/common/AsideMenu.js +++ b/src/common/AsideMenu.js @@ -24,7 +24,7 @@ const propTypes = { showOnMobile: PropTypes.bool, }; -const AsideMenu = (DeleteServerButton) => { +const AsideMenu = (DeleteServerButton, ShlinkVersions) => { const AsideMenu = ({ selectedServer, className, showOnMobile }) => { const serverId = selectedServer ? selectedServer.id : ''; const asideClass = classNames('aside-menu', className, { @@ -49,6 +49,7 @@ const AsideMenu = (DeleteServerButton) => { Manage tags + diff --git a/src/common/ShlinkVersions.js b/src/common/ShlinkVersions.js new file mode 100644 index 00000000..37726936 --- /dev/null +++ b/src/common/ShlinkVersions.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { serverType } from '../servers/prop-types'; + +const propTypes = { + selectedServer: serverType, +}; + +const ShlinkVersions = ({ selectedServer }) => { + const { version } = selectedServer; + + return Server: v{version}; +}; + +ShlinkVersions.propTypes = propTypes; + +export default ShlinkVersions; diff --git a/src/common/services/provideServices.js b/src/common/services/provideServices.js index f8f5c980..fdb25687 100644 --- a/src/common/services/provideServices.js +++ b/src/common/services/provideServices.js @@ -4,6 +4,7 @@ import Home from '../Home'; import MenuLayout from '../MenuLayout'; import AsideMenu from '../AsideMenu'; import ErrorHandler from '../ErrorHandler'; +import ShlinkVersions from '../ShlinkVersions'; const provideServices = (bottle, connect, withRouter) => { bottle.constant('window', global.window); @@ -30,7 +31,10 @@ const provideServices = (bottle, connect, withRouter) => { bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ])); bottle.decorator('MenuLayout', withRouter); - bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton'); + bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton', 'ShlinkVersions'); + + bottle.serviceFactory('ShlinkVersions', () => ShlinkVersions); + bottle.decorator('ShlinkVersions', connect([ 'selectedServer' ])); bottle.serviceFactory('ErrorHandler', ErrorHandler, 'window', 'console'); }; diff --git a/src/servers/DeleteServerButton.js b/src/servers/DeleteServerButton.js index f5dce2da..8d0caaff 100644 --- a/src/servers/DeleteServerButton.js +++ b/src/servers/DeleteServerButton.js @@ -1,40 +1,38 @@ +import React, { useState } from 'react'; import { faMinusCircle as deleteIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import React from 'react'; import PropTypes from 'prop-types'; import { serverType } from './prop-types'; -const DeleteServerButton = (DeleteServerModal) => class DeleteServerButton extends React.Component { - static propTypes = { - server: serverType, - className: PropTypes.string, - }; +const propTypes = { + server: serverType, + className: PropTypes.string, +}; - state = { isModalOpen: false }; - - render() { - const { server, className } = this.props; +const DeleteServerButton = (DeleteServerModal) => { + const DeleteServerButtonComp = ({ server, className }) => { + const [ isModalOpen, setModalOpen ] = useState(false); return ( - this.setState({ isModalOpen: true })} - > + setModalOpen(true)}> - Delete this server + Remove this server this.setState(({ isModalOpen }) => ({ isModalOpen: !isModalOpen }))} + isOpen={isModalOpen} + toggle={() => setModalOpen(!isModalOpen)} server={server} key="deleteServerModal" /> ); - } + }; + + DeleteServerButtonComp.propTypes = propTypes; + + return DeleteServerButtonComp; }; export default DeleteServerButton; diff --git a/src/servers/DeleteServerModal.js b/src/servers/DeleteServerModal.js index da056acf..756c9c8e 100644 --- a/src/servers/DeleteServerModal.js +++ b/src/servers/DeleteServerModal.js @@ -22,12 +22,14 @@ const DeleteServerModal = ({ server, toggle, isOpen, deleteServer, history }) => return ( - Delete server + Remove server -

Are you sure you want to delete server {server ? server.name : ''}?

+

Are you sure you want to remove {server ? server.name : ''}?

- No data will be deleted, only the access to that server will be removed from this host. - You can create it again at any moment. + + No data will be deleted, only the access to this server will be removed from this host. + You can create it again at any moment. +

From 1e949b3a22d20160df00b1c998fef8caccc9b41b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 5 Mar 2020 11:11:26 +0100 Subject: [PATCH 03/11] Added shlink versions to side menu --- src/common/AsideMenu.js | 3 +- src/common/MenuLayout.js | 20 ++-- src/common/ShlinkVersions.js | 9 +- src/common/services/provideServices.js | 2 +- src/servers/DeleteServerButton.js | 4 +- src/servers/prop-types/index.js | 1 + src/servers/reducers/selectedServer.js | 14 ++- src/servers/services/provideServices.js | 2 +- src/short-urls/CreateShortUrl.js | 138 ++++++++++++------------ src/short-urls/ShortUrls.js | 4 +- src/tags/TagsList.js | 4 +- src/utils/utils.js | 6 +- src/visits/ShortUrlVisits.js | 4 +- 13 files changed, 110 insertions(+), 101 deletions(-) diff --git a/src/common/AsideMenu.js b/src/common/AsideMenu.js index 80f0cd2c..56e7d10c 100644 --- a/src/common/AsideMenu.js +++ b/src/common/AsideMenu.js @@ -24,7 +24,7 @@ const propTypes = { showOnMobile: PropTypes.bool, }; -const AsideMenu = (DeleteServerButton, ShlinkVersions) => { +const AsideMenu = (DeleteServerButton) => { const AsideMenu = ({ selectedServer, className, showOnMobile }) => { const serverId = selectedServer ? selectedServer.id : ''; const asideClass = classNames('aside-menu', className, { @@ -49,7 +49,6 @@ const AsideMenu = (DeleteServerButton, ShlinkVersions) => { Manage tags - diff --git a/src/common/MenuLayout.js b/src/common/MenuLayout.js index 0038e907..23b881fe 100644 --- a/src/common/MenuLayout.js +++ b/src/common/MenuLayout.js @@ -61,15 +61,17 @@ const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisi
setShowSidebar(false)}> - - - - - - } - /> - +
+ + + + + + } + /> + +
diff --git a/src/common/ShlinkVersions.js b/src/common/ShlinkVersions.js index 37726936..c83e21cb 100644 --- a/src/common/ShlinkVersions.js +++ b/src/common/ShlinkVersions.js @@ -1,14 +1,17 @@ import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; import { serverType } from '../servers/prop-types'; const propTypes = { selectedServer: serverType, + className: PropTypes.string, }; -const ShlinkVersions = ({ selectedServer }) => { - const { version } = selectedServer; +const ShlinkVersions = ({ selectedServer, className }) => { + const { printableVersion } = selectedServer; - return Server: v{version}; + return Client: v2.3.1 / Server: {printableVersion}; }; ShlinkVersions.propTypes = propTypes; diff --git a/src/common/services/provideServices.js b/src/common/services/provideServices.js index fdb25687..d5861a2d 100644 --- a/src/common/services/provideServices.js +++ b/src/common/services/provideServices.js @@ -31,7 +31,7 @@ const provideServices = (bottle, connect, withRouter) => { bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ])); bottle.decorator('MenuLayout', withRouter); - bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton', 'ShlinkVersions'); + bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton'); bottle.serviceFactory('ShlinkVersions', () => ShlinkVersions); bottle.decorator('ShlinkVersions', connect([ 'selectedServer' ])); diff --git a/src/servers/DeleteServerButton.js b/src/servers/DeleteServerButton.js index 8d0caaff..2bc51601 100644 --- a/src/servers/DeleteServerButton.js +++ b/src/servers/DeleteServerButton.js @@ -9,7 +9,7 @@ const propTypes = { className: PropTypes.string, }; -const DeleteServerButton = (DeleteServerModal) => { +const DeleteServerButton = (DeleteServerModal, ShlinkVersions) => { const DeleteServerButtonComp = ({ server, className }) => { const [ isModalOpen, setModalOpen ] = useState(false); @@ -20,6 +20,8 @@ const DeleteServerButton = (DeleteServerModal) => { Remove this server + + setModalOpen(!isModalOpen)} diff --git a/src/servers/prop-types/index.js b/src/servers/prop-types/index.js index 35d43e49..caec6f92 100644 --- a/src/servers/prop-types/index.js +++ b/src/servers/prop-types/index.js @@ -6,4 +6,5 @@ export const serverType = PropTypes.shape({ url: PropTypes.string, apiKey: PropTypes.string, version: PropTypes.string, + printableVersion: PropTypes.string, }); diff --git a/src/servers/reducers/selectedServer.js b/src/servers/reducers/selectedServer.js index 29dfb4e9..095f9342 100644 --- a/src/servers/reducers/selectedServer.js +++ b/src/servers/reducers/selectedServer.js @@ -1,4 +1,5 @@ import { createAction, handleActions } from 'redux-actions'; +import { pipe } from 'ramda'; import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams'; import { versionIsValidSemVer } from '../../utils/utils'; @@ -12,6 +13,11 @@ export const LATEST_VERSION_CONSTRAINT = 'latest'; /* eslint-enable padding-line-between-statements */ const initialState = null; +const versionToSemVer = pipe( + (version) => version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version, + (version) => !versionIsValidSemVer(version) ? MIN_FALLBACK_VERSION : version +); +const versionToPrintable = (version) => !versionIsValidSemVer(version) ? version : `v${version}`; export const resetSelectedServer = createAction(RESET_SELECTED_SERVER); @@ -20,16 +26,14 @@ export const selectServer = ({ findServerById }, buildShlinkApiClient) => (serve const selectedServer = findServerById(serverId); const { health } = buildShlinkApiClient(selectedServer); - const version = await health() - .then(({ version }) => version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version) - .then((version) => !versionIsValidSemVer(version) ? MIN_FALLBACK_VERSION : version) - .catch(() => MIN_FALLBACK_VERSION); + const { version } = await health().catch(() => MIN_FALLBACK_VERSION); dispatch({ type: SELECT_SERVER, selectedServer: { ...selectedServer, - version, + version: versionToSemVer(version), + printableVersion: versionToPrintable(version), }, }); }; diff --git a/src/servers/services/provideServices.js b/src/servers/services/provideServices.js index e4231e87..d29fddc3 100644 --- a/src/servers/services/provideServices.js +++ b/src/servers/services/provideServices.js @@ -24,7 +24,7 @@ const provideServices = (bottle, connect, withRouter) => { bottle.decorator('DeleteServerModal', withRouter); bottle.decorator('DeleteServerModal', connect(null, [ 'deleteServer' ])); - bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal'); + bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal', 'ShlinkVersions'); bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter'); bottle.decorator('ImportServersBtn', connect(null, [ 'createServers' ])); diff --git a/src/short-urls/CreateShortUrl.js b/src/short-urls/CreateShortUrl.js index d1ace683..e8fe9742 100644 --- a/src/short-urls/CreateShortUrl.js +++ b/src/short-urls/CreateShortUrl.js @@ -77,83 +77,81 @@ const CreateShortUrl = ( const disableDomain = isEmpty(currentServerVersion) || compareVersions(currentServerVersion, '<', '1.19.0-beta.1'); return ( -
-
+ +
+ this.setState({ longUrl: e.target.value })} + /> +
+ +
- this.setState({ longUrl: e.target.value })} - /> +
- -
- +
+
+ {renderOptionalInput('customSlug', 'Custom slug')}
- -
-
- {renderOptionalInput('customSlug', 'Custom slug')} -
-
- {renderOptionalInput('domain', 'Domain', 'text', { - disabled: disableDomain, - ...disableDomain && { title: 'Shlink 1.19.0 or higher is required to be able to provide the domain' }, - })} -
+
+ {renderOptionalInput('domain', 'Domain', 'text', { + disabled: disableDomain, + ...disableDomain && { title: 'Shlink 1.19.0 or higher is required to be able to provide the domain' }, + })}
- -
-
- {renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })} -
-
- {renderDateInput('validSince', 'Enabled since...', { maxDate: this.state.validUntil })} -
-
- {renderDateInput('validUntil', 'Enabled until...', { minDate: this.state.validSince })} -
-
- - -
- this.setState({ findIfExists })} - > - Use existing URL if found - - -
-
- - -
- -
- - -
+
+
+ {renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })} +
+
+ {renderDateInput('validSince', 'Enabled since...', { maxDate: this.state.validUntil })} +
+
+ {renderDateInput('validUntil', 'Enabled until...', { minDate: this.state.validSince })} +
+
+ + +
+ this.setState({ findIfExists })} + > + Use existing URL if found + + +
+
+ + +
+ + +
+ + + ); } }; diff --git a/src/short-urls/ShortUrls.js b/src/short-urls/ShortUrls.js index 3bfeec46..3efe9868 100644 --- a/src/short-urls/ShortUrls.js +++ b/src/short-urls/ShortUrls.js @@ -19,11 +19,11 @@ const ShortUrls = (SearchBar, ShortUrlsList) => { const urlsListKey = `${serverId}_${page}`; return ( -
+
-
+ ); }; diff --git a/src/tags/TagsList.js b/src/tags/TagsList.js index e9964c22..9cee6953 100644 --- a/src/tags/TagsList.js +++ b/src/tags/TagsList.js @@ -69,14 +69,14 @@ const TagsList = (TagCard) => class TagsList extends React.Component { const { filterTags } = this.props; return ( -
+ {!this.props.tagsList.loading && }
{this.renderContent()}
-
+ ); } }; diff --git a/src/utils/utils.js b/src/utils/utils.js index 48af3f55..da40702d 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -2,7 +2,7 @@ import L from 'leaflet'; import marker2x from 'leaflet/dist/images/marker-icon-2x.png'; import marker from 'leaflet/dist/images/marker-icon.png'; import markerShadow from 'leaflet/dist/images/marker-shadow.png'; -import { range } from 'ramda'; +import { identity, memoizeWith, range } from 'ramda'; import { useState } from 'react'; import { compare } from 'compare-versions'; @@ -59,13 +59,13 @@ export const compareVersions = (firstVersion, operator, secondVersion) => compar operator ); -export const versionIsValidSemVer = (version) => { +export const versionIsValidSemVer = memoizeWith(identity, (version) => { try { return compareVersions(version, '=', version); } catch (e) { return false; } -}; +}); export const formatDate = (format = 'YYYY-MM-DD') => (date) => date && date.format ? date.format(format) : date; diff --git a/src/visits/ShortUrlVisits.js b/src/visits/ShortUrlVisits.js index e235ffef..a0875b0b 100644 --- a/src/visits/ShortUrlVisits.js +++ b/src/visits/ShortUrlVisits.js @@ -133,7 +133,7 @@ const ShortUrlVisits = ( const setDate = (dateField) => (date) => this.setState({ [dateField]: date }, this.loadVisits); return ( -
+
@@ -148,7 +148,7 @@ const ShortUrlVisits = (
{renderVisitsContent()}
-
+ ); } }; From dbee62ac8c2518d92705aef22b12cb764e7470b9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 5 Mar 2020 11:46:38 +0100 Subject: [PATCH 04/11] Moved shlink versions component to main container --- src/common/MenuLayout.js | 8 ++++++-- src/common/MenuLayout.scss | 23 +++++++++++++++++++++++ src/common/services/provideServices.js | 3 ++- src/index.scss | 8 -------- src/servers/DeleteServerButton.js | 4 +--- src/servers/services/provideServices.js | 2 +- 6 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/common/MenuLayout.js b/src/common/MenuLayout.js index 23b881fe..437f9d02 100644 --- a/src/common/MenuLayout.js +++ b/src/common/MenuLayout.js @@ -17,7 +17,7 @@ const propTypes = { selectedServer: serverType, }; -const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisits) => { +const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisits, ShlinkVersions) => { const MenuLayoutComp = ({ match, location, selectedServer, selectServer }) => { const [ showSideBar, setShowSidebar ] = useState(false); @@ -61,7 +61,7 @@ const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisi
setShowSidebar(false)}> -
+
@@ -72,6 +72,10 @@ const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisi />
+ +
+ +
diff --git a/src/common/MenuLayout.scss b/src/common/MenuLayout.scss index 78276ea9..352bf32d 100644 --- a/src/common/MenuLayout.scss +++ b/src/common/MenuLayout.scss @@ -32,3 +32,26 @@ .menu-layout__burger-icon--active { color: white; } + +$footer-height: 2.3rem; +$footer-margin: .8rem; + +.menu-layout__container { + padding: 20px 0 ($footer-height + $footer-margin); + min-height: 100%; + margin-bottom: -($footer-height + $footer-margin); + + @media (min-width: $mdMin) { + padding: 30px 15px ($footer-height + $footer-margin); + } +} + +.menu-layout__footer { + height: $footer-height; + margin-top: $footer-margin; + padding: 0; + + @media (min-width: $mdMin) { + padding: 0 15px; + } +} diff --git a/src/common/services/provideServices.js b/src/common/services/provideServices.js index d5861a2d..2bde42b0 100644 --- a/src/common/services/provideServices.js +++ b/src/common/services/provideServices.js @@ -26,7 +26,8 @@ const provideServices = (bottle, connect, withRouter) => { 'ShortUrls', 'AsideMenu', 'CreateShortUrl', - 'ShortUrlVisits' + 'ShortUrlVisits', + 'ShlinkVersions' ); bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ])); bottle.decorator('MenuLayout', withRouter); diff --git a/src/index.scss b/src/index.scss index b639ddcd..b2a386ee 100644 --- a/src/index.scss +++ b/src/index.scss @@ -28,14 +28,6 @@ body, color: inherit !important; } -.shlink-container { - padding: 20px 0; - - @media (min-width: $mdMin) { - padding: 30px 15px; - } -} - .badge-main { color: #fff; background-color: $mainColor; diff --git a/src/servers/DeleteServerButton.js b/src/servers/DeleteServerButton.js index 2bc51601..8d0caaff 100644 --- a/src/servers/DeleteServerButton.js +++ b/src/servers/DeleteServerButton.js @@ -9,7 +9,7 @@ const propTypes = { className: PropTypes.string, }; -const DeleteServerButton = (DeleteServerModal, ShlinkVersions) => { +const DeleteServerButton = (DeleteServerModal) => { const DeleteServerButtonComp = ({ server, className }) => { const [ isModalOpen, setModalOpen ] = useState(false); @@ -20,8 +20,6 @@ const DeleteServerButton = (DeleteServerModal, ShlinkVersions) => { Remove this server - - setModalOpen(!isModalOpen)} diff --git a/src/servers/services/provideServices.js b/src/servers/services/provideServices.js index d29fddc3..e4231e87 100644 --- a/src/servers/services/provideServices.js +++ b/src/servers/services/provideServices.js @@ -24,7 +24,7 @@ const provideServices = (bottle, connect, withRouter) => { bottle.decorator('DeleteServerModal', withRouter); bottle.decorator('DeleteServerModal', connect(null, [ 'deleteServer' ])); - bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal', 'ShlinkVersions'); + bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal'); bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter'); bottle.decorator('ImportServersBtn', connect(null, [ 'createServers' ])); From c181831a37079939a5d1cfc4f2650eafe82b5779 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 5 Mar 2020 11:58:35 +0100 Subject: [PATCH 05/11] Fixed tests --- test/common/AsideMenu.test.js | 3 +-- test/servers/DeleteServerButton.test.js | 12 ++---------- test/servers/DeleteServerModal.test.js | 2 +- test/servers/reducers/selectedServer.test.js | 9 +++++---- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/test/common/AsideMenu.test.js b/test/common/AsideMenu.test.js index 4541b00d..b86b5650 100644 --- a/test/common/AsideMenu.test.js +++ b/test/common/AsideMenu.test.js @@ -1,6 +1,5 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { NavLink } from 'react-router-dom'; import asideMenuCreator from '../../src/common/AsideMenu'; describe('', () => { @@ -15,7 +14,7 @@ describe('', () => { afterEach(() => wrapped.unmount()); it('contains links to different sections', () => { - const links = wrapped.find(NavLink); + const links = wrapped.find('[to]'); expect(links).toHaveLength(3); links.forEach((link) => expect(link.prop('to')).toContain('abc123')); diff --git a/test/servers/DeleteServerButton.test.js b/test/servers/DeleteServerButton.test.js index fbe11b09..a377cf83 100644 --- a/test/servers/DeleteServerButton.test.js +++ b/test/servers/DeleteServerButton.test.js @@ -21,16 +21,8 @@ describe('', () => { it('displays modal when button is clicked', () => { const btn = wrapper.find('.button'); - expect(wrapper.state('isModalOpen')).toEqual(false); + expect(wrapper.find(DeleteServerModal).prop('isOpen')).toEqual(false); btn.simulate('click'); - expect(wrapper.state('isModalOpen')).toEqual(true); - }); - - it('changes modal open state when toggled', () => { - const modal = wrapper.find(DeleteServerModal); - - expect(wrapper.state('isModalOpen')).toEqual(false); - modal.prop('toggle')(); - expect(wrapper.state('isModalOpen')).toEqual(true); + expect(wrapper.find(DeleteServerModal).prop('isOpen')).toEqual(true); }); }); diff --git a/test/servers/DeleteServerModal.test.js b/test/servers/DeleteServerModal.test.js index 21f938c7..6aca966b 100644 --- a/test/servers/DeleteServerModal.test.js +++ b/test/servers/DeleteServerModal.test.js @@ -38,7 +38,7 @@ describe('', () => { const modalBody = wrapper.find(ModalBody); expect(modalBody.find('p').first().text()).toEqual( - `Are you sure you want to delete server ${serverName}?` + `Are you sure you want to remove ${serverName}?` ); }); diff --git a/test/servers/reducers/selectedServer.test.js b/test/servers/reducers/selectedServer.test.js index 4d20fb5c..33728d9f 100644 --- a/test/servers/reducers/selectedServer.test.js +++ b/test/servers/reducers/selectedServer.test.js @@ -44,13 +44,14 @@ describe('selectedServerReducer', () => { afterEach(jest.clearAllMocks); it.each([ - [ version, version ], - [ 'latest', MAX_FALLBACK_VERSION ], - [ '%invalid_semver%', MIN_FALLBACK_VERSION ], - ])('dispatches proper actions', async (serverVersion, expectedVersion) => { + [ version, version, `v${version}` ], + [ 'latest', MAX_FALLBACK_VERSION, 'latest' ], + [ '%invalid_semver%', MIN_FALLBACK_VERSION, '%invalid_semver%' ], + ])('dispatches proper actions', async (serverVersion, expectedVersion, expectedPrintableVersion) => { const expectedSelectedServer = { ...selectedServer, version: expectedVersion, + printableVersion: expectedPrintableVersion, }; apiClientMock.health.mockResolvedValue({ version: serverVersion }); From be50b24504960ec07fa9885061a95cf0499da33c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 5 Mar 2020 12:53:32 +0100 Subject: [PATCH 06/11] Added mechanism to provide a version to shlink-web-client --- src/common/ShlinkVersions.js | 16 +++++++++++++--- src/servers/helpers/ForServerVersion.js | 2 +- src/servers/reducers/selectedServer.js | 5 ++--- src/short-urls/CreateShortUrl.js | 2 +- src/utils/utils.js | 17 +---------------- src/utils/versionHelpers.js | 21 +++++++++++++++++++++ 6 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 src/utils/versionHelpers.js diff --git a/src/common/ShlinkVersions.js b/src/common/ShlinkVersions.js index c83e21cb..31cf5b02 100644 --- a/src/common/ShlinkVersions.js +++ b/src/common/ShlinkVersions.js @@ -1,17 +1,27 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { pipe } from 'ramda'; import { serverType } from '../servers/prop-types'; +import { versionToPrintable, versionToSemVer } from '../utils/versionHelpers'; + +const SHLINK_WEB_CLIENT_VERSION = '%_VERSION_%'; const propTypes = { selectedServer: serverType, className: PropTypes.string, + clientVersion: PropTypes.string, }; -const ShlinkVersions = ({ selectedServer, className }) => { - const { printableVersion } = selectedServer; +const ShlinkVersions = ({ selectedServer, className, clientVersion = SHLINK_WEB_CLIENT_VERSION }) => { + const { printableVersion: serverVersion } = selectedServer; + const normalizedClientVersion = pipe(versionToSemVer(), versionToPrintable)(clientVersion); - return Client: v2.3.1 / Server: {printableVersion}; + return ( + + Client: {normalizedClientVersion} - Server: {serverVersion} + + ); }; ShlinkVersions.propTypes = propTypes; diff --git a/src/servers/helpers/ForServerVersion.js b/src/servers/helpers/ForServerVersion.js index e89eacfc..0d753f30 100644 --- a/src/servers/helpers/ForServerVersion.js +++ b/src/servers/helpers/ForServerVersion.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { compareVersions } from '../../utils/utils'; import { serverType } from '../prop-types'; +import { compareVersions } from '../../utils/versionHelpers'; const propTypes = { minVersion: PropTypes.string, diff --git a/src/servers/reducers/selectedServer.js b/src/servers/reducers/selectedServer.js index 095f9342..861fba85 100644 --- a/src/servers/reducers/selectedServer.js +++ b/src/servers/reducers/selectedServer.js @@ -1,7 +1,7 @@ import { createAction, handleActions } from 'redux-actions'; import { pipe } from 'ramda'; import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams'; -import { versionIsValidSemVer } from '../../utils/utils'; +import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/versionHelpers'; /* eslint-disable padding-line-between-statements */ export const SELECT_SERVER = 'shlink/selectedServer/SELECT_SERVER'; @@ -15,9 +15,8 @@ export const LATEST_VERSION_CONSTRAINT = 'latest'; const initialState = null; const versionToSemVer = pipe( (version) => version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version, - (version) => !versionIsValidSemVer(version) ? MIN_FALLBACK_VERSION : version + toSemVer(MIN_FALLBACK_VERSION) ); -const versionToPrintable = (version) => !versionIsValidSemVer(version) ? version : `v${version}`; export const resetSelectedServer = createAction(RESET_SELECTED_SERVER); diff --git a/src/short-urls/CreateShortUrl.js b/src/short-urls/CreateShortUrl.js index e8fe9742..39d02506 100644 --- a/src/short-urls/CreateShortUrl.js +++ b/src/short-urls/CreateShortUrl.js @@ -7,7 +7,7 @@ import * as PropTypes from 'prop-types'; import DateInput from '../utils/DateInput'; import Checkbox from '../utils/Checkbox'; import { serverType } from '../servers/prop-types'; -import { compareVersions } from '../utils/utils'; +import { compareVersions } from '../utils/versionHelpers'; import { createShortUrlResultType } from './reducers/shortUrlCreation'; import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon'; diff --git a/src/utils/utils.js b/src/utils/utils.js index da40702d..d3118236 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -2,9 +2,8 @@ import L from 'leaflet'; import marker2x from 'leaflet/dist/images/marker-icon-2x.png'; import marker from 'leaflet/dist/images/marker-icon.png'; import markerShadow from 'leaflet/dist/images/marker-shadow.png'; -import { identity, memoizeWith, range } from 'ramda'; +import { range } from 'ramda'; import { useState } from 'react'; -import { compare } from 'compare-versions'; const TEN_ROUNDING_NUMBER = 10; const DEFAULT_TIMEOUT_DELAY = 2000; @@ -53,20 +52,6 @@ export const useToggle = (initialValue = false) => { return [ flag, () => setFlag(!flag) ]; }; -export const compareVersions = (firstVersion, operator, secondVersion) => compare( - firstVersion, - secondVersion, - operator -); - -export const versionIsValidSemVer = memoizeWith(identity, (version) => { - try { - return compareVersions(version, '=', version); - } catch (e) { - return false; - } -}); - export const formatDate = (format = 'YYYY-MM-DD') => (date) => date && date.format ? date.format(format) : date; export const formatIsoDate = (date) => date && date.format ? date.format() : date; diff --git a/src/utils/versionHelpers.js b/src/utils/versionHelpers.js new file mode 100644 index 00000000..92867939 --- /dev/null +++ b/src/utils/versionHelpers.js @@ -0,0 +1,21 @@ +import { compare } from 'compare-versions'; +import { identity, memoizeWith } from 'ramda'; + +export const compareVersions = (firstVersion, operator, secondVersion) => compare( + firstVersion, + secondVersion, + operator, +); + +const versionIsValidSemVer = memoizeWith(identity, (version) => { + try { + return compareVersions(version, '=', version); + } catch (e) { + return false; + } +}); + +export const versionToPrintable = (version) => !versionIsValidSemVer(version) ? version : `v${version}`; + +export const versionToSemVer = (defaultValue = 'latest') => + (version) => versionIsValidSemVer(version) ? version : defaultValue; From f59e569e227158d4df7a7ce4576c73b27979c297 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 5 Mar 2020 13:04:12 +0100 Subject: [PATCH 07/11] Extracted logic to determine app version from function to generate dist file --- scripts/build.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/scripts/build.js b/scripts/build.js index 8c0a933e..02ea01cc 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -47,6 +47,7 @@ if (!checkRequiredFiles([ paths.appHtml, paths.appIndexJs ])) { const argvSliceStart = 2; const argv = process.argv.slice(argvSliceStart); const writeStatsJson = argv.indexOf('--stats') !== -1; +const { version, hasVersion } = getVersionFromArgs(argv); // Generate configuration const config = configFactory('production'); @@ -117,7 +118,7 @@ checkBrowsers(paths.appPath, isInteractive) process.exit(1); } ) - .then(zipDist) + .then(() => hasVersion && zipDist(version)) .catch((err) => { if (err && err.message) { console.log(err.message); @@ -200,15 +201,7 @@ function copyPublicFolder() { }); } -function zipDist() { - const minArgsToContainVersion = 3; - - // If no version was provided, do nothing - if (process.argv.length < minArgsToContainVersion) { - return; - } - - const [ , , version ] = process.argv; +function zipDist(version) { const versionFileName = `./dist/shlink-web-client_${version}_dist.zip`; console.log(chalk.cyan(`Generating dist file for version ${chalk.bold(version)}...`)); @@ -227,3 +220,9 @@ function zipDist() { console.log(e); } } + +function getVersionFromArgs(argv) { + const [ version ] = argv; + + return { version, hasVersion: !!version }; +} From 8e1c6908c65228838c482fef3845015c461aa140 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 5 Mar 2020 13:27:57 +0100 Subject: [PATCH 08/11] Updated build script so that it replaces version placeholder when a version is provided --- scripts/build.js | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/scripts/build.js b/scripts/build.js index 02ea01cc..2178b7b5 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -14,7 +14,6 @@ process.on('unhandledRejection', (err) => { // Ensure environment variables are read. require('../config/env'); -const path = require('path'); const chalk = require('chalk'); const fs = require('fs-extra'); const webpack = require('webpack'); @@ -22,7 +21,6 @@ const bfj = require('bfj'); const AdmZip = require('adm-zip'); const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); -const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); const printBuildError = require('react-dev-utils/printBuildError'); const { checkBrowsers } = require('react-dev-utils/browsersHelper'); @@ -30,7 +28,6 @@ const paths = require('../config/paths'); const configFactory = require('../config/webpack.config'); const { measureFileSizesBeforeBuild, printFileSizesAfterBuild } = FileSizeReporter; -const useYarn = fs.existsSync(paths.yarnLockFile); // These sizes are pretty large. We'll warn for bundles exceeding them. const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; // eslint-disable-line @@ -86,6 +83,7 @@ checkBrowsers(paths.appPath, isInteractive) ); } else { console.log(chalk.green('Compiled successfully.\n')); + hasVersion && replaceVersionPlaceholder(version); } console.log('File sizes after gzip:\n'); @@ -97,20 +95,6 @@ checkBrowsers(paths.appPath, isInteractive) WARN_AFTER_CHUNK_GZIP_SIZE ); console.log(); - - const appPackage = require(paths.appPackageJson); - - const { publicUrl } = paths; - const { output: { publicPath } } = config; - const buildFolder = path.relative(process.cwd(), paths.appBuild); - - printHostingInstructions( - appPackage, - publicUrl, - publicPath, - buildFolder, - useYarn - ); }, (err) => { console.log(chalk.red('Failed to compile.\n')); @@ -219,6 +203,7 @@ function zipDist(version) { console.log(chalk.red('An error occurred while generating dist file')); console.log(e); } + console.log(); } function getVersionFromArgs(argv) { @@ -226,3 +211,16 @@ function getVersionFromArgs(argv) { return { version, hasVersion: !!version }; } + +function replaceVersionPlaceholder(version) { + const staticJsFilesPath = './build/static/js'; + const versionPlaceholder = '%_VERSION_%'; + + const isMainFile = (file) => file.startsWith('main.') && file.endsWith('.js'); + const [ mainJsFile ] = fs.readdirSync(staticJsFilesPath).filter(isMainFile); + const filePath = `${staticJsFilesPath}/${mainJsFile}`; + const fileContent = fs.readFileSync(filePath, 'utf-8'); + const replaced = fileContent.replace(versionPlaceholder, version); + + fs.writeFileSync(filePath, replaced, 'utf-8'); +} From 4a6dd66ecd733c56e92db461d7871f5e1e47e198 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 5 Mar 2020 13:37:07 +0100 Subject: [PATCH 09/11] Added scripts to pass version when building docker image --- Dockerfile | 4 +++- hooks/build | 10 ++++++++++ scripts/build.js | 3 ++- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100755 hooks/build diff --git a/Dockerfile b/Dockerfile index 02684ce2..39e2368d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,8 @@ FROM node:12.14.1-alpine as node COPY . /shlink-web-client -RUN cd /shlink-web-client && npm install && npm run build +ARG VERSION="latest" +ENV VERSION ${VERSION} +RUN cd /shlink-web-client && npm install && npm run build -- ${VERSION} --no-dist FROM nginx:1.17.7-alpine LABEL maintainer="Alejandro Celaya " diff --git a/hooks/build b/hooks/build new file mode 100755 index 00000000..f912acdd --- /dev/null +++ b/hooks/build @@ -0,0 +1,10 @@ +#!/bin/bash +set -ex + +if [[ ${SOURCE_BRANCH} == 'master' ]]; then + SHLINK_WEB_CLIENT_RELEASE='latest' +else + SHLINK_WEB_CLIENT_RELEASE=${SOURCE_BRANCH#?} +fi + +docker build --build-arg VERSION=${SHLINK_WEB_CLIENT_RELEASE} -t ${IMAGE_NAME} . diff --git a/scripts/build.js b/scripts/build.js index 2178b7b5..f76af2d4 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -44,6 +44,7 @@ if (!checkRequiredFiles([ paths.appHtml, paths.appIndexJs ])) { const argvSliceStart = 2; const argv = process.argv.slice(argvSliceStart); const writeStatsJson = argv.indexOf('--stats') !== -1; +const withoutDist = argv.indexOf('--no-dist') !== -1; const { version, hasVersion } = getVersionFromArgs(argv); // Generate configuration @@ -102,7 +103,7 @@ checkBrowsers(paths.appPath, isInteractive) process.exit(1); } ) - .then(() => hasVersion && zipDist(version)) + .then(() => hasVersion && !withoutDist && zipDist(version)) .catch((err) => { if (err && err.message) { console.log(err.message); From e761f5e1bd1e6dfad2ecc923596ae48fa0914b31 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 5 Mar 2020 13:45:24 +0100 Subject: [PATCH 10/11] Updated changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bd8c4b1..d51bf867 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), #### Added -* *Nothing* +* [#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. #### Changed From 73e3f42614ea34aaa88a136a05468219bb6bdc48 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 5 Mar 2020 13:55:39 +0100 Subject: [PATCH 11/11] Added ShlinkVersions test --- test/common/ShlinkVersions.test.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 test/common/ShlinkVersions.test.js diff --git a/test/common/ShlinkVersions.test.js b/test/common/ShlinkVersions.test.js new file mode 100644 index 00000000..685158b0 --- /dev/null +++ b/test/common/ShlinkVersions.test.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import ShlinkVersions from '../../src/common/ShlinkVersions'; + +describe('', () => { + let wrapper; + const createWrapper = (props) => { + wrapper = shallow(); + + return wrapper; + }; + + afterEach(() => wrapper && wrapper.unmount()); + + it.each([ + [ '1.2.3', 'foo', 'Client: v1.2.3 - Server: foo' ], + [ 'foo', '1.2.3', 'Client: latest - Server: 1.2.3' ], + [ 'latest', 'latest', 'Client: latest - Server: latest' ], + [ '5.5.0', '0.2.8', 'Client: v5.5.0 - Server: 0.2.8' ], + [ 'not-semver', 'something', 'Client: latest - Server: something' ], + ])('displays expected versions', (clientVersion, printableVersion, expected) => { + const wrapper = createWrapper({ clientVersion, selectedServer: { printableVersion } }); + + expect(wrapper.text()).toEqual(expected); + }); +});