Added shlink versions to side menu

This commit is contained in:
Alejandro Celaya 2020-03-05 11:11:26 +01:00
parent b02dcf6c53
commit 1e949b3a22
13 changed files with 110 additions and 101 deletions

View file

@ -24,7 +24,7 @@ const propTypes = {
showOnMobile: PropTypes.bool, showOnMobile: PropTypes.bool,
}; };
const AsideMenu = (DeleteServerButton, ShlinkVersions) => { const AsideMenu = (DeleteServerButton) => {
const AsideMenu = ({ selectedServer, className, showOnMobile }) => { const AsideMenu = ({ selectedServer, className, showOnMobile }) => {
const serverId = selectedServer ? selectedServer.id : ''; const serverId = selectedServer ? selectedServer.id : '';
const asideClass = classNames('aside-menu', className, { const asideClass = classNames('aside-menu', className, {
@ -49,7 +49,6 @@ const AsideMenu = (DeleteServerButton, ShlinkVersions) => {
<span className="aside-menu__item-text">Manage tags</span> <span className="aside-menu__item-text">Manage tags</span>
</AsideMenuItem> </AsideMenuItem>
<ShlinkVersions />
<DeleteServerButton className="aside-menu__item aside-menu__item--danger" server={selectedServer} /> <DeleteServerButton className="aside-menu__item aside-menu__item--danger" server={selectedServer} />
</nav> </nav>
</aside> </aside>

View file

@ -61,15 +61,17 @@ const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisi
<div className="row menu-layout__swipeable-inner"> <div className="row menu-layout__swipeable-inner">
<AsideMenu className="col-lg-2 col-md-3" selectedServer={selectedServer} showOnMobile={showSideBar} /> <AsideMenu className="col-lg-2 col-md-3" selectedServer={selectedServer} showOnMobile={showSideBar} />
<div className="col-lg-10 offset-lg-2 col-md-9 offset-md-3" onClick={() => setShowSidebar(false)}> <div className="col-lg-10 offset-lg-2 col-md-9 offset-md-3" onClick={() => setShowSidebar(false)}>
<Switch> <div className="shlink-container">
<Route exact path="/server/:serverId/list-short-urls/:page" component={ShortUrls} /> <Switch>
<Route exact path="/server/:serverId/create-short-url" component={CreateShortUrl} /> <Route exact path="/server/:serverId/list-short-urls/:page" component={ShortUrls} />
<Route exact path="/server/:serverId/short-code/:shortCode/visits" component={ShortUrlVisits} /> <Route exact path="/server/:serverId/create-short-url" component={CreateShortUrl} />
<Route exact path="/server/:serverId/manage-tags" component={TagsList} /> <Route exact path="/server/:serverId/short-code/:shortCode/visits" component={ShortUrlVisits} />
<Route <Route exact path="/server/:serverId/manage-tags" component={TagsList} />
render={() => <NotFound to={`/server/${serverId}/list-short-urls/1`} btnText="List short URLs" />} <Route
/> render={() => <NotFound to={`/server/${serverId}/list-short-urls/1`} btnText="List short URLs" />}
</Switch> />
</Switch>
</div>
</div> </div>
</div> </div>
</Swipeable> </Swipeable>

View file

@ -1,14 +1,17 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { serverType } from '../servers/prop-types'; import { serverType } from '../servers/prop-types';
const propTypes = { const propTypes = {
selectedServer: serverType, selectedServer: serverType,
className: PropTypes.string,
}; };
const ShlinkVersions = ({ selectedServer }) => { const ShlinkVersions = ({ selectedServer, className }) => {
const { version } = selectedServer; const { printableVersion } = selectedServer;
return <span>Server: v{version}</span>; return <small className={classNames('text-muted', className)}>Client: v2.3.1 / Server: {printableVersion}</small>;
}; };
ShlinkVersions.propTypes = propTypes; ShlinkVersions.propTypes = propTypes;

View file

@ -31,7 +31,7 @@ const provideServices = (bottle, connect, withRouter) => {
bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ])); bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ]));
bottle.decorator('MenuLayout', withRouter); bottle.decorator('MenuLayout', withRouter);
bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton', 'ShlinkVersions'); bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton');
bottle.serviceFactory('ShlinkVersions', () => ShlinkVersions); bottle.serviceFactory('ShlinkVersions', () => ShlinkVersions);
bottle.decorator('ShlinkVersions', connect([ 'selectedServer' ])); bottle.decorator('ShlinkVersions', connect([ 'selectedServer' ]));

View file

@ -9,7 +9,7 @@ const propTypes = {
className: PropTypes.string, className: PropTypes.string,
}; };
const DeleteServerButton = (DeleteServerModal) => { const DeleteServerButton = (DeleteServerModal, ShlinkVersions) => {
const DeleteServerButtonComp = ({ server, className }) => { const DeleteServerButtonComp = ({ server, className }) => {
const [ isModalOpen, setModalOpen ] = useState(false); const [ isModalOpen, setModalOpen ] = useState(false);
@ -20,6 +20,8 @@ const DeleteServerButton = (DeleteServerModal) => {
<span className="aside-menu__item-text">Remove this server</span> <span className="aside-menu__item-text">Remove this server</span>
</span> </span>
<ShlinkVersions className="mt-2 pl-2" />
<DeleteServerModal <DeleteServerModal
isOpen={isModalOpen} isOpen={isModalOpen}
toggle={() => setModalOpen(!isModalOpen)} toggle={() => setModalOpen(!isModalOpen)}

View file

@ -6,4 +6,5 @@ export const serverType = PropTypes.shape({
url: PropTypes.string, url: PropTypes.string,
apiKey: PropTypes.string, apiKey: PropTypes.string,
version: PropTypes.string, version: PropTypes.string,
printableVersion: PropTypes.string,
}); });

View file

@ -1,4 +1,5 @@
import { createAction, handleActions } from 'redux-actions'; import { createAction, handleActions } from 'redux-actions';
import { pipe } from 'ramda';
import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams'; import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams';
import { versionIsValidSemVer } from '../../utils/utils'; import { versionIsValidSemVer } from '../../utils/utils';
@ -12,6 +13,11 @@ export const LATEST_VERSION_CONSTRAINT = 'latest';
/* eslint-enable padding-line-between-statements */ /* eslint-enable padding-line-between-statements */
const initialState = null; 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); export const resetSelectedServer = createAction(RESET_SELECTED_SERVER);
@ -20,16 +26,14 @@ export const selectServer = ({ findServerById }, buildShlinkApiClient) => (serve
const selectedServer = findServerById(serverId); const selectedServer = findServerById(serverId);
const { health } = buildShlinkApiClient(selectedServer); const { health } = buildShlinkApiClient(selectedServer);
const version = await health() const { version } = await health().catch(() => MIN_FALLBACK_VERSION);
.then(({ version }) => version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version)
.then((version) => !versionIsValidSemVer(version) ? MIN_FALLBACK_VERSION : version)
.catch(() => MIN_FALLBACK_VERSION);
dispatch({ dispatch({
type: SELECT_SERVER, type: SELECT_SERVER,
selectedServer: { selectedServer: {
...selectedServer, ...selectedServer,
version, version: versionToSemVer(version),
printableVersion: versionToPrintable(version),
}, },
}); });
}; };

View file

@ -24,7 +24,7 @@ const provideServices = (bottle, connect, withRouter) => {
bottle.decorator('DeleteServerModal', withRouter); bottle.decorator('DeleteServerModal', withRouter);
bottle.decorator('DeleteServerModal', connect(null, [ 'deleteServer' ])); bottle.decorator('DeleteServerModal', connect(null, [ 'deleteServer' ]));
bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal'); bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal', 'ShlinkVersions');
bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter'); bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter');
bottle.decorator('ImportServersBtn', connect(null, [ 'createServers' ])); bottle.decorator('ImportServersBtn', connect(null, [ 'createServers' ]));

View file

@ -77,83 +77,81 @@ const CreateShortUrl = (
const disableDomain = isEmpty(currentServerVersion) || compareVersions(currentServerVersion, '<', '1.19.0-beta.1'); const disableDomain = isEmpty(currentServerVersion) || compareVersions(currentServerVersion, '<', '1.19.0-beta.1');
return ( return (
<div className="shlink-container"> <form onSubmit={save}>
<form onSubmit={save}> <div className="form-group">
<input
className="form-control form-control-lg"
type="url"
placeholder="Insert the URL to be shortened"
required
value={this.state.longUrl}
onChange={(e) => this.setState({ longUrl: e.target.value })}
/>
</div>
<Collapse isOpen={this.state.moreOptionsVisible}>
<div className="form-group"> <div className="form-group">
<input <TagsSelector tags={this.state.tags} onChange={changeTags} />
className="form-control form-control-lg"
type="url"
placeholder="Insert the URL to be shortened"
required
value={this.state.longUrl}
onChange={(e) => this.setState({ longUrl: e.target.value })}
/>
</div> </div>
<Collapse isOpen={this.state.moreOptionsVisible}> <div className="row">
<div className="form-group"> <div className="col-sm-6">
<TagsSelector tags={this.state.tags} onChange={changeTags} /> {renderOptionalInput('customSlug', 'Custom slug')}
</div> </div>
<div className="col-sm-6">
<div className="row"> {renderOptionalInput('domain', 'Domain', 'text', {
<div className="col-sm-6"> disabled: disableDomain,
{renderOptionalInput('customSlug', 'Custom slug')} ...disableDomain && { title: 'Shlink 1.19.0 or higher is required to be able to provide the domain' },
</div> })}
<div className="col-sm-6">
{renderOptionalInput('domain', 'Domain', 'text', {
disabled: disableDomain,
...disableDomain && { title: 'Shlink 1.19.0 or higher is required to be able to provide the domain' },
})}
</div>
</div> </div>
<div className="row">
<div className="col-sm-6">
{renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
</div>
<div className="col-sm-3">
{renderDateInput('validSince', 'Enabled since...', { maxDate: this.state.validUntil })}
</div>
<div className="col-sm-3">
{renderDateInput('validUntil', 'Enabled until...', { minDate: this.state.validSince })}
</div>
</div>
<ForServerVersion minVersion="1.16.0">
<div className="mb-4 text-right">
<Checkbox
className="mr-2"
checked={this.state.findIfExists}
onChange={(findIfExists) => this.setState({ findIfExists })}
>
Use existing URL if found
</Checkbox>
<UseExistingIfFoundInfoIcon />
</div>
</ForServerVersion>
</Collapse>
<div>
<button
type="button"
className="btn btn-outline-secondary"
onClick={() => this.setState(({ moreOptionsVisible }) => ({ moreOptionsVisible: !moreOptionsVisible }))}
>
<FontAwesomeIcon icon={this.state.moreOptionsVisible ? upIcon : downIcon} />
&nbsp;
{this.state.moreOptionsVisible ? 'Less' : 'More'} options
</button>
<button
className="btn btn-outline-primary float-right"
disabled={shortUrlCreationResult.loading || isEmpty(this.state.longUrl)}
>
{shortUrlCreationResult.loading ? 'Creating...' : 'Create'}
</button>
</div> </div>
<CreateShortUrlResult {...shortUrlCreationResult} resetCreateShortUrl={resetCreateShortUrl} /> <div className="row">
</form> <div className="col-sm-6">
</div> {renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
</div>
<div className="col-sm-3">
{renderDateInput('validSince', 'Enabled since...', { maxDate: this.state.validUntil })}
</div>
<div className="col-sm-3">
{renderDateInput('validUntil', 'Enabled until...', { minDate: this.state.validSince })}
</div>
</div>
<ForServerVersion minVersion="1.16.0">
<div className="mb-4 text-right">
<Checkbox
className="mr-2"
checked={this.state.findIfExists}
onChange={(findIfExists) => this.setState({ findIfExists })}
>
Use existing URL if found
</Checkbox>
<UseExistingIfFoundInfoIcon />
</div>
</ForServerVersion>
</Collapse>
<div>
<button
type="button"
className="btn btn-outline-secondary"
onClick={() => this.setState(({ moreOptionsVisible }) => ({ moreOptionsVisible: !moreOptionsVisible }))}
>
<FontAwesomeIcon icon={this.state.moreOptionsVisible ? upIcon : downIcon} />
&nbsp;
{this.state.moreOptionsVisible ? 'Less' : 'More'} options
</button>
<button
className="btn btn-outline-primary float-right"
disabled={shortUrlCreationResult.loading || isEmpty(this.state.longUrl)}
>
{shortUrlCreationResult.loading ? 'Creating...' : 'Create'}
</button>
</div>
<CreateShortUrlResult {...shortUrlCreationResult} resetCreateShortUrl={resetCreateShortUrl} />
</form>
); );
} }
}; };

View file

@ -19,11 +19,11 @@ const ShortUrls = (SearchBar, ShortUrlsList) => {
const urlsListKey = `${serverId}_${page}`; const urlsListKey = `${serverId}_${page}`;
return ( return (
<div className="shlink-container"> <React.Fragment>
<div className="form-group"><SearchBar /></div> <div className="form-group"><SearchBar /></div>
<ShortUrlsList {...props} shortUrlsList={data} key={urlsListKey} /> <ShortUrlsList {...props} shortUrlsList={data} key={urlsListKey} />
<Paginator paginator={pagination} serverId={serverId} /> <Paginator paginator={pagination} serverId={serverId} />
</div> </React.Fragment>
); );
}; };

View file

@ -69,14 +69,14 @@ const TagsList = (TagCard) => class TagsList extends React.Component {
const { filterTags } = this.props; const { filterTags } = this.props;
return ( return (
<div className="shlink-container"> <React.Fragment>
{!this.props.tagsList.loading && {!this.props.tagsList.loading &&
<SearchField className="mb-3" placeholder="Search tags..." onChange={filterTags} /> <SearchField className="mb-3" placeholder="Search tags..." onChange={filterTags} />
} }
<div className="row"> <div className="row">
{this.renderContent()} {this.renderContent()}
</div> </div>
</div> </React.Fragment>
); );
} }
}; };

View file

@ -2,7 +2,7 @@ import L from 'leaflet';
import marker2x from 'leaflet/dist/images/marker-icon-2x.png'; import marker2x from 'leaflet/dist/images/marker-icon-2x.png';
import marker from 'leaflet/dist/images/marker-icon.png'; import marker from 'leaflet/dist/images/marker-icon.png';
import markerShadow from 'leaflet/dist/images/marker-shadow.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 { useState } from 'react';
import { compare } from 'compare-versions'; import { compare } from 'compare-versions';
@ -59,13 +59,13 @@ export const compareVersions = (firstVersion, operator, secondVersion) => compar
operator operator
); );
export const versionIsValidSemVer = (version) => { export const versionIsValidSemVer = memoizeWith(identity, (version) => {
try { try {
return compareVersions(version, '=', version); return compareVersions(version, '=', version);
} catch (e) { } catch (e) {
return false; return false;
} }
}; });
export const formatDate = (format = 'YYYY-MM-DD') => (date) => date && date.format ? date.format(format) : date; export const formatDate = (format = 'YYYY-MM-DD') => (date) => date && date.format ? date.format(format) : date;

View file

@ -133,7 +133,7 @@ const ShortUrlVisits = (
const setDate = (dateField) => (date) => this.setState({ [dateField]: date }, this.loadVisits); const setDate = (dateField) => (date) => this.setState({ [dateField]: date }, this.loadVisits);
return ( return (
<div className="shlink-container"> <React.Fragment>
<VisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} /> <VisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} />
<section className="mt-4"> <section className="mt-4">
@ -148,7 +148,7 @@ const ShortUrlVisits = (
<section> <section>
{renderVisitsContent()} {renderVisitsContent()}
</section> </section>
</div> </React.Fragment>
); );
} }
}; };