review fix

This commit is contained in:
Ildar Kamalov 2025-08-12 16:13:13 +03:00
parent 2ba671b408
commit e495a5585c
17 changed files with 80 additions and 272 deletions

View File

@ -40,16 +40,7 @@ module.exports = {
argsIgnorePattern: '^_',
},
],
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'import/extensions': 'off',
'class-methods-use-this': 'off',
'no-shadow': 'off',
camelcase: 'off',

View File

@ -71,7 +71,6 @@
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-import-resolver-webpack": "^0.13.10",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.1",
@ -8030,104 +8029,6 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/eslint-import-resolver-webpack": {
"version": "0.13.10",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.10.tgz",
"integrity": "sha512-ciVTEg7sA56wRMR772PyjcBRmyBMLS46xgzQZqt6cWBEKc7cK65ZSSLCTLVRu2gGtKyXUb5stwf4xxLBfERLFA==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^3.2.7",
"enhanced-resolve": "^0.9.1",
"find-root": "^1.1.0",
"hasown": "^2.0.2",
"interpret": "^1.4.0",
"is-core-module": "^2.15.1",
"is-regex": "^1.2.0",
"lodash": "^4.17.21",
"resolve": "^2.0.0-next.5",
"semver": "^5.7.2"
},
"engines": {
"node": ">= 6"
},
"peerDependencies": {
"eslint-plugin-import": ">=1.4.0",
"webpack": ">=1.11.0"
}
},
"node_modules/eslint-import-resolver-webpack/node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/eslint-import-resolver-webpack/node_modules/enhanced-resolve": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz",
"integrity": "sha512-kxpoMgrdtkXZ5h0SeraBS1iRntpTpQ3R8ussdb38+UAFnMGX5DDyJXePm+OCHOcoXvHDw7mc2erbJBpDnl7TPw==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.1.2",
"memory-fs": "^0.2.0",
"tapable": "^0.1.8"
},
"engines": {
"node": ">=0.6"
}
},
"node_modules/eslint-import-resolver-webpack/node_modules/interpret": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/eslint-import-resolver-webpack/node_modules/resolve": {
"version": "2.0.0-next.5",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
"integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/eslint-import-resolver-webpack/node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/eslint-import-resolver-webpack/node_modules/tapable": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz",
"integrity": "sha512-jX8Et4hHg57mug1/079yitEKWGB3LCwoxByLsNim89LABq8NqgiX+6iYVOsq0vX8uJHkU+DZ5fnq95f800bEsQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/eslint-module-utils": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
@ -11579,13 +11480,6 @@
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/memory-fs": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz",
"integrity": "sha512-+y4mDxU4rvXXu5UDSGCGNiesFmwCHuefGMoPCO1WYucNYj7DsLqrFaa2fXVI0H+NNiPTwwzKwspn9yTZqUGqng==",
"dev": true,
"license": "MIT"
},
"node_modules/meow": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz",
@ -16298,21 +16192,6 @@
}
}
},
"node_modules/vite-node/node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/vitest": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
@ -16516,21 +16395,6 @@
}
}
},
"node_modules/vitest/node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",

View File

@ -85,7 +85,6 @@
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-import-resolver-webpack": "^0.13.10",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.1",

View File

@ -27,10 +27,10 @@ export const Radio = <T extends number | string | boolean = string>({
{options.map((o) => (
<label
key={`${o.value}`}
htmlFor={name ? `${name}-${String(o.value)}` : String(o.value)}
htmlFor={name ? `${name}-${o.value}` : String(o.value)}
className={cn(s.radio, className)}>
<input
id={name ? `${name}-${String(o.value)}` : String(o.value)}
id={name ? `${name}-${o.value}` : String(o.value)}
type="radio"
className={s.input}
name={name}

View File

@ -3,6 +3,7 @@
width: 100%;
@media (min-width: 768px) {
max-width: 100%;
width: 720px;
}
}

View File

@ -7,7 +7,7 @@ import { Sidebar } from 'panel/common/ui/Sidebar';
import { Icons } from 'panel/common/ui/Icons';
import { Footer } from 'panel/common/ui/Footer';
import { Header } from 'panel/common/ui/Header';
import Settings from 'panel/containers/Settings';
import { Settings } from 'panel/components/Settings';
import { LocalesType } from 'panel/common/intl';
import Toasts from '../Toasts';

View File

@ -6,6 +6,8 @@ import theme from 'panel/lib/theme';
import { RoutePath } from 'panel/components/Routes/Paths';
import { Link } from 'panel/common/ui/Link';
import { setFiltersConfig } from 'panel/actions/filtering';
import { useDispatch } from 'react-redux';
import { SwitchGroup } from './SettingsGroup';
export type FormValues = {
@ -15,11 +17,12 @@ export type FormValues = {
type Props = {
initialValues: FormValues;
setFiltersConfig: (values: FormValues) => void;
processing: boolean;
};
export const FiltersConfig = ({ initialValues, setFiltersConfig, processing }: Props) => {
export const FiltersConfig = ({ initialValues, processing }: Props) => {
const dispatch = useDispatch();
const { watch, control, setValue } = useForm({
mode: 'onBlur',
defaultValues: initialValues,
@ -28,7 +31,7 @@ export const FiltersConfig = ({ initialValues, setFiltersConfig, processing }: P
const enabled = watch('enabled');
useEffect(() => {
setFiltersConfig({ ...initialValues, enabled });
dispatch(setFiltersConfig({ ...initialValues, enabled }));
}, [enabled]);
return (

View File

@ -31,7 +31,7 @@ export const IgnoredDomains = ({ control, processing, ignoreEnabled, setValue, s
onChange={(e) => setValue('ignore_enabled', e.target.checked)}
disabled={processing}>
<Controller
name={'ignored'}
name="ignored"
control={control}
render={({ field, fieldState }) => (
<Textarea
@ -71,7 +71,7 @@ export const IgnoredDomains = ({ control, processing, ignoreEnabled, setValue, s
</a>
</div>
}
placeholder={'example.com\n*.example.com\n||example.com^'}
placeholder="example.com\n*.example.com\n||example.com^"
size="large"
disabled={processing || !ignoreEnabled}
errorMessage={fieldState.error?.message}

View File

@ -4,6 +4,8 @@ import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
import intl from 'panel/common/intl';
import { HOUR } from 'panel/helpers/constants';
import { formatIntervalText } from 'panel/components/Settings/helpers';
import { useDispatch } from 'react-redux';
import { clearLogs, setLogsConfig } from 'panel/actions/queryLogs';
import { Form, FormValues } from './Form';
export type LogsConfigPayload = {
@ -22,8 +24,6 @@ type Props = {
processing: boolean;
ignored: string[];
processingClear: boolean;
setLogsConfig: (values: LogsConfigPayload) => unknown;
clearLogs: () => void;
};
export const LogsConfig = ({
@ -34,9 +34,9 @@ export const LogsConfig = ({
processing,
processingClear,
ignored,
setLogsConfig,
clearLogs,
}: Props) => {
const dispatch = useDispatch();
const [openConfirmDialog, setOpenConfirmDialog] = useState(false);
const [confirmConfig, setConfirmConfig] = useState<LogsConfigPayload | null>(null);
@ -49,7 +49,7 @@ export const LogsConfig = ({
};
const handleClearConfirm = () => {
clearLogs();
dispatch(clearLogs());
handleClose();
};
@ -69,7 +69,7 @@ export const LogsConfig = ({
return;
}
setLogsConfig(data);
dispatch(setLogsConfig(data));
};
return (
@ -91,7 +91,7 @@ export const LogsConfig = ({
<ConfirmDialog
onClose={() => setConfirmConfig(null)}
onConfirm={() => {
setLogsConfig(confirmConfig);
dispatch(setLogsConfig(confirmConfig));
setConfirmConfig(null);
}}
buttonText={intl.getMessage('settings_yes_decrease')}

View File

@ -26,10 +26,16 @@ export const RetentionCustomInput = <TFormValues extends { customInterval?: numb
placeholder,
}: Props<TFormValues>) => {
const inputRef = useRef<HTMLInputElement>(null);
const prevIntervalRef = useRef(intervalValue);
useEffect(() => {
if (intervalValue === RETENTION_CUSTOM && !processing) {
inputRef.current?.focus();
const wasCustom = prevIntervalRef.current === RETENTION_CUSTOM;
const isCustom = intervalValue === RETENTION_CUSTOM;
prevIntervalRef.current = intervalValue;
if (!wasCustom && isCustom && !processing) {
inputRef.current?.focus({ preventScroll: true });
}
}, [intervalValue, processing]);
@ -51,11 +57,7 @@ export const RetentionCustomInput = <TFormValues extends { customInterval?: numb
}}
onBlur={field.onBlur}
disabled={processing || intervals.includes(intervalValue)}
error={
intervalValue === RETENTION_CUSTOM &&
fieldState.isTouched &&
String(field.value ?? '').trim() === ''
}
error={!!fieldState.error}
min={RETENTION_RANGE.MIN}
max={RETENTION_RANGE.MAX}
/>

View File

@ -1,19 +1,21 @@
import React, { useEffect } from 'react';
import cn from 'clsx';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import intl from 'panel/common/intl';
import { Checkbox } from 'panel/common/controls/Checkbox';
import theme from 'panel/lib/theme';
import { PageLoader } from 'panel/common/ui/Loader';
import { SettingsData, StatsData, QueryLogsData, FilteringData } from 'panel/initialState';
import { RootState, SettingsData } from 'panel/initialState';
import { initSettings, toggleSetting } from 'panel/actions';
import { getStatsConfig } from 'panel/actions/stats';
import { getLogsConfig } from 'panel/actions/queryLogs';
import { getFilteringStatus } from 'panel/actions/filtering';
import { StatsConfig } from './StatsConfig/StatsConfig';
import { LogsConfig } from './LogsConfig';
import { FiltersConfig } from './FiltersConfig';
import { getSafeSearchProviderTitle } from './helpers';
import type { StatsConfigPayload } from './StatsConfig/StatsConfig';
import type { LogsConfigPayload } from './LogsConfig/LogsConfig';
import type { FormValues as FiltersFormValues } from './FiltersConfig';
import { SwitchGroup } from './SettingsGroup';
const SETTINGS = {
@ -29,49 +31,25 @@ const SETTINGS = {
},
};
type ToggleSettingArgKey = keyof typeof SETTINGS | 'safesearch';
type ToggleSettingArgValue = boolean | Record<string, boolean>;
export const Settings = () => {
const dispatch = useDispatch();
type Props = {
settings: SettingsData;
stats: StatsData;
queryLogs: QueryLogsData;
filtering: FilteringData;
initSettings: () => void;
toggleSetting: (key: ToggleSettingArgKey, value: ToggleSettingArgValue) => void;
getStatsConfig: () => void;
setStatsConfig: (config: StatsConfigPayload) => void;
resetStats: () => void;
setFiltersConfig: (values: FiltersFormValues) => void;
getFilteringStatus: () => void;
getLogsConfig: () => void;
setLogsConfig: (values: LogsConfigPayload) => void;
clearLogs: () => void;
};
const settings = useSelector((state: RootState) => state.settings, shallowEqual);
const stats = useSelector((state: RootState) => state.stats, shallowEqual);
const queryLogs = useSelector((state: RootState) => state.queryLogs, shallowEqual);
const filtering = useSelector((state: RootState) => state.filtering, shallowEqual);
export const Settings = ({
initSettings,
getStatsConfig,
getLogsConfig,
getFilteringStatus,
settings,
toggleSetting,
setStatsConfig,
resetStats,
stats,
queryLogs,
setLogsConfig,
clearLogs,
filtering,
setFiltersConfig,
}: Props) => {
useEffect(() => {
initSettings();
getStatsConfig();
getFilteringStatus();
getLogsConfig();
dispatch(initSettings());
dispatch(getStatsConfig());
dispatch(getFilteringStatus());
dispatch(getLogsConfig());
}, []);
const handleSettingToggle =
(key: keyof typeof SETTINGS) => (e: React.ChangeEvent<HTMLInputElement>) =>
dispatch(toggleSetting(key, !e.target.checked));
const renderSettings = (settingsList?: SettingsData['settingsList']) =>
settingsList
? (Object.keys(SETTINGS) as Array<keyof typeof SETTINGS>).map((key) => {
@ -84,7 +62,7 @@ export const Settings = ({
description={subtitle}
id={String(key)}
checked={enabled}
onChange={(e) => toggleSetting(key, !e.target.checked)}
onChange={handleSettingToggle(key)}
/>
</div>
);
@ -100,13 +78,30 @@ export const Settings = ({
const { enabled, ...searches } = safesearch;
type SafeSearchConfigShape = Record<string, boolean> & { enabled: boolean };
const onSafeSearchEnabledChange =
(e: React.ChangeEvent<HTMLInputElement>) => {
const payload = { ...safesearch, enabled: e.target.checked } as SafeSearchConfigShape;
dispatch(toggleSetting('safesearch', payload));
};
const onProviderChange =
(searchKey: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
const payload = {
...safesearch,
[searchKey]: e.target.checked,
} as SafeSearchConfigShape;
dispatch(toggleSetting('safesearch', payload));
};
return (
<SwitchGroup
id="safesearch"
title={intl.getMessage('settings_safe_search')}
description={intl.getMessage('settings_safe_search_desc')}
checked={enabled}
onChange={(e) => toggleSetting('safesearch', { ...safesearch, enabled: e.target.checked })}>
onChange={onSafeSearchEnabledChange}>
<div>
{Object.keys(searches).map((searchKey) => (
<div key={searchKey} className={theme.form.checkbox}>
@ -114,9 +109,7 @@ export const Settings = ({
id={searchKey}
checked={searches[searchKey]}
disabled={!enabled}
onChange={(e) => {
toggleSetting('safesearch', { ...safesearch, [searchKey]: e.target.checked });
}}>
onChange={onProviderChange(searchKey)}>
{getSafeSearchProviderTitle(searchKey)}
</Checkbox>
</div>
@ -148,7 +141,6 @@ export const Settings = ({
enabled: filtering.enabled,
}}
processing={filtering.processingSetConfig}
setFiltersConfig={setFiltersConfig}
/>
{renderSettings(settings.settingsList)}
@ -167,8 +159,6 @@ export const Settings = ({
anonymize_client_ip={queryLogs.anonymize_client_ip}
processing={queryLogs.processingSetConfig}
processingClear={queryLogs.processingClear}
setLogsConfig={setLogsConfig}
clearLogs={clearLogs}
/>
<h2 className={cn(theme.layout.subtitle, theme.title.h5, theme.title.h4_tablet)}>
@ -182,8 +172,6 @@ export const Settings = ({
enabled={stats.enabled}
processing={stats.processingSetConfig}
processingReset={stats.processingReset}
setStatsConfig={setStatsConfig}
resetStats={resetStats}
/>
</>
)}

View File

@ -5,6 +5,8 @@ import intl from 'panel/common/intl';
import { HOUR } from 'panel/helpers/constants';
import { formatIntervalText } from 'panel/components/Settings/helpers';
import { resetStats, setStatsConfig } from 'panel/actions/stats';
import { useDispatch } from 'react-redux';
import { Form, FormValues } from './Form';
export type StatsConfigPayload = {
@ -20,8 +22,6 @@ type Props = {
enabled: boolean;
processing: boolean;
processingReset: boolean;
setStatsConfig: (config: StatsConfigPayload) => void;
resetStats: () => void;
};
export const StatsConfig = ({
@ -31,9 +31,9 @@ export const StatsConfig = ({
enabled,
processing,
processingReset,
setStatsConfig,
resetStats,
}: Props) => {
const dispatch = useDispatch();
const [openClearDialog, setOpenClearDialog] = useState(false);
const [confirmConfig, setConfirmConfig] = useState<StatsConfigPayload | null>(null);
@ -46,7 +46,7 @@ export const StatsConfig = ({
};
const handleClearConfirm = () => {
resetStats();
dispatch(resetStats());
handleClose();
};
@ -66,7 +66,7 @@ export const StatsConfig = ({
return;
}
setStatsConfig(data);
dispatch(setStatsConfig(data));
};
return (
@ -88,7 +88,7 @@ export const StatsConfig = ({
<ConfirmDialog
onClose={() => setConfirmConfig(null)}
onConfirm={() => {
setStatsConfig(confirmConfig);
dispatch(setStatsConfig(confirmConfig));
setConfirmConfig(null);
}}
buttonText={intl.getMessage('settings_yes_decrease')}

View File

@ -29,7 +29,7 @@ export const getDefaultInterval = (customInterval?: number, interval?: number) =
return interval || DAY;
};
const SAFESEARCH_TITLES: Record<string, string> = {
const SAFESEARCH_TITLES = {
bing: 'Bing',
duckduckgo: 'DuckDuckGo',
ecosia: 'Ecosia',
@ -37,7 +37,7 @@ const SAFESEARCH_TITLES: Record<string, string> = {
pixabay: 'Pixabay',
yandex: 'Yandex',
youtube: 'YouTube',
};
} as const;
export const getSafeSearchProviderTitle = (key: string) => {
return SAFESEARCH_TITLES[key] ?? captitalizeWords(key);

View File

@ -1,38 +0,0 @@
import { connect } from 'react-redux';
import { initSettings, toggleSetting } from '../actions';
import { getBlockedServices, updateBlockedServices } from '../actions/services';
import { getStatsConfig, setStatsConfig, resetStats } from '../actions/stats';
import { clearLogs, getLogsConfig, setLogsConfig } from '../actions/queryLogs';
import { getFilteringStatus, setFiltersConfig } from '../actions/filtering';
import { Settings } from '../components/Settings';
const mapStateToProps = (state: any) => {
const { settings, services, stats, queryLogs, filtering } = state;
const props = {
settings,
services,
stats,
queryLogs,
filtering,
};
return props;
};
const mapDispatchToProps = {
initSettings,
toggleSetting,
getBlockedServices,
updateBlockedServices,
getStatsConfig,
setStatsConfig,
resetStats,
clearLogs,
getLogsConfig,
setLogsConfig,
getFilteringStatus,
setFiltersConfig,
};
export default connect(mapStateToProps, mapDispatchToProps)(Settings);

View File

@ -476,7 +476,7 @@ export const TOAST_TYPES = {
export const SUCCESS_TOAST_TIMEOUT = 5000;
export const ONE_SECOND_IN_MS = 1000;
export const FAILURE_TOAST_TIMEOUT = 5000;
export const FAILURE_TOAST_TIMEOUT = 10000;
export const TOAST_TIMEOUTS = {
[TOAST_TYPES.SUCCESS]: SUCCESS_TOAST_TIMEOUT,

View File

@ -5,7 +5,6 @@ import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import CopyPlugin from 'copy-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import * as url from 'url';
// eslint-disable-next-line import/extensions
import { BUILD_ENVS } from './constants.js';
// eslint-disable-next-line no-underscore-dangle

View File

@ -1,9 +1,8 @@
import { merge } from 'webpack-merge';
import yaml from 'js-yaml';
import fs from 'fs';
// eslint-disable-next-line import/extensions
import { BASE_URL } from './constants.js';
// eslint-disable-next-line import/extensions
import common from './webpack.common.js';
const ZERO_HOST = '0.0.0.0';