mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Added shlink versions to side menu
This commit is contained in:
parent
b02dcf6c53
commit
1e949b3a22
13 changed files with 110 additions and 101 deletions
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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' ]));
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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' ]));
|
||||||
|
|
|
@ -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} />
|
|
||||||
|
|
||||||
{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} />
|
||||||
|
|
||||||
|
{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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue