Pull request #730: + client: Add Hot Module Replacement

Merge in DNS/adguard-home from feature/hmr to master

Squashed commit of the following:

commit 952ed1955c2a7a32446d99489f137f02eb47c99e
Merge: 83484931 de92c852
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu Aug 13 11:02:10 2020 +0300

    Merge branch 'master' into feature/hmr

commit 8348493105d7d63d8b0836a5c272df2b17a6b142
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Aug 5 15:07:31 2020 +0300

    Remove empty prop types, remove Services empty container

commit b2fe4a30b79d91e482318ee5deea8e49c7038f7e
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Aug 5 13:56:35 2020 +0300

    Move constants

commit f8be4c18c35193ad77bf5e25f311ad834c1d6870
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Aug 5 13:19:02 2020 +0300

    Fix Setup bug, update webpack.dev

commit 1d9cc4ddf8af2c979eb707a7f0fc06744eec186c
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Aug 5 12:10:38 2020 +0300

    Review changes

commit a1edb21358def21ed1808b081ffc2f0b6755e3da
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Aug 5 11:46:58 2020 +0300

    Remove lazy loading, fix updated components

commit 0aa2cf55f8d4206ac9e2f99fc1b990ed8a9c7825
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Aug 4 20:32:19 2020 +0300

    Refactor App component, add lazy loading

commit 3c2ba4772a91ff7b06641dba6c6bf3fdcd2fdf7f
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Aug 4 17:12:41 2020 +0300

    Simplify App hot loading boilerplate, setup lazy loading, update Header

commit 8df3221f315372b066f2ac0c9a1687f1677b8415
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Aug 4 15:16:06 2020 +0300

    + client: Add Hot Module Replacement
This commit is contained in:
Artem Baskal
2020-08-13 11:15:45 +03:00
parent de92c85256
commit 97df19898f
38 changed files with 1098 additions and 822 deletions

View File

@@ -1,30 +1,16 @@
import React, { Component, Fragment } from 'react';
import React, { useEffect } from 'react';
import { HashRouter, Route } from 'react-router-dom';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
import LoadingBar from 'react-redux-loading-bar';
import { hot } from 'react-hot-loader/root';
import 'react-table/react-table.css';
import '../ui/Tabler.css';
import '../ui/ReactTable.css';
import './index.css';
import Header from '../../containers/Header';
import Dashboard from '../../containers/Dashboard';
import Settings from '../../containers/Settings';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import CustomRules from '../../containers/CustomRules';
import DnsBlocklist from '../../containers/DnsBlocklist';
import DnsAllowlist from '../../containers/DnsAllowlist';
import DnsRewrites from '../../containers/DnsRewrites';
import Dns from '../../containers/Dns';
import Encryption from '../../containers/Encryption';
import Dhcp from '../../containers/Dhcp';
import Clients from '../../containers/Clients';
import Logs from '../../containers/Logs';
import SetupGuide from '../../containers/SetupGuide';
import propTypes from 'prop-types';
import Toasts from '../Toasts';
import Footer from '../ui/Footer';
import Status from '../ui/Status';
@@ -35,31 +21,107 @@ import Icons from '../ui/Icons';
import i18n from '../../i18n';
import Loading from '../ui/Loading';
import { FILTERS_URLS, MENU_URLS, SETTINGS_URLS } from '../../helpers/constants';
import Services from '../Filters/Services';
import { getLogsUrlParams, setHtmlLangAttr } from '../../helpers/helpers';
import Header from '../Header';
import { changeLanguage, getDnsStatus } from '../../actions';
class App extends Component {
componentDidMount() {
this.props.getDnsStatus();
}
import Dashboard from '../../containers/Dashboard';
import Logs from '../../containers/Logs';
import SetupGuide from '../../containers/SetupGuide';
import Settings from '../../containers/Settings';
import Dns from '../../containers/Dns';
import Encryption from '../../containers/Encryption';
import Dhcp from '../../containers/Dhcp';
import Clients from '../../containers/Clients';
import DnsBlocklist from '../../containers/DnsBlocklist';
import DnsAllowlist from '../../containers/DnsAllowlist';
import DnsRewrites from '../../containers/DnsRewrites';
import CustomRules from '../../containers/CustomRules';
import Services from '../Filters/Services';
componentDidUpdate(prevProps) {
if (this.props.dashboard.language !== prevProps.dashboard.language) {
this.setLanguage();
}
}
const ROUTES = [
{
path: MENU_URLS.root,
component: Dashboard,
exact: true,
},
{
path: [`${MENU_URLS.logs}${getLogsUrlParams(':search?', ':response_status?')}`, MENU_URLS.logs],
component: Logs,
},
{
path: MENU_URLS.guide,
component: SetupGuide,
},
{
path: SETTINGS_URLS.settings,
component: Settings,
},
{
path: SETTINGS_URLS.dns,
component: Dns,
},
{
path: SETTINGS_URLS.encryption,
component: Encryption,
},
{
path: SETTINGS_URLS.dhcp,
component: Dhcp,
},
{
path: SETTINGS_URLS.clients,
component: Clients,
},
{
path: FILTERS_URLS.dns_blocklists,
component: DnsBlocklist,
},
{
path: FILTERS_URLS.dns_allowlists,
component: DnsAllowlist,
},
{
path: FILTERS_URLS.dns_rewrites,
component: DnsRewrites,
},
{
path: FILTERS_URLS.custom_rules,
component: CustomRules,
},
{
path: FILTERS_URLS.blocked_services,
component: Services,
},
];
reloadPage = () => {
window.location.reload();
};
const renderRoute = ({ path, component, exact }, idx) => <Route
key={idx}
exact={exact}
path={path}
component={component}
/>;
handleUpdate = () => {
this.props.getUpdate();
};
const App = () => {
const dispatch = useDispatch();
const {
language,
isCoreRunning,
isUpdateAvailable,
processing,
} = useSelector((state) => state.dashboard, shallowEqual);
setLanguage = () => {
const { processing, language } = this.props.dashboard;
const { processing: processingEncryption } = useSelector((
state,
) => state.encryption, shallowEqual);
const updateAvailable = isCoreRunning && isUpdateAvailable;
useEffect(() => {
dispatch(getDnsStatus());
}, []);
const setLanguage = () => {
if (!processing) {
if (language) {
i18n.changeLanguage(language);
@@ -68,93 +130,52 @@ class App extends Component {
}
i18n.on('languageChanged', (lang) => {
this.props.changeLanguage(lang);
dispatch(changeLanguage(lang));
});
};
render() {
const { dashboard, encryption, getVersion } = this.props;
const updateAvailable = dashboard.isCoreRunning && dashboard.isUpdateAvailable;
useEffect(() => {
setLanguage();
}, [language]);
return (
<HashRouter hashType="noslash">
<Fragment>
{updateAvailable && (
<Fragment>
<UpdateTopline
url={dashboard.announcementUrl}
version={dashboard.newVersion}
canAutoUpdate={dashboard.canAutoUpdate}
getUpdate={this.handleUpdate}
processingUpdate={dashboard.processingUpdate}
/>
<UpdateOverlay processingUpdate={dashboard.processingUpdate} />
</Fragment>
)}
{!encryption.processing && (
<EncryptionTopline notAfter={encryption.not_after} />
)}
<LoadingBar className="loading-bar" updateTime={1000} />
<Route component={Header} />
<div className="container container--wrap pb-5">
{dashboard.processing && <Loading />}
{!dashboard.isCoreRunning && (
<div className="row row-cards">
<div className="col-lg-12">
<Status reloadPage={this.reloadPage}
message="dns_start"
/>
<Loading />
</div>
const reloadPage = () => {
window.location.reload();
};
return (
<HashRouter hashType="noslash">
<>
{updateAvailable && <>
<UpdateTopline />
<UpdateOverlay />
</>}
{!processingEncryption && <EncryptionTopline />}
<LoadingBar className="loading-bar" updateTime={1000} />
<Header />
<div className="container container--wrap pb-5">
{processing && <Loading />}
{!isCoreRunning && (
<div className="row row-cards">
<div className="col-lg-12">
<Status reloadPage={reloadPage} message="dns_start" />
<Loading />
</div>
)}
{!dashboard.processing && dashboard.isCoreRunning && (
<>
<Route path={MENU_URLS.root} exact component={Dashboard} />
<Route
path={[`${MENU_URLS.logs}${getLogsUrlParams(':search?', ':response_status?')}`, MENU_URLS.logs]}
component={Logs} />
<Route path={MENU_URLS.guide} component={SetupGuide} />
<Route path={SETTINGS_URLS.settings} component={Settings} />
<Route path={SETTINGS_URLS.dns} component={Dns} />
<Route path={SETTINGS_URLS.encryption} component={Encryption} />
<Route path={SETTINGS_URLS.dhcp} component={Dhcp} />
<Route path={SETTINGS_URLS.clients} component={Clients} />
<Route path={FILTERS_URLS.dns_blocklists}
component={DnsBlocklist} />
<Route path={FILTERS_URLS.dns_allowlists}
component={DnsAllowlist} />
<Route path={FILTERS_URLS.dns_rewrites} component={DnsRewrites} />
<Route path={FILTERS_URLS.custom_rules} component={CustomRules} />
<Route path={FILTERS_URLS.blocked_services} component={Services} />
</>
)}
</div>
<Footer
dnsVersion={dashboard.dnsVersion}
dnsPort={dashboard.dnsPort}
processingVersion={dashboard.processingVersion}
getVersion={getVersion}
checkUpdateFlag={dashboard.checkUpdateFlag}
/>
<Toasts />
<Icons />
</Fragment>
</HashRouter>
);
}
}
App.propTypes = {
getDnsStatus: PropTypes.func,
getUpdate: PropTypes.func,
enableDns: PropTypes.func,
dashboard: PropTypes.object,
isCoreRunning: PropTypes.bool,
error: PropTypes.string,
changeLanguage: PropTypes.func,
encryption: PropTypes.object,
getVersion: PropTypes.func,
</div>
)}
{!processing && isCoreRunning && ROUTES.map(renderRoute)}
</div>
<Footer />
<Toasts />
<Icons />
</>
</HashRouter>
);
};
export default withTranslation()(App);
renderRoute.propTypes = {
path: propTypes.oneOfType([propTypes.string, propTypes.arrayOf(propTypes.string)]).isRequired,
component: propTypes.element.isRequired,
exact: propTypes.bool,
};
export default hot(App);

View File

@@ -59,6 +59,4 @@ const Services = () => {
);
};
Services.propTypes = {};
export default Services;

View File

@@ -90,9 +90,8 @@ class Menu extends Component {
};
getActiveClassForDropdown = (URLS) => {
const { pathname } = this.props.location;
const isActivePage = Object.values(URLS)
.some((item) => item === pathname);
.some((item) => item === this.props.pathname);
return isActivePage ? 'active' : '';
};
@@ -180,9 +179,9 @@ class Menu extends Component {
}
Menu.propTypes = {
isMenuOpen: PropTypes.bool,
closeMenu: PropTypes.func,
location: PropTypes.object,
isMenuOpen: PropTypes.bool.isRequired,
closeMenu: PropTypes.func.isRequired,
pathname: PropTypes.string.isRequired,
t: PropTypes.func,
};

View File

@@ -1,83 +1,77 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { shallowEqual, useSelector } from 'react-redux';
import { Trans } from 'react-i18next';
import classnames from 'classnames';
import { Trans, withTranslation } from 'react-i18next';
import Menu from './Menu';
import logo from '../ui/svg/logo.svg';
import './Header.css';
class Header extends Component {
state = {
isMenuOpen: false,
const Header = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const {
protectionEnabled,
processing,
isCoreRunning,
processingProfile,
name,
} = useSelector((state) => state.dashboard, shallowEqual);
const { pathname } = useLocation();
const toggleMenuOpen = () => {
setIsMenuOpen((isMenuOpen) => !isMenuOpen);
};
toggleMenuOpen = () => {
this.setState((prevState) => ({ isMenuOpen: !prevState.isMenuOpen }));
const closeMenu = () => {
setIsMenuOpen(false);
};
closeMenu = () => {
this.setState({ isMenuOpen: false });
};
const badgeClass = classnames('badge dns-status', {
'badge-success': protectionEnabled,
'badge-danger': !protectionEnabled,
});
render() {
const { dashboard, location } = this.props;
const { isMenuOpen } = this.state;
const badgeClass = classnames({
'badge dns-status': true,
'badge-success': dashboard.protectionEnabled,
'badge-danger': !dashboard.protectionEnabled,
});
return (
<div className="header">
<div className="header__container">
<div className="header__row">
<div
className="header-toggler d-lg-none ml-lg-0 collapsed"
onClick={this.toggleMenuOpen}
>
<span className="header-toggler-icon" />
</div>
<div className="header__column">
<div className="d-flex align-items-center">
<Link to="/" className="nav-link pl-0 pr-1">
<img src={logo} alt="" className="header-brand-img" />
</Link>
{!dashboard.processing && dashboard.isCoreRunning && (
<span className={badgeClass}>
<Trans>{dashboard.protectionEnabled ? 'on' : 'off'}</Trans>
return (
<div className="header">
<div className="header__container">
<div className="header__row">
<div
className="header-toggler d-lg-none ml-lg-0 collapsed"
onClick={toggleMenuOpen}
>
<span className="header-toggler-icon" />
</div>
<div className="header__column">
<div className="d-flex align-items-center">
<Link to="/" className="nav-link pl-0 pr-1">
<img src={logo} alt="" className="header-brand-img" />
</Link>
{!processing && isCoreRunning && (
<span className={badgeClass}>
<Trans>{protectionEnabled ? 'on' : 'off'}</Trans>
</span>
)}
</div>
)}
</div>
<Menu
location={location}
isMenuOpen={isMenuOpen}
closeMenu={this.closeMenu}
/>
<div className="header__column">
<div className="header__right">
{!dashboard.processingProfile && dashboard.name
&& <a href="control/logout" className="btn btn-sm btn-outline-secondary">
<Trans>sign_out</Trans>
</a>
}
</div>
</div>
<Menu
pathname={pathname}
isMenuOpen={isMenuOpen}
closeMenu={closeMenu}
/>
<div className="header__column">
<div className="header__right">
{!processingProfile && name
&& <a href="control/logout" className="btn btn-sm btn-outline-secondary">
<Trans>sign_out</Trans>
</a>}
</div>
</div>
</div>
</div>
);
}
}
Header.propTypes = {
dashboard: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
getVersion: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
</div>
);
};
export default withTranslation()(Header);
export default Header;

View File

@@ -437,7 +437,7 @@
}
.custom-select__arrow--left {
background: #fff url('./chevron-down.svg') no-repeat;
background: var(--white) url('../ui/svg/chevron-down.svg') no-repeat;
background-position: 5px 9px;
background-size: 22px;
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>

Before

Width:  |  Height:  |  Size: 264 B

View File

@@ -1,4 +1,4 @@
import React, { Component, Fragment } from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Trans, withTranslation } from 'react-i18next';
@@ -34,13 +34,14 @@ class Dhcp extends Component {
} = this.props.dhcp;
const otherDhcpFound = check?.otherServer
&& check.otherServer.found === DHCP_STATUS_RESPONSE.YES;
const filledConfig = Object.keys(config).every((key) => {
if (key === 'enabled' || key === 'icmp_timeout_msec') {
return true;
}
const filledConfig = Object.keys(config)
.every((key) => {
if (key === 'enabled' || key === 'icmp_timeout_msec') {
return true;
}
return config[key];
});
return config[key];
});
if (config.enabled) {
return (
@@ -114,40 +115,35 @@ class Dhcp extends Component {
getStaticIpWarning = (t, check, interfaceName) => {
if (check.staticIP.static === DHCP_STATUS_RESPONSE.ERROR) {
return (
<Fragment>
<div className="text-danger mb-2">
<Trans>dhcp_static_ip_error</Trans>
<div className="mt-2 mb-2">
<Accordion label={t('error_details')}>
<span>{check.staticIP.error}</span>
</Accordion>
</div>
return <>
<div className="text-danger mb-2">
<Trans>dhcp_static_ip_error</Trans>
<div className="mt-2 mb-2">
<Accordion label={t('error_details')}>
<span>{check.staticIP.error}</span>
</Accordion>
</div>
<hr className="mt-4 mb-4" />
</Fragment>
);
} if (
check.staticIP.static === DHCP_STATUS_RESPONSE.NO
</div>
<hr className="mt-4 mb-4" />
</>;
}
if (check.staticIP.static === DHCP_STATUS_RESPONSE.NO
&& check.staticIP.ip
&& interfaceName
) {
return (
<Fragment>
<div className="text-secondary mb-2">
<Trans
components={[<strong key="0">example</strong>]}
values={{
interfaceName,
ipAddress: check.staticIP.ip,
}}
>
dhcp_dynamic_ip_found
</Trans>
</div>
<hr className="mt-4 mb-4" />
</Fragment>
);
&& interfaceName) {
return <>
<div className="text-secondary mb-2">
<Trans
components={[<strong key="0">example</strong>]}
values={{
interfaceName,
ipAddress: check.staticIP.ip,
}}
>
dhcp_dynamic_ip_found
</Trans>
</div>
<hr className="mt-4 mb-4" />
</>;
}
return '';
@@ -163,104 +159,101 @@ class Dhcp extends Component {
removeStaticLease,
toggleLeaseModal,
} = this.props;
const statusButtonClass = classnames({
'btn btn-primary btn-standard': true,
'btn btn-primary btn-standard btn-loading': dhcp.processingStatus,
});
const { enabled, interface_name, ...values } = dhcp.config;
return (
<Fragment>
<PageTitle title={t('dhcp_settings')} />
{(dhcp.processing || dhcp.processingInterfaces) && <Loading />}
{!dhcp.processing && !dhcp.processingInterfaces && (
<Fragment>
<Card
title={t('dhcp_title')}
subtitle={t('dhcp_description')}
bodyType="card-body box-body--settings"
>
<div className="dhcp">
<Fragment>
<Form
onSubmit={this.handleFormSubmit}
initialValues={{
interface_name,
...values,
}}
interfaces={dhcp.interfaces}
processingConfig={dhcp.processingConfig}
processingInterfaces={dhcp.processingInterfaces}
enabled={enabled}
resetDhcp={resetDhcp}
/>
<hr />
<div className="card-actions mb-3">
{this.getToggleDhcpButton()}
<button
type="button"
className={statusButtonClass}
onClick={() => findActiveDhcp(interface_name)}
disabled={
enabled || !interface_name || dhcp.processingConfig
}
>
<Trans>check_dhcp_servers</Trans>
</button>
</div>
{!enabled && dhcp.check && (
<Fragment>
{this.getStaticIpWarning(t, dhcp.check, interface_name)}
{this.getActiveDhcpMessage(t, dhcp.check)}
{this.getDhcpWarning(dhcp.check)}
</Fragment>
)}
</Fragment>
return <>
<PageTitle title={t('dhcp_settings')} />
{(dhcp.processing || dhcp.processingInterfaces) && <Loading />}
{!dhcp.processing && !dhcp.processingInterfaces && <>
<Card
title={t('dhcp_title')}
subtitle={t('dhcp_description')}
bodyType="card-body box-body--settings"
>
<div className="dhcp">
<>
<Form
onSubmit={this.handleFormSubmit}
initialValues={{
interface_name,
...values,
}}
interfaces={dhcp.interfaces}
processingConfig={dhcp.processingConfig}
processingInterfaces={dhcp.processingInterfaces}
enabled={enabled}
resetDhcp={resetDhcp}
/>
<hr />
<div className="card-actions mb-3">
{this.getToggleDhcpButton()}
<button
type="button"
className={statusButtonClass}
onClick={() => findActiveDhcp(interface_name)}
disabled={
enabled || !interface_name || dhcp.processingConfig
}
>
<Trans>check_dhcp_servers</Trans>
</button>
</div>
</Card>
{dhcp.config.enabled && (
<Card
title={t('dhcp_leases')}
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col">
<Leases leases={dhcp.leases} />
</div>
</div>
</Card>
)}
<Card
title={t('dhcp_static_leases')}
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col-12">
<StaticLeases
staticLeases={dhcp.staticLeases}
isModalOpen={dhcp.isModalOpen}
addStaticLease={addStaticLease}
removeStaticLease={removeStaticLease}
toggleLeaseModal={toggleLeaseModal}
processingAdding={dhcp.processingAdding}
processingDeleting={dhcp.processingDeleting}
/>
</div>
<div className="col-12">
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => toggleLeaseModal()}
>
<Trans>dhcp_add_static_lease</Trans>
</button>
</div>
{!enabled && dhcp.check && (
<>
{this.getStaticIpWarning(t, dhcp.check, interface_name)}
{this.getActiveDhcpMessage(t, dhcp.check)}
{this.getDhcpWarning(dhcp.check)}
</>
)}
</>
</div>
</Card>
{dhcp.config.enabled && (
<Card
title={t('dhcp_leases')}
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col">
<Leases leases={dhcp.leases} />
</div>
</Card>
</Fragment>
</div>
</Card>
)}
</Fragment>
);
<Card
title={t('dhcp_static_leases')}
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col-12">
<StaticLeases
staticLeases={dhcp.staticLeases}
isModalOpen={dhcp.isModalOpen}
addStaticLease={addStaticLease}
removeStaticLease={removeStaticLease}
toggleLeaseModal={toggleLeaseModal}
processingAdding={dhcp.processingAdding}
processingDeleting={dhcp.processingDeleting}
/>
</div>
<div className="col-12">
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => toggleLeaseModal()}
>
<Trans>dhcp_add_static_lease</Trans>
</button>
</div>
</div>
</Card>
</>}
</>;
}
}

View File

@@ -1,40 +1,36 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import Form from './Form';
import Card from '../../../ui/Card';
import { setAccessList } from '../../../../actions/access';
class Access extends Component {
handleFormSubmit = (values) => {
this.props.setAccessList(values);
const Access = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const {
processing,
processingSet,
...values
} = useSelector((state) => state.access, shallowEqual);
const handleFormSubmit = (values) => {
dispatch(setAccessList(values));
};
render() {
const { t, access } = this.props;
const { processing, processingSet, ...values } = access;
return (
<Card
title={t('access_title')}
subtitle={t('access_desc')}
bodyType="card-body box-body--settings"
>
<Form
initialValues={values}
onSubmit={this.handleFormSubmit}
processingSet={processingSet}
/>
</Card>
);
}
}
Access.propTypes = {
access: PropTypes.object.isRequired,
setAccessList: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
return (
<Card
title={t('access_title')}
subtitle={t('access_desc')}
bodyType="card-body box-body--settings"
>
<Form
initialValues={values}
onSubmit={handleFormSubmit}
processingSet={processingSet}
/>
</Card>
);
};
export default withTranslation()(Access);
export default Access;

View File

@@ -1,9 +1,8 @@
import React, { Fragment } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import { shallowEqual, useSelector } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import { Trans, useTranslation } from 'react-i18next';
import {
renderInputField,
renderRadioField,
@@ -18,32 +17,36 @@ import {
} from '../../../../helpers/validators';
import { BLOCKING_MODES, FORM_NAME } from '../../../../helpers/constants';
const checkboxes = [{
name: 'edns_cs_enabled',
placeholder: 'edns_enable',
subtitle: 'edns_cs_desc',
},
{
name: 'dnssec_enabled',
placeholder: 'dnssec_enable',
subtitle: 'dnssec_enable_desc',
},
{
name: 'disable_ipv6',
placeholder: 'disable_ipv6',
subtitle: 'disable_ipv6_desc',
}];
const checkboxes = [
{
name: 'edns_cs_enabled',
placeholder: 'edns_enable',
subtitle: 'edns_cs_desc',
},
{
name: 'dnssec_enabled',
placeholder: 'dnssec_enable',
subtitle: 'dnssec_enable_desc',
},
{
name: 'disable_ipv6',
placeholder: 'disable_ipv6',
subtitle: 'disable_ipv6_desc',
},
];
const customIps = [{
description: 'blocking_ipv4_desc',
name: 'blocking_ipv4',
validateIp: validateIpv4,
},
{
description: 'blocking_ipv6_desc',
name: 'blocking_ipv6',
validateIp: validateIpv6,
}];
const customIps = [
{
description: 'blocking_ipv4_desc',
name: 'blocking_ipv4',
validateIp: validateIpv4,
},
{
description: 'blocking_ipv6_desc',
name: 'blocking_ipv6',
validateIp: validateIpv6,
},
];
const getFields = (processing, t) => Object.values(BLOCKING_MODES)
.map((mode) => (
@@ -58,114 +61,107 @@ const getFields = (processing, t) => Object.values(BLOCKING_MODES)
/>
));
let Form = ({
handleSubmit, submitting, invalid, processing, blockingMode, t,
}) => <form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12 col-sm-6">
<div className="form__group form__group--settings">
<label htmlFor="ratelimit"
className="form__label form__label--with-desc">
<Trans>rate_limit</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>rate_limit_desc</Trans>
</div>
<Field
name="ratelimit"
type="number"
component={renderInputField}
className="form-control"
placeholder={t('form_enter_rate_limit')}
normalize={toNumber}
validate={[validateRequiredValue, validateBiggerOrEqualZeroValue]}
/>
</div>
</div>
{checkboxes.map(({ name, placeholder, subtitle }) => <div className="col-12" key={name}>
<div className="form__group form__group--settings">
<Field
name={name}
type="checkbox"
component={renderSelectField}
placeholder={t(placeholder)}
disabled={processing}
subtitle={t(subtitle)}
/>
</div>
</div>)}
<div className="col-12">
<div className="form__group form__group--settings mb-4">
<label className="form__label form__label--with-desc">
<Trans>blocking_mode</Trans>
</label>
<div className="form__desc form__desc--top">
{Object.values(BLOCKING_MODES)
.map((mode) => (
<li key={mode}>
<Trans>{`blocking_mode_${mode}`}</Trans>
</li>
))}
</div>
<div className="custom-controls-stacked">
{getFields(processing, t)}
</div>
</div>
</div>
{blockingMode === BLOCKING_MODES.custom_ip && (
<Fragment>
{customIps.map(({
description,
name,
validateIp,
}) => <div className="col-12 col-sm-6" key={name}>
<div className="form__group form__group--settings">
<label className="form__label form__label--with-desc"
htmlFor={name}><Trans>{name}</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>{description}</Trans>
</div>
<Field
name={name}
component={renderInputField}
className="form-control"
placeholder={t('form_enter_ip')}
validate={[validateIp, validateRequiredValue]}
/>
const Form = ({
handleSubmit, submitting, invalid, processing,
}) => {
const { t } = useTranslation();
const {
blocking_mode,
} = useSelector((state) => state.form[FORM_NAME.BLOCKING_MODE].values ?? {}, shallowEqual);
return <form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12 col-sm-6">
<div className="form__group form__group--settings">
<label htmlFor="ratelimit"
className="form__label form__label--with-desc">
<Trans>rate_limit</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>rate_limit_desc</Trans>
</div>
</div>)}
</Fragment>
)}
</div>
<button
type="submit"
className="btn btn-success btn-standard btn-large"
disabled={submitting || invalid || processing}
>
<Trans>save_btn</Trans>
</button>
</form>;
<Field
name="ratelimit"
type="number"
component={renderInputField}
className="form-control"
placeholder={t('form_enter_rate_limit')}
normalize={toNumber}
validate={[validateRequiredValue, validateBiggerOrEqualZeroValue]}
/>
</div>
</div>
{checkboxes.map(({ name, placeholder, subtitle }) => <div className="col-12" key={name}>
<div className="form__group form__group--settings">
<Field
name={name}
type="checkbox"
component={renderSelectField}
placeholder={t(placeholder)}
disabled={processing}
subtitle={t(subtitle)}
/>
</div>
</div>)}
<div className="col-12">
<div className="form__group form__group--settings mb-4">
<label className="form__label form__label--with-desc">
<Trans>blocking_mode</Trans>
</label>
<div className="form__desc form__desc--top">
{Object.values(BLOCKING_MODES)
.map((mode) => (
<li key={mode}>
<Trans>{`blocking_mode_${mode}`}</Trans>
</li>
))}
</div>
<div className="custom-controls-stacked">
{getFields(processing, t)}
</div>
</div>
</div>
{blocking_mode === BLOCKING_MODES.custom_ip && (
<>
{customIps.map(({
description,
name,
validateIp,
}) => <div className="col-12 col-sm-6" key={name}>
<div className="form__group form__group--settings">
<label className="form__label form__label--with-desc"
htmlFor={name}><Trans>{name}</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>{description}</Trans>
</div>
<Field
name={name}
component={renderInputField}
className="form-control"
placeholder={t('form_enter_ip')}
validate={[validateIp, validateRequiredValue]}
/>
</div>
</div>)}
</>
)}
</div>
<button
type="submit"
className="btn btn-success btn-standard btn-large"
disabled={submitting || invalid || processing}
>
<Trans>save_btn</Trans>
</button>
</form>;
};
Form.propTypes = {
blockingMode: PropTypes.string.isRequired,
handleSubmit: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
};
const selector = formValueSelector(FORM_NAME.BLOCKING_MODE);
Form = connect((state) => {
const blockingMode = selector(state, 'blocking_mode');
return {
blockingMode,
};
})(Form);
export default flow([
withTranslation(),
reduxForm({ form: FORM_NAME.BLOCKING_MODE }),
])(Form);
export default reduxForm({ form: FORM_NAME.BLOCKING_MODE })(Form);

View File

@@ -1,15 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
import { useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import Card from '../../../ui/Card';
import Form from './Form';
import { setDnsConfig } from '../../../../actions/dnsConfig';
const Config = ({ t, dnsConfig, setDnsConfig }) => {
const handleFormSubmit = (values) => {
setDnsConfig(values);
};
const Config = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const {
blocking_mode,
ratelimit,
@@ -19,7 +17,11 @@ const Config = ({ t, dnsConfig, setDnsConfig }) => {
dnssec_enabled,
disable_ipv6,
processingSetConfig,
} = dnsConfig;
} = useSelector((state) => state.dnsConfig, shallowEqual);
const handleFormSubmit = (values) => {
dispatch(setDnsConfig(values));
};
return (
<Card
@@ -46,10 +48,4 @@ const Config = ({ t, dnsConfig, setDnsConfig }) => {
);
};
Config.propTypes = {
dnsConfig: PropTypes.object.isRequired,
setDnsConfig: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation()(Config);
export default Config;

View File

@@ -1,29 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { shallowEqual, useDispatch } from 'react-redux';
import Form from './Form';
import Card from '../../../ui/Card';
import { setDnsConfig } from '../../../../actions/dnsConfig';
const Upstream = (props) => {
const [t] = useTranslation();
const Upstream = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const {
upstream_dns,
bootstrap_dns,
upstream_mode,
processingSetConfig,
} = ((state) => state.dnsConfig, shallowEqual);
const { processingTestUpstream } = ((state) => state.settings, shallowEqual);
const handleSubmit = (values) => {
dispatch(setDnsConfig(values));
};
const {
processingTestUpstream,
dnsConfig: {
upstream_dns,
bootstrap_dns,
processingSetConfig,
upstream_mode,
},
} = props;
return (
<Card
title={t('upstream_dns')}
@@ -48,9 +45,4 @@ const Upstream = (props) => {
);
};
Upstream.propTypes = {
processingTestUpstream: PropTypes.bool.isRequired,
dnsConfig: PropTypes.object.isRequired,
};
export default Upstream;

View File

@@ -1,67 +1,40 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import Upstream from './Upstream';
import Access from './Access';
import Config from './Config';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
import CacheConfig from './Cache';
import { getDnsConfig } from '../../../actions/dnsConfig';
import { getAccessList } from '../../../actions/access';
const Dns = (props) => {
const Dns = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const processing = useSelector((state) => state.access.processing);
const processingGetConfig = useSelector((state) => state.dnsConfig.processingGetConfig);
const isDataLoading = processing || processingGetConfig;
useEffect(() => {
props.getAccessList();
props.getDnsConfig();
dispatch(getAccessList());
dispatch(getDnsConfig());
}, []);
const {
settings,
access,
setAccessList,
dnsConfig,
setDnsConfig,
} = props;
const isDataLoading = access.processing || dnsConfig.processingGetConfig;
return (
<>
<PageTitle title={t('dns_settings')} />
{isDataLoading
? <Loading />
: <>
<Upstream
processingTestUpstream={settings.processingTestUpstream}
dnsConfig={dnsConfig}
/>
<Config
dnsConfig={dnsConfig}
setDnsConfig={setDnsConfig}
/>
<CacheConfig
dnsConfig={dnsConfig}
setDnsConfig={setDnsConfig}
/>
<Access
access={access}
setAccessList={setAccessList}
/>
</>}
</>
);
};
Dns.propTypes = {
settings: PropTypes.object.isRequired,
getAccessList: PropTypes.func.isRequired,
setAccessList: PropTypes.func.isRequired,
access: PropTypes.object.isRequired,
dnsConfig: PropTypes.object.isRequired,
setDnsConfig: PropTypes.func.isRequired,
getDnsConfig: PropTypes.func.isRequired,
return <>
<PageTitle title={t('dns_settings')} />
{isDataLoading
? <Loading />
: <>
<Upstream />
<Config />
<CacheConfig />
<Access />
</>}
</>;
};
export default Dns;

View File

@@ -1,19 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withTranslation } from 'react-i18next';
import { Trans } from 'react-i18next';
import isAfter from 'date-fns/is_after';
import addDays from 'date-fns/add_days';
import { useSelector } from 'react-redux';
import Topline from './Topline';
import { EMPTY_DATE } from '../../helpers/constants';
const EncryptionTopline = (props) => {
if (props.notAfter === EMPTY_DATE) {
return false;
const EncryptionTopline = () => {
const not_after = useSelector((state) => state.encryption.not_after);
if (not_after === EMPTY_DATE) {
return null;
}
const isAboutExpire = isAfter(addDays(Date.now(), 30), props.notAfter);
const isExpired = isAfter(Date.now(), props.notAfter);
const isAboutExpire = isAfter(addDays(Date.now(), 30), not_after);
const isExpired = isAfter(Date.now(), not_after);
if (isExpired) {
return (
@@ -23,7 +24,9 @@ const EncryptionTopline = (props) => {
</Trans>
</Topline>
);
} if (isAboutExpire) {
}
if (isAboutExpire) {
return (
<Topline type="warning">
<Trans components={[<a href="#encryption" key="0">link</a>]}>
@@ -36,8 +39,4 @@ const EncryptionTopline = (props) => {
return false;
};
EncryptionTopline.propTypes = {
notAfter: PropTypes.string.isRequired,
};
export default withTranslation()(EncryptionTopline);
export default EncryptionTopline;

View File

@@ -1,5 +1,4 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
@@ -28,7 +27,7 @@ const linksData = [
},
];
const Footer = (props) => {
const Footer = () => {
const { t } = useTranslation();
const getYear = () => {
@@ -59,11 +58,6 @@ const Footer = (props) => {
{t(name)}
</a>);
const {
dnsVersion, processingVersion, getVersion, checkUpdateFlag,
} = props;
return (
<>
<footer className="footer">
@@ -94,12 +88,7 @@ const Footer = (props) => {
<div className="footer__row">
{renderCopyright()}
<div className="footer__column footer__column--language">
<Version
dnsVersion={dnsVersion}
processingVersion={processingVersion}
getVersion={getVersion}
checkUpdateFlag={checkUpdateFlag}
/>
<Version />
</div>
</div>
</div>
@@ -108,11 +97,4 @@ const Footer = (props) => {
);
};
Footer.propTypes = {
dnsVersion: PropTypes.string,
processingVersion: PropTypes.bool,
getVersion: PropTypes.func,
checkUpdateFlag: PropTypes.bool,
};
export default Footer;

View File

@@ -1,9 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import './Loading.css';
const Loading = () => (
<div className="loading" />
const Loading = ({ className }) => (
<div className={classNames('loading', className)} />
);
Loading.propTypes = {
className: PropTypes.string,
};
export default Loading;

View File

@@ -1,14 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withTranslation } from 'react-i18next';
import { Trans } from 'react-i18next';
import classnames from 'classnames';
import { useSelector } from 'react-redux';
import './Overlay.css';
const UpdateOverlay = (props) => {
const overlayClass = classnames({
overlay: true,
'overlay--visible': props.processingUpdate,
const UpdateOverlay = () => {
const processingUpdate = useSelector((state) => state.dashboard.processingUpdate);
const overlayClass = classnames('overlay', {
'overlay--visible': processingUpdate,
});
return (
@@ -19,8 +18,4 @@ const UpdateOverlay = (props) => {
);
};
UpdateOverlay.propTypes = {
processingUpdate: PropTypes.bool,
};
export default withTranslation()(UpdateOverlay);
export default UpdateOverlay;

View File

@@ -1,42 +1,46 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Trans, withTranslation } from 'react-i18next';
import React from 'react';
import { Trans } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import Topline from './Topline';
import { getUpdate } from '../../actions';
const UpdateTopline = (props) => (
<Topline type="info">
<Fragment>
const UpdateTopline = () => {
const {
announcementUrl,
newVersion,
canAutoUpdate,
processingUpdate,
} = useSelector((state) => state.dashboard, shallowEqual);
const dispatch = useDispatch();
const handleUpdate = () => {
dispatch(getUpdate());
};
return <Topline type="info">
<>
<Trans
values={{ version: props.version }}
values={{ version: newVersion }}
components={[
<a href={props.url} target="_blank" rel="noopener noreferrer" key="0">
<a href={announcementUrl} target="_blank" rel="noopener noreferrer" key="0">
Click here
</a>,
]}
>
update_announcement
</Trans>
{props.canAutoUpdate
&& <button
type="button"
className="btn btn-sm btn-primary ml-3"
onClick={props.getUpdate}
disabled={props.processingUpdate}
>
<Trans>update_now</Trans>
</button>
{canAutoUpdate
&& <button
type="button"
className="btn btn-sm btn-primary ml-3"
onClick={handleUpdate}
disabled={processingUpdate}
>
<Trans>update_now</Trans>
</button>
}
</Fragment>
</Topline>
);
UpdateTopline.propTypes = {
version: PropTypes.string,
url: PropTypes.string.isRequired,
canAutoUpdate: PropTypes.bool,
getUpdate: PropTypes.func,
processingUpdate: PropTypes.bool,
</>
</Topline>;
};
export default withTranslation()(UpdateTopline);
export default UpdateTopline;

View File

@@ -1,13 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { getVersion } from '../../actions';
import './Version.css';
const Version = (props) => {
const Version = () => {
const dispatch = useDispatch();
const { t } = useTranslation();
const {
dnsVersion, processingVersion, t, checkUpdateFlag,
} = props;
dnsVersion,
processingVersion,
checkUpdateFlag,
} = useSelector((state) => state?.dashboard ?? {}, shallowEqual);
const onClick = () => {
dispatch(getVersion(true));
};
return (
<div className="version">
@@ -20,7 +28,7 @@ const Version = (props) => {
{checkUpdateFlag && <button
type="button"
className="btn btn-icon btn-icon-sm btn-outline-primary btn-sm ml-2"
onClick={() => props.getVersion(true)}
onClick={onClick}
disabled={processingVersion}
title={t('check_updates_now')}
>
@@ -33,12 +41,4 @@ const Version = (props) => {
);
};
Version.propTypes = {
dnsVersion: PropTypes.string,
getVersion: PropTypes.func,
processingVersion: PropTypes.bool,
checkUpdateFlag: PropTypes.bool,
t: PropTypes.func.isRequired,
};
export default withTranslation()(Version);
export default Version;

View File

@@ -1 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>

Before

Width:  |  Height:  |  Size: 264 B

After

Width:  |  Height:  |  Size: 276 B

View File

@@ -1 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe">
<circle cx="12" cy="12" r="10"/>
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
</svg>

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 371 B

View File

@@ -1 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12" y2="17"></line></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle">
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
<line x1="12" y1="17" x2="12" y2="17"></line>
</svg>

Before

Width:  |  Height:  |  Size: 357 B

After

Width:  |  Height:  |  Size: 379 B

View File

@@ -1 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="164" height="41" viewBox="0 0 164 41"><g fill-rule="evenodd"><path d="M129.984 22l-1.162-2.945h-5.792L121.931 22H118l6.277-15h3.509L134 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM117 16.1c0 .88-.153 1.682-.46 2.404a5.223 5.223 0 0 1-1.318 1.857c-.57.516-1.26.918-2.066 1.207-.807.289-1.703.433-2.688.433-1 0-1.9-.144-2.699-.433-.8-.29-1.477-.691-2.034-1.207a5.232 5.232 0 0 1-1.285-1.857c-.3-.722-.45-1.524-.45-2.404V7h3.64v8.81c0 .4.054.777.161 1.135.108.358.272.677.493.96.221.281.514.505.878.67.364.165.803.248 1.317.248.514 0 .953-.083 1.317-.248.365-.165.66-.389.89-.67.228-.283.392-.602.492-.96.1-.358.15-.736.15-1.135V7H117v9.099zm-16 4.673c-.733.362-1.59.658-2.57.886-.98.228-2.047.342-3.203.342-1.199 0-2.302-.181-3.31-.544-1.008-.362-1.875-.872-2.601-1.53a6.977 6.977 0 0 1-1.703-2.366c-.409-.92-.613-1.943-.613-3.07 0-1.141.208-2.175.624-3.1a6.903 6.903 0 0 1 1.723-2.367 7.71 7.71 0 0 1 2.58-1.5C92.914 7.174 93.98 7 95.121 7c1.184 0 2.284.171 3.299.513 1.015.343 1.84.802 2.474 1.38l-2.284 2.476c-.352-.39-.817-.708-1.395-.956-.579-.249-1.234-.373-1.967-.373-.635 0-1.22.111-1.756.332a4.23 4.23 0 0 0-1.395.927 4.178 4.178 0 0 0-.92 1.41 4.734 4.734 0 0 0-.328 1.78c0 .659.099 1.263.296 1.813.197.55.49 1.024.878 1.42.387.395.867.704 1.438.926.57.221 1.223.332 1.956.332.423 0 .825-.03 1.205-.09.381-.061.733-.158 1.058-.293V16h-2.855v-2.779H101v7.55zm63-6.314c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H150V7h5.422c1.06 0 2.104.124 3.135.37a7.866 7.866 0 0 1 2.753 1.23c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zm-75.23 0c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H71V7h5.422c1.06 0 2.104.124 3.135.37A7.866 7.866 0 0 1 82.31 8.6c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zM65.984 22l-1.162-2.945H59.03L57.931 22H54l6.277-15h3.509L70 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM143.855 22l-3.171-5.953h-1.202V22H136V7h5.596c.705 0 1.392.074 2.062.222.67.149 1.271.4 1.803.753a3.9 3.9 0 0 1 1.275 1.398c.318.579.476 1.3.476 2.16 0 1.018-.269 1.872-.808 2.564-.539.693-1.285 1.187-2.238 1.484L148 22h-4.145zm-.145-10.403c0-.353-.073-.639-.218-.858a1.502 1.502 0 0 0-.56-.508 2.393 2.393 0 0 0-.766-.244 5.535 5.535 0 0 0-.819-.063h-1.886v3.495h1.679c.29 0 .587-.024.891-.074.304-.05.58-.137.83-.264.248-.128.452-.311.61-.551.16-.24.239-.551.239-.933zM55 37.851v-8.702h.951v3.866h4.866V29.15h.952v8.702h-.952v-3.916h-4.866v3.916H55zM68.068 38c-2.565 0-4.288-2.076-4.288-4.5 0-2.4 1.747-4.5 4.312-4.5 2.565 0 4.288 2.076 4.288 4.5 0 2.4-1.747 4.5-4.312 4.5zm.024-.907c1.927 0 3.3-1.592 3.3-3.593 0-1.977-1.397-3.593-3.324-3.593-1.927 0-3.3 1.592-3.3 3.593 0 1.977 1.397 3.593 3.324 3.593zm6.3.758v-8.702h.963l3.07 4.749 3.072-4.749h.964v8.702h-.952v-7.049l-3.071 4.662h-.048l-3.071-4.65v7.037h-.928zm10.453 0v-8.702h6.095v.895h-5.143v2.971h4.6v.895h-4.6v3.046H91v.895h-6.155z"/><path fill-rule="nonzero" d="M2.831 14.045c.775 4.287 2.266 8.333 4.685 12.143 2.958 4.659 7.21 8.797 12.984 12.319 5.774-3.522 10.026-7.66 12.984-12.319 2.42-3.81 3.91-7.856 4.685-12.143.489-2.706.644-4.844.672-8.003C33.368 3.522 26.636 2.14 20.5 2.14c-6.137 0-12.869 1.381-18.341 3.9.028 3.16.183 5.298.672 8.004zM20.5 0C26.908 0 34.637 1.47 41 4.706c0 6.988.087 24.398-20.5 36.294C-.088 29.104 0 11.694 0 4.706 6.363 1.47 14.092 0 20.5 0z"/><path d="M20.234 27L33 11.344c-.935-.682-1.756-.2-2.208.172l-.016.001-10.644 10.076-4.01-4.392c-1.913-2.011-4.514-.477-5.122-.072L20.234 27"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="164" height="41" viewBox="0 0 164 41">
<g fill-rule="evenodd">
<path d="M129.984 22l-1.162-2.945h-5.792L121.931 22H118l6.277-15h3.509L134 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM117 16.1c0 .88-.153 1.682-.46 2.404a5.223 5.223 0 0 1-1.318 1.857c-.57.516-1.26.918-2.066 1.207-.807.289-1.703.433-2.688.433-1 0-1.9-.144-2.699-.433-.8-.29-1.477-.691-2.034-1.207a5.232 5.232 0 0 1-1.285-1.857c-.3-.722-.45-1.524-.45-2.404V7h3.64v8.81c0 .4.054.777.161 1.135.108.358.272.677.493.96.221.281.514.505.878.67.364.165.803.248 1.317.248.514 0 .953-.083 1.317-.248.365-.165.66-.389.89-.67.228-.283.392-.602.492-.96.1-.358.15-.736.15-1.135V7H117v9.099zm-16 4.673c-.733.362-1.59.658-2.57.886-.98.228-2.047.342-3.203.342-1.199 0-2.302-.181-3.31-.544-1.008-.362-1.875-.872-2.601-1.53a6.977 6.977 0 0 1-1.703-2.366c-.409-.92-.613-1.943-.613-3.07 0-1.141.208-2.175.624-3.1a6.903 6.903 0 0 1 1.723-2.367 7.71 7.71 0 0 1 2.58-1.5C92.914 7.174 93.98 7 95.121 7c1.184 0 2.284.171 3.299.513 1.015.343 1.84.802 2.474 1.38l-2.284 2.476c-.352-.39-.817-.708-1.395-.956-.579-.249-1.234-.373-1.967-.373-.635 0-1.22.111-1.756.332a4.23 4.23 0 0 0-1.395.927 4.178 4.178 0 0 0-.92 1.41 4.734 4.734 0 0 0-.328 1.78c0 .659.099 1.263.296 1.813.197.55.49 1.024.878 1.42.387.395.867.704 1.438.926.57.221 1.223.332 1.956.332.423 0 .825-.03 1.205-.09.381-.061.733-.158 1.058-.293V16h-2.855v-2.779H101v7.55zm63-6.314c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H150V7h5.422c1.06 0 2.104.124 3.135.37a7.866 7.866 0 0 1 2.753 1.23c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zm-75.23 0c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H71V7h5.422c1.06 0 2.104.124 3.135.37A7.866 7.866 0 0 1 82.31 8.6c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zM65.984 22l-1.162-2.945H59.03L57.931 22H54l6.277-15h3.509L70 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM143.855 22l-3.171-5.953h-1.202V22H136V7h5.596c.705 0 1.392.074 2.062.222.67.149 1.271.4 1.803.753a3.9 3.9 0 0 1 1.275 1.398c.318.579.476 1.3.476 2.16 0 1.018-.269 1.872-.808 2.564-.539.693-1.285 1.187-2.238 1.484L148 22h-4.145zm-.145-10.403c0-.353-.073-.639-.218-.858a1.502 1.502 0 0 0-.56-.508 2.393 2.393 0 0 0-.766-.244 5.535 5.535 0 0 0-.819-.063h-1.886v3.495h1.679c.29 0 .587-.024.891-.074.304-.05.58-.137.83-.264.248-.128.452-.311.61-.551.16-.24.239-.551.239-.933zM55 37.851v-8.702h.951v3.866h4.866V29.15h.952v8.702h-.952v-3.916h-4.866v3.916H55zM68.068 38c-2.565 0-4.288-2.076-4.288-4.5 0-2.4 1.747-4.5 4.312-4.5 2.565 0 4.288 2.076 4.288 4.5 0 2.4-1.747 4.5-4.312 4.5zm.024-.907c1.927 0 3.3-1.592 3.3-3.593 0-1.977-1.397-3.593-3.324-3.593-1.927 0-3.3 1.592-3.3 3.593 0 1.977 1.397 3.593 3.324 3.593zm6.3.758v-8.702h.963l3.07 4.749 3.072-4.749h.964v8.702h-.952v-7.049l-3.071 4.662h-.048l-3.071-4.65v7.037h-.928zm10.453 0v-8.702h6.095v.895h-5.143v2.971h4.6v.895h-4.6v3.046H91v.895h-6.155z"/>
<path fill-rule="nonzero"
d="M2.831 14.045c.775 4.287 2.266 8.333 4.685 12.143 2.958 4.659 7.21 8.797 12.984 12.319 5.774-3.522 10.026-7.66 12.984-12.319 2.42-3.81 3.91-7.856 4.685-12.143.489-2.706.644-4.844.672-8.003C33.368 3.522 26.636 2.14 20.5 2.14c-6.137 0-12.869 1.381-18.341 3.9.028 3.16.183 5.298.672 8.004zM20.5 0C26.908 0 34.637 1.47 41 4.706c0 6.988.087 24.398-20.5 36.294C-.088 29.104 0 11.694 0 4.706 6.363 1.47 14.092 0 20.5 0z"/>
<path d="M20.234 27L33 11.344c-.935-.682-1.756-.2-2.208.172l-.016.001-10.644 10.076-4.01-4.392c-1.913-2.011-4.514-.477-5.122-.072L20.234 27"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -1 +1,7 @@
<svg stroke="#84868c" fill="none" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m3 6h2 16"/><path d="m19 6v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2-2v-14m3 0v-2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="m10 11v6"/><path d="m14 11v6"/></svg>
<svg stroke="#84868c" fill="none" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m3 6h2 16"/>
<path d="m19 6v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2-2v-14m3 0v-2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
<path d="m10 11v6"/>
<path d="m14 11v6"/>
</svg>

Before

Width:  |  Height:  |  Size: 340 B

After

Width:  |  Height:  |  Size: 367 B

View File

@@ -1 +1,5 @@
<svg stroke="#84868c" fill="none" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m18 6-12 12"/><path d="m6 6 12 12"/></svg>
<svg stroke="#84868c" fill="none" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m18 6-12 12"/>
<path d="m6 6 12 12"/>
</svg>

Before

Width:  |  Height:  |  Size: 227 B

After

Width:  |  Height:  |  Size: 244 B