mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-12 02:23:49 +00:00
Merge pull request #215 from acelaya-forks/feature/versions
Feature/versions
This commit is contained in:
@@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
|
|
||||||
#### Added
|
#### 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
|
#### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
FROM node:12.14.1-alpine as node
|
FROM node:12.14.1-alpine as node
|
||||||
COPY . /shlink-web-client
|
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
|
FROM nginx:1.17.7-alpine
|
||||||
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
||||||
|
|||||||
10
hooks/build
Executable file
10
hooks/build
Executable file
@@ -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} .
|
||||||
@@ -14,7 +14,6 @@ process.on('unhandledRejection', (err) => {
|
|||||||
// Ensure environment variables are read.
|
// Ensure environment variables are read.
|
||||||
require('../config/env');
|
require('../config/env');
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
@@ -22,7 +21,6 @@ const bfj = require('bfj');
|
|||||||
const AdmZip = require('adm-zip');
|
const AdmZip = require('adm-zip');
|
||||||
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||||
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
|
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
|
||||||
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
|
|
||||||
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
|
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
|
||||||
const printBuildError = require('react-dev-utils/printBuildError');
|
const printBuildError = require('react-dev-utils/printBuildError');
|
||||||
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||||
@@ -30,7 +28,6 @@ const paths = require('../config/paths');
|
|||||||
const configFactory = require('../config/webpack.config');
|
const configFactory = require('../config/webpack.config');
|
||||||
|
|
||||||
const { measureFileSizesBeforeBuild, printFileSizesAfterBuild } = FileSizeReporter;
|
const { measureFileSizesBeforeBuild, printFileSizesAfterBuild } = FileSizeReporter;
|
||||||
const useYarn = fs.existsSync(paths.yarnLockFile);
|
|
||||||
|
|
||||||
// These sizes are pretty large. We'll warn for bundles exceeding them.
|
// These sizes are pretty large. We'll warn for bundles exceeding them.
|
||||||
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; // eslint-disable-line
|
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; // eslint-disable-line
|
||||||
@@ -47,6 +44,8 @@ if (!checkRequiredFiles([ paths.appHtml, paths.appIndexJs ])) {
|
|||||||
const argvSliceStart = 2;
|
const argvSliceStart = 2;
|
||||||
const argv = process.argv.slice(argvSliceStart);
|
const argv = process.argv.slice(argvSliceStart);
|
||||||
const writeStatsJson = argv.indexOf('--stats') !== -1;
|
const writeStatsJson = argv.indexOf('--stats') !== -1;
|
||||||
|
const withoutDist = argv.indexOf('--no-dist') !== -1;
|
||||||
|
const { version, hasVersion } = getVersionFromArgs(argv);
|
||||||
|
|
||||||
// Generate configuration
|
// Generate configuration
|
||||||
const config = configFactory('production');
|
const config = configFactory('production');
|
||||||
@@ -85,6 +84,7 @@ checkBrowsers(paths.appPath, isInteractive)
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log(chalk.green('Compiled successfully.\n'));
|
console.log(chalk.green('Compiled successfully.\n'));
|
||||||
|
hasVersion && replaceVersionPlaceholder(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('File sizes after gzip:\n');
|
console.log('File sizes after gzip:\n');
|
||||||
@@ -96,20 +96,6 @@ checkBrowsers(paths.appPath, isInteractive)
|
|||||||
WARN_AFTER_CHUNK_GZIP_SIZE
|
WARN_AFTER_CHUNK_GZIP_SIZE
|
||||||
);
|
);
|
||||||
console.log();
|
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) => {
|
(err) => {
|
||||||
console.log(chalk.red('Failed to compile.\n'));
|
console.log(chalk.red('Failed to compile.\n'));
|
||||||
@@ -117,7 +103,7 @@ checkBrowsers(paths.appPath, isInteractive)
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(zipDist)
|
.then(() => hasVersion && !withoutDist && zipDist(version))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err && err.message) {
|
if (err && err.message) {
|
||||||
console.log(err.message);
|
console.log(err.message);
|
||||||
@@ -200,15 +186,7 @@ function copyPublicFolder() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function zipDist() {
|
function zipDist(version) {
|
||||||
const minArgsToContainVersion = 3;
|
|
||||||
|
|
||||||
// If no version was provided, do nothing
|
|
||||||
if (process.argv.length < minArgsToContainVersion) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [ , , version ] = process.argv;
|
|
||||||
const versionFileName = `./dist/shlink-web-client_${version}_dist.zip`;
|
const versionFileName = `./dist/shlink-web-client_${version}_dist.zip`;
|
||||||
|
|
||||||
console.log(chalk.cyan(`Generating dist file for version ${chalk.bold(version)}...`));
|
console.log(chalk.cyan(`Generating dist file for version ${chalk.bold(version)}...`));
|
||||||
@@ -226,4 +204,24 @@ function zipDist() {
|
|||||||
console.log(chalk.red('An error occurred while generating dist file'));
|
console.log(chalk.red('An error occurred while generating dist file'));
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVersionFromArgs(argv) {
|
||||||
|
const [ version ] = 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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,21 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { serverType } from '../servers/prop-types';
|
import { serverType } from '../servers/prop-types';
|
||||||
import './AsideMenu.scss';
|
import './AsideMenu.scss';
|
||||||
|
|
||||||
const defaultProps = {
|
const AsideMenuItem = ({ children, to, ...rest }) => (
|
||||||
className: '',
|
<NavLink className="aside-menu__item" activeClassName="aside-menu__item--selected" to={to} {...rest}>
|
||||||
showOnMobile: false,
|
{children}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
|
||||||
|
AsideMenuItem.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
to: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
selectedServer: serverType,
|
selectedServer: serverType,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
@@ -20,51 +27,34 @@ const propTypes = {
|
|||||||
const AsideMenu = (DeleteServerButton) => {
|
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, {
|
||||||
'aside-menu--hidden': !showOnMobile,
|
'aside-menu--hidden': !showOnMobile,
|
||||||
});
|
});
|
||||||
const shortUrlsIsActive = (match, location) => location.pathname.match('/list-short-urls');
|
const shortUrlsIsActive = (match, location) => location.pathname.match('/list-short-urls');
|
||||||
|
const buildPath = (suffix) => `/server/${serverId}${suffix}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={asideClass}>
|
<aside className={asideClass}>
|
||||||
<nav className="nav flex-column aside-menu__nav">
|
<nav className="nav flex-column aside-menu__nav">
|
||||||
<NavLink
|
<AsideMenuItem to={buildPath('/list-short-urls/1')} isActive={shortUrlsIsActive}>
|
||||||
className="aside-menu__item"
|
|
||||||
activeClassName="aside-menu__item--selected"
|
|
||||||
to={`/server/${serverId}/list-short-urls/1`}
|
|
||||||
isActive={shortUrlsIsActive}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={listIcon} />
|
<FontAwesomeIcon icon={listIcon} />
|
||||||
<span className="aside-menu__item-text">List short URLs</span>
|
<span className="aside-menu__item-text">List short URLs</span>
|
||||||
</NavLink>
|
</AsideMenuItem>
|
||||||
<NavLink
|
<AsideMenuItem to={buildPath('/create-short-url')}>
|
||||||
className="aside-menu__item"
|
|
||||||
activeClassName="aside-menu__item--selected"
|
|
||||||
to={`/server/${serverId}/create-short-url`}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={createIcon} flip="horizontal" />
|
<FontAwesomeIcon icon={createIcon} flip="horizontal" />
|
||||||
<span className="aside-menu__item-text">Create short URL</span>
|
<span className="aside-menu__item-text">Create short URL</span>
|
||||||
</NavLink>
|
</AsideMenuItem>
|
||||||
|
<AsideMenuItem to={buildPath('/manage-tags')}>
|
||||||
<NavLink
|
|
||||||
className="aside-menu__item"
|
|
||||||
activeClassName="aside-menu__item--selected"
|
|
||||||
to={`/server/${serverId}/manage-tags`}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={tagsIcon} />
|
<FontAwesomeIcon icon={tagsIcon} />
|
||||||
<span className="aside-menu__item-text">Manage tags</span>
|
<span className="aside-menu__item-text">Manage tags</span>
|
||||||
</NavLink>
|
</AsideMenuItem>
|
||||||
|
|
||||||
<DeleteServerButton
|
<DeleteServerButton className="aside-menu__item aside-menu__item--danger" server={selectedServer} />
|
||||||
className="aside-menu__item aside-menu__item--danger"
|
|
||||||
server={selectedServer}
|
|
||||||
/>
|
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
AsideMenu.defaultProps = defaultProps;
|
|
||||||
AsideMenu.propTypes = propTypes;
|
AsideMenu.propTypes = propTypes;
|
||||||
|
|
||||||
return AsideMenu;
|
return AsideMenu;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const propTypes = {
|
|||||||
selectedServer: serverType,
|
selectedServer: serverType,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisits) => {
|
const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisits, ShlinkVersions) => {
|
||||||
const MenuLayoutComp = ({ match, location, selectedServer, selectServer }) => {
|
const MenuLayoutComp = ({ match, location, selectedServer, selectServer }) => {
|
||||||
const [ showSideBar, setShowSidebar ] = useState(false);
|
const [ showSideBar, setShowSidebar ] = useState(false);
|
||||||
|
|
||||||
@@ -61,15 +61,21 @@ 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="menu-layout__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 className="menu-layout__footer text-center text-md-right">
|
||||||
|
<ShlinkVersions />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Swipeable>
|
</Swipeable>
|
||||||
|
|||||||
@@ -32,3 +32,26 @@
|
|||||||
.menu-layout__burger-icon--active {
|
.menu-layout__burger-icon--active {
|
||||||
color: white;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
29
src/common/ShlinkVersions.js
Normal file
29
src/common/ShlinkVersions.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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, clientVersion = SHLINK_WEB_CLIENT_VERSION }) => {
|
||||||
|
const { printableVersion: serverVersion } = selectedServer;
|
||||||
|
const normalizedClientVersion = pipe(versionToSemVer(), versionToPrintable)(clientVersion);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<small className={classNames('text-muted', className)}>
|
||||||
|
Client: <b>{normalizedClientVersion}</b> - Server: <b>{serverVersion}</b>
|
||||||
|
</small>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ShlinkVersions.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ShlinkVersions;
|
||||||
@@ -4,6 +4,7 @@ import Home from '../Home';
|
|||||||
import MenuLayout from '../MenuLayout';
|
import MenuLayout from '../MenuLayout';
|
||||||
import AsideMenu from '../AsideMenu';
|
import AsideMenu from '../AsideMenu';
|
||||||
import ErrorHandler from '../ErrorHandler';
|
import ErrorHandler from '../ErrorHandler';
|
||||||
|
import ShlinkVersions from '../ShlinkVersions';
|
||||||
|
|
||||||
const provideServices = (bottle, connect, withRouter) => {
|
const provideServices = (bottle, connect, withRouter) => {
|
||||||
bottle.constant('window', global.window);
|
bottle.constant('window', global.window);
|
||||||
@@ -25,13 +26,17 @@ const provideServices = (bottle, connect, withRouter) => {
|
|||||||
'ShortUrls',
|
'ShortUrls',
|
||||||
'AsideMenu',
|
'AsideMenu',
|
||||||
'CreateShortUrl',
|
'CreateShortUrl',
|
||||||
'ShortUrlVisits'
|
'ShortUrlVisits',
|
||||||
|
'ShlinkVersions'
|
||||||
);
|
);
|
||||||
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');
|
bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton');
|
||||||
|
|
||||||
|
bottle.serviceFactory('ShlinkVersions', () => ShlinkVersions);
|
||||||
|
bottle.decorator('ShlinkVersions', connect([ 'selectedServer' ]));
|
||||||
|
|
||||||
bottle.serviceFactory('ErrorHandler', ErrorHandler, 'window', 'console');
|
bottle.serviceFactory('ErrorHandler', ErrorHandler, 'window', 'console');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,14 +28,6 @@ body,
|
|||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shlink-container {
|
|
||||||
padding: 20px 0;
|
|
||||||
|
|
||||||
@media (min-width: $mdMin) {
|
|
||||||
padding: 30px 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-main {
|
.badge-main {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: $mainColor;
|
background-color: $mainColor;
|
||||||
|
|||||||
@@ -1,40 +1,38 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
import { faMinusCircle as deleteIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faMinusCircle as deleteIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { serverType } from './prop-types';
|
import { serverType } from './prop-types';
|
||||||
|
|
||||||
const DeleteServerButton = (DeleteServerModal) => class DeleteServerButton extends React.Component {
|
const propTypes = {
|
||||||
static propTypes = {
|
server: serverType,
|
||||||
server: serverType,
|
className: PropTypes.string,
|
||||||
className: PropTypes.string,
|
};
|
||||||
};
|
|
||||||
|
|
||||||
state = { isModalOpen: false };
|
const DeleteServerButton = (DeleteServerModal) => {
|
||||||
|
const DeleteServerButtonComp = ({ server, className }) => {
|
||||||
render() {
|
const [ isModalOpen, setModalOpen ] = useState(false);
|
||||||
const { server, className } = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<span
|
<span className={className} key="deleteServerBtn" onClick={() => setModalOpen(true)}>
|
||||||
className={className}
|
|
||||||
key="deleteServerBtn"
|
|
||||||
onClick={() => this.setState({ isModalOpen: true })}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={deleteIcon} />
|
<FontAwesomeIcon icon={deleteIcon} />
|
||||||
<span className="aside-menu__item-text">Delete this server</span>
|
<span className="aside-menu__item-text">Remove this server</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<DeleteServerModal
|
<DeleteServerModal
|
||||||
isOpen={this.state.isModalOpen}
|
isOpen={isModalOpen}
|
||||||
toggle={() => this.setState(({ isModalOpen }) => ({ isModalOpen: !isModalOpen }))}
|
toggle={() => setModalOpen(!isModalOpen)}
|
||||||
server={server}
|
server={server}
|
||||||
key="deleteServerModal"
|
key="deleteServerModal"
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
DeleteServerButtonComp.propTypes = propTypes;
|
||||||
|
|
||||||
|
return DeleteServerButtonComp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeleteServerButton;
|
export default DeleteServerButton;
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ const DeleteServerModal = ({ server, toggle, isOpen, deleteServer, history }) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} toggle={toggle} centered>
|
<Modal isOpen={isOpen} toggle={toggle} centered>
|
||||||
<ModalHeader toggle={toggle}><span className="text-danger">Delete server</span></ModalHeader>
|
<ModalHeader toggle={toggle}><span className="text-danger">Remove server</span></ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<p>Are you sure you want to delete server <b>{server ? server.name : ''}</b>?</p>
|
<p>Are you sure you want to remove <b>{server ? server.name : ''}</b>?</p>
|
||||||
<p>
|
<p>
|
||||||
No data will be deleted, only the access to that server will be removed from this host.
|
<i>
|
||||||
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.
|
||||||
|
</i>
|
||||||
</p>
|
</p>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { compareVersions } from '../../utils/utils';
|
|
||||||
import { serverType } from '../prop-types';
|
import { serverType } from '../prop-types';
|
||||||
|
import { compareVersions } from '../../utils/versionHelpers';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
minVersion: PropTypes.string,
|
minVersion: PropTypes.string,
|
||||||
|
|||||||
@@ -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,6 +1,7 @@
|
|||||||
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 { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/versionHelpers';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
export const SELECT_SERVER = 'shlink/selectedServer/SELECT_SERVER';
|
export const SELECT_SERVER = 'shlink/selectedServer/SELECT_SERVER';
|
||||||
@@ -12,6 +13,10 @@ 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,
|
||||||
|
toSemVer(MIN_FALLBACK_VERSION)
|
||||||
|
);
|
||||||
|
|
||||||
export const resetSelectedServer = createAction(RESET_SELECTED_SERVER);
|
export const resetSelectedServer = createAction(RESET_SELECTED_SERVER);
|
||||||
|
|
||||||
@@ -20,16 +25,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),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import * as PropTypes from 'prop-types';
|
|||||||
import DateInput from '../utils/DateInput';
|
import DateInput from '../utils/DateInput';
|
||||||
import Checkbox from '../utils/Checkbox';
|
import Checkbox from '../utils/Checkbox';
|
||||||
import { serverType } from '../servers/prop-types';
|
import { serverType } from '../servers/prop-types';
|
||||||
import { compareVersions } from '../utils/utils';
|
import { compareVersions } from '../utils/versionHelpers';
|
||||||
import { createShortUrlResultType } from './reducers/shortUrlCreation';
|
import { createShortUrlResultType } from './reducers/shortUrlCreation';
|
||||||
import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
|
import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
|
||||||
|
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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 { range } from 'ramda';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { compare } from 'compare-versions';
|
|
||||||
|
|
||||||
const TEN_ROUNDING_NUMBER = 10;
|
const TEN_ROUNDING_NUMBER = 10;
|
||||||
const DEFAULT_TIMEOUT_DELAY = 2000;
|
const DEFAULT_TIMEOUT_DELAY = 2000;
|
||||||
@@ -53,20 +52,6 @@ export const useToggle = (initialValue = false) => {
|
|||||||
return [ flag, () => setFlag(!flag) ];
|
return [ flag, () => setFlag(!flag) ];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const compareVersions = (firstVersion, operator, secondVersion) => compare(
|
|
||||||
firstVersion,
|
|
||||||
secondVersion,
|
|
||||||
operator
|
|
||||||
);
|
|
||||||
|
|
||||||
export const versionIsValidSemVer = (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 formatDate = (format = 'YYYY-MM-DD') => (date) => date && date.format ? date.format(format) : date;
|
||||||
|
|
||||||
export const formatIsoDate = (date) => date && date.format ? date.format() : date;
|
export const formatIsoDate = (date) => date && date.format ? date.format() : date;
|
||||||
|
|||||||
21
src/utils/versionHelpers.js
Normal file
21
src/utils/versionHelpers.js
Normal file
@@ -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;
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
|
||||||
import asideMenuCreator from '../../src/common/AsideMenu';
|
import asideMenuCreator from '../../src/common/AsideMenu';
|
||||||
|
|
||||||
describe('<AsideMenu />', () => {
|
describe('<AsideMenu />', () => {
|
||||||
@@ -15,7 +14,7 @@ describe('<AsideMenu />', () => {
|
|||||||
afterEach(() => wrapped.unmount());
|
afterEach(() => wrapped.unmount());
|
||||||
|
|
||||||
it('contains links to different sections', () => {
|
it('contains links to different sections', () => {
|
||||||
const links = wrapped.find(NavLink);
|
const links = wrapped.find('[to]');
|
||||||
|
|
||||||
expect(links).toHaveLength(3);
|
expect(links).toHaveLength(3);
|
||||||
links.forEach((link) => expect(link.prop('to')).toContain('abc123'));
|
links.forEach((link) => expect(link.prop('to')).toContain('abc123'));
|
||||||
|
|||||||
26
test/common/ShlinkVersions.test.js
Normal file
26
test/common/ShlinkVersions.test.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import ShlinkVersions from '../../src/common/ShlinkVersions';
|
||||||
|
|
||||||
|
describe('<ShlinkVersions />', () => {
|
||||||
|
let wrapper;
|
||||||
|
const createWrapper = (props) => {
|
||||||
|
wrapper = shallow(<ShlinkVersions {...props} />);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -21,16 +21,8 @@ describe('<DeleteServerButton />', () => {
|
|||||||
it('displays modal when button is clicked', () => {
|
it('displays modal when button is clicked', () => {
|
||||||
const btn = wrapper.find('.button');
|
const btn = wrapper.find('.button');
|
||||||
|
|
||||||
expect(wrapper.state('isModalOpen')).toEqual(false);
|
expect(wrapper.find(DeleteServerModal).prop('isOpen')).toEqual(false);
|
||||||
btn.simulate('click');
|
btn.simulate('click');
|
||||||
expect(wrapper.state('isModalOpen')).toEqual(true);
|
expect(wrapper.find(DeleteServerModal).prop('isOpen')).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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ describe('<DeleteServerModal />', () => {
|
|||||||
const modalBody = wrapper.find(ModalBody);
|
const modalBody = wrapper.find(ModalBody);
|
||||||
|
|
||||||
expect(modalBody.find('p').first().text()).toEqual(
|
expect(modalBody.find('p').first().text()).toEqual(
|
||||||
`Are you sure you want to delete server ${serverName}?`
|
`Are you sure you want to remove ${serverName}?`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -44,13 +44,14 @@ describe('selectedServerReducer', () => {
|
|||||||
afterEach(jest.clearAllMocks);
|
afterEach(jest.clearAllMocks);
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[ version, version ],
|
[ version, version, `v${version}` ],
|
||||||
[ 'latest', MAX_FALLBACK_VERSION ],
|
[ 'latest', MAX_FALLBACK_VERSION, 'latest' ],
|
||||||
[ '%invalid_semver%', MIN_FALLBACK_VERSION ],
|
[ '%invalid_semver%', MIN_FALLBACK_VERSION, '%invalid_semver%' ],
|
||||||
])('dispatches proper actions', async (serverVersion, expectedVersion) => {
|
])('dispatches proper actions', async (serverVersion, expectedVersion, expectedPrintableVersion) => {
|
||||||
const expectedSelectedServer = {
|
const expectedSelectedServer = {
|
||||||
...selectedServer,
|
...selectedServer,
|
||||||
version: expectedVersion,
|
version: expectedVersion,
|
||||||
|
printableVersion: expectedPrintableVersion,
|
||||||
};
|
};
|
||||||
|
|
||||||
apiClientMock.health.mockResolvedValue({ version: serverVersion });
|
apiClientMock.health.mockResolvedValue({ version: serverVersion });
|
||||||
|
|||||||
Reference in New Issue
Block a user