mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2026-08-04 15:28:58 +00:00
AGDNS-4277
This commit is contained in:
parent
1aee4f3025
commit
bac2eaae8b
74 changed files with 1350 additions and 1146 deletions
|
|
@ -31,6 +31,7 @@ module.exports = {
|
|||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
varsIgnorePattern: '^_',
|
||||
argsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
|
|
|
|||
10
client_v2/package-lock.json
generated
10
client_v2/package-lock.json
generated
|
|
@ -19,7 +19,6 @@
|
|||
"date-fns": "^4.1.0",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.19",
|
||||
"nanoid": "^5.1.0",
|
||||
"qs": "^6.14.0",
|
||||
"solid-js": "^1.9.0",
|
||||
|
|
@ -33,7 +32,6 @@
|
|||
"@solidjs/testing-library": "^0.8.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/qs": "^6.15.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
||||
|
|
@ -5271,13 +5269,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
|
||||
"integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
|
|
@ -12439,6 +12430,7 @@
|
|||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@
|
|||
"date-fns": "^4.1.0",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.19",
|
||||
"nanoid": "^5.1.0",
|
||||
"qs": "^6.14.0",
|
||||
"solid-js": "^1.9.0",
|
||||
|
|
@ -49,7 +48,6 @@
|
|||
"@solidjs/testing-library": "^0.8.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/qs": "^6.15.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
||||
|
|
|
|||
|
|
@ -8,8 +8,11 @@ import {
|
|||
auditTranslations,
|
||||
collectTranslationUsageFromFiles,
|
||||
formatAuditReport,
|
||||
formatPluralReport,
|
||||
listSourceFiles,
|
||||
loadLocaleMessages,
|
||||
loadTwoskyLocales,
|
||||
validatePlurals,
|
||||
} from './translation-audit.js';
|
||||
|
||||
export const runTranslationAudit = async ({
|
||||
|
|
@ -29,7 +32,13 @@ export const runTranslationAudit = async ({
|
|||
const usage = await collectTranslationUsageFromFiles(filePaths);
|
||||
const report = auditTranslations({ localeMessages, usage });
|
||||
|
||||
write(`${formatAuditReport(report, { rootDir })}\n`);
|
||||
write(`${formatAuditReport(report, { rootDir })}\n\n`);
|
||||
|
||||
const repoRoot = path.resolve(rootDir, '..');
|
||||
const localesDir = path.join(srcDir, '__locales');
|
||||
const supportedLocales = await loadTwoskyLocales(repoRoot);
|
||||
const pluralErrors = await validatePlurals(localesDir, supportedLocales);
|
||||
write(`${formatPluralReport(pluralErrors)}\n`);
|
||||
|
||||
return 0;
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,60 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { validator } from '@adguard/translate';
|
||||
import ts from 'typescript';
|
||||
|
||||
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx']);
|
||||
const TRANSLATION_METHODS = new Set(['getMessage', 'getPlural']);
|
||||
|
||||
/**
|
||||
* Loads the locale-to-underscore map from .twosky.json in the repo root.
|
||||
* Returns a Map where keys are hyphenated locale codes (e.g. "pt-br") and
|
||||
* values are @adguard/translate locale codes (e.g. "pt_br"), or `null` for
|
||||
* unsupported locales.
|
||||
*
|
||||
* @param {string} repoRoot - Path to the repository root (contains .twosky.json).
|
||||
* @returns {Promise<Map<string, string|null>>}
|
||||
*/
|
||||
export const loadTwoskyLocales = async (repoRoot) => {
|
||||
const twoskyPath = path.join(repoRoot, '.twosky.json');
|
||||
const raw = await fs.readFile(twoskyPath, 'utf8');
|
||||
const projects = JSON.parse(raw);
|
||||
|
||||
const homeV2 = projects.find((p) => p.project_id === 'home_v2');
|
||||
if (!homeV2) {
|
||||
throw new Error('home_v2 project not found in .twosky.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a hyphenated twosky locale code to @adguard/translate format.
|
||||
* Most become underscore (pt-br → pt_br). Special cases for locales that
|
||||
* the library handles under a parent code, or doesn't handle at all.
|
||||
*/
|
||||
const toTranslateLocale = (code) => {
|
||||
// Sinhala not in @adguard/translate plural rules table
|
||||
if (code === 'si-lk') {
|
||||
return null;
|
||||
}
|
||||
// Hong Kong & Serbian Cyrillic → parent locale
|
||||
if (code === 'zh-hk') {
|
||||
return 'zh';
|
||||
}
|
||||
if (code === 'sr-cs') {
|
||||
return 'sr';
|
||||
}
|
||||
|
||||
return code.replace(/-/g, '_');
|
||||
};
|
||||
|
||||
const map = new Map();
|
||||
for (const code of Object.keys(homeV2.languages)) {
|
||||
map.set(code, toTranslateLocale(code));
|
||||
}
|
||||
|
||||
return map;
|
||||
};
|
||||
|
||||
const isIntlMethod = (node) => {
|
||||
if (!ts.isPropertyAccessExpression(node)) {
|
||||
return false;
|
||||
|
|
@ -216,3 +265,80 @@ export const collectTranslationUsageFromFiles = async (filePaths) => {
|
|||
|
||||
return usage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates plural forms in all locale files against the @adguard/translate
|
||||
* library's plural rules (CLDR-based). Returns an array of errors grouped by
|
||||
* locale file and key.
|
||||
*
|
||||
* @param {string} localesDir - Path to __locales directory
|
||||
* @param {{write?: (chunk: string) => void}} [options]
|
||||
* @returns {Promise<Array<{file: string, key: string, value: string}>>}
|
||||
*/
|
||||
export const validatePlurals = async (localesDir, supportedLocales) => {
|
||||
const errors = [];
|
||||
const entries = await fs.readdir(localesDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile() || !entry.name.endsWith('.json')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const localeName = entry.name.replace(/\.json$/, '');
|
||||
const localeCode = supportedLocales.get(localeName);
|
||||
|
||||
// `undefined` means not in twosky list; `null` means unsupported
|
||||
if (localeCode === undefined || localeCode === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const localePath = path.join(localesDir, entry.name);
|
||||
const messages = await loadLocaleMessages(localePath);
|
||||
|
||||
for (const [key, value] of Object.entries(messages)) {
|
||||
// Only validate strings that START with "|" (plural form separator).
|
||||
// Strings containing "||" (like filter rule examples) are not plurals.
|
||||
if (typeof value !== 'string' || !value.trimStart().startsWith('|')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!validator.isPluralFormValid(value, localeCode, key)) {
|
||||
errors.push({ file: entry.name, key, value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the plural validation report.
|
||||
*
|
||||
* @param {Array<{file: string, key: string, value: string}>} errors
|
||||
* @returns {string}
|
||||
*/
|
||||
export const formatPluralReport = (errors) => {
|
||||
if (errors.length === 0) {
|
||||
return 'Plural form validation: all locales passed';
|
||||
}
|
||||
|
||||
const byFile = new Map();
|
||||
for (const err of errors) {
|
||||
const list = byFile.get(err.file) || [];
|
||||
list.push(err);
|
||||
byFile.set(err.file, list);
|
||||
}
|
||||
|
||||
const lines = [`Plural form errors: ${errors.length} keys`];
|
||||
|
||||
for (const [file, errs] of byFile) {
|
||||
lines.push(`\n ${file}:`);
|
||||
for (const { key, value } of errs) {
|
||||
const pipeCount = (value.match(/\|/g) || []).length;
|
||||
lines.push(` ${key} (${pipeCount} pipes)`);
|
||||
lines.push(` "${value.slice(0, 80)}${value.length > 80 ? '…' : ''}"`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -208,7 +208,8 @@
|
|||
"dns_blocking_mode_title": "Blocking mode",
|
||||
"dns_blocking_mode_ttl_label": "Blocked response TTL, in seconds",
|
||||
"dns_blocking_mode_ttl_placeholder": "Enter TTL",
|
||||
"dns_bootstrap_dns_desc": "Sets the DNS servers used to resolve DoH/DoT upstream resolver hostnames",
|
||||
"dns_bootstrap_dns_desc": "Sets the DNS servers used to resolve the hostnames of DoH/DoT upstream resolvers",
|
||||
"dns_bootstrap_dns_desc_2": "Enter one IP address per line. Comments must start on a new line with #",
|
||||
"dns_bootstrap_dns_label": "Bootstrap DNS servers, one per line",
|
||||
"dns_bootstrap_dns_placeholder": "IP addresses",
|
||||
"dns_bootstrap_dns_title": "Bootstrap DNS servers",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ vi.mock('panel/stores/toasts', () => ({
|
|||
addErrorToast: mocks.addErrorToast,
|
||||
}));
|
||||
|
||||
import { toggleClientBlock } from 'panel/stores/access';
|
||||
import { toggleClientBlock, setAccessList } from 'panel/stores/access';
|
||||
|
||||
describe('toggleClientBlock', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
|
@ -77,3 +77,38 @@ describe('toggleClientBlock', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAccessList', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('splits newline-delimited strings into arrays', async () => {
|
||||
await setAccessList({
|
||||
allowed_clients: '1.1.1.1\n2.2.2.2',
|
||||
disallowed_clients: '3.3.3.3',
|
||||
blocked_hosts: 'badhost.com\nevil.com',
|
||||
});
|
||||
expect(mocks.accessSet).toHaveBeenCalledWith({
|
||||
allowed_clients: ['1.1.1.1', '2.2.2.2'],
|
||||
disallowed_clients: ['3.3.3.3'],
|
||||
blocked_hosts: ['badhost.com', 'evil.com'],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles single values correctly', async () => {
|
||||
await setAccessList({ allowed_clients: '1.1.1.1' });
|
||||
expect(mocks.accessSet).toHaveBeenCalledWith({
|
||||
allowed_clients: ['1.1.1.1'],
|
||||
disallowed_clients: undefined,
|
||||
blocked_hosts: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles empty values', async () => {
|
||||
await setAccessList({ allowed_clients: '' });
|
||||
expect(mocks.accessSet).toHaveBeenCalledWith({
|
||||
allowed_clients: [],
|
||||
disallowed_clients: undefined,
|
||||
blocked_hosts: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ describe('clientForm store', () => {
|
|||
duckduckgo: false,
|
||||
yandex: false,
|
||||
pixabay: false,
|
||||
ecosia: false,
|
||||
},
|
||||
});
|
||||
expect(clientFormState.safe_search.enabled).toBe(true);
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@ describe('setTlsConfig', () => {
|
|||
await setTlsConfig({
|
||||
certificate_chain: '',
|
||||
private_key: '',
|
||||
port_https: '',
|
||||
port_dns_over_tls: '',
|
||||
port_dns_over_quic: '',
|
||||
port_https: 0,
|
||||
port_dns_over_tls: 0,
|
||||
port_dns_over_quic: 0,
|
||||
});
|
||||
const sent = mocks.tlsConfigure.mock.calls[0][0];
|
||||
expect(sent.port_https).toBe(0);
|
||||
|
|
@ -89,7 +89,7 @@ describe('setTlsConfig', () => {
|
|||
expect(encryptionState.warning_validation).toBe('');
|
||||
expect(encryptionState.subject).toBe('');
|
||||
expect(encryptionState.issuer).toBe('');
|
||||
expect(encryptionState.key_type).toBe('');
|
||||
expect(encryptionState.key_type).toBeUndefined();
|
||||
expect(encryptionState.dns_names).toBeNull();
|
||||
});
|
||||
|
||||
|
|
|
|||
119
client_v2/src/__tests__/helpers/helpers-scalar.spec.ts
Normal file
119
client_v2/src/__tests__/helpers/helpers-scalar.spec.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
msToSeconds,
|
||||
msToMinutes,
|
||||
msToHours,
|
||||
secondsToMilliseconds,
|
||||
splitByNewLine,
|
||||
trimLinesAndRemoveEmpty,
|
||||
normalizeRulesTextarea,
|
||||
captitalizeWords,
|
||||
getWebAddress,
|
||||
getInterfaceIp,
|
||||
isIpInCidr,
|
||||
parseSubnetMask,
|
||||
subnetMaskToBitMask,
|
||||
} from '../../helpers/helpers';
|
||||
|
||||
describe('ms helpers', () => {
|
||||
it('converts ms -> seconds', () => {
|
||||
expect(msToSeconds(1500)).toBe(1);
|
||||
});
|
||||
it('converts ms -> minutes', () => {
|
||||
expect(msToMinutes(120_000)).toBe(2);
|
||||
});
|
||||
it('converts ms -> hours', () => {
|
||||
expect(msToHours(3_600_000)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('secondsToMilliseconds', () => {
|
||||
it('multiplies by 1000', () => {
|
||||
expect(secondsToMilliseconds(3)).toBe(3000);
|
||||
});
|
||||
it('returns falsy input as-is', () => {
|
||||
expect(secondsToMilliseconds(0)).toBe(0);
|
||||
// The current implementation returns `seconds` unchanged if falsy
|
||||
expect(secondsToMilliseconds(undefined as unknown as number)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitByNewLine', () => {
|
||||
it('splits and removes empty lines', () => {
|
||||
expect(splitByNewLine('a\nb\n\nc')).toStrictEqual(['a', 'b', 'c']);
|
||||
});
|
||||
it('returns [] for falsy input', () => {
|
||||
expect(splitByNewLine('')).toStrictEqual([]);
|
||||
expect(splitByNewLine(undefined as unknown as string)).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trimLinesAndRemoveEmpty', () => {
|
||||
it('trims lines', () => {
|
||||
expect(trimLinesAndRemoveEmpty(' a \n\n b ')).toBe('a\nb');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeRulesTextarea', () => {
|
||||
it('strips leading newlines and collapses repeated blank lines', () => {
|
||||
expect(normalizeRulesTextarea('\na\n\nb')).toBe('a\nb');
|
||||
});
|
||||
});
|
||||
|
||||
describe('captitalizeWords', () => {
|
||||
it('capitalizes each word splitting by space, dash, or underscore', () => {
|
||||
expect(captitalizeWords('safe_browsing mode-test')).toBe('Safe Browsing Mode Test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWebAddress', () => {
|
||||
it('builds http url omitting standard port 80', () => {
|
||||
expect(getWebAddress('192.168.1.1', 80)).toBe('http://192.168.1.1');
|
||||
});
|
||||
it('appends non-standard port', () => {
|
||||
expect(getWebAddress('192.168.1.1', 8080)).toBe('http://192.168.1.1:8080');
|
||||
});
|
||||
it('brackets IPv6 with zone encoding', () => {
|
||||
expect(getWebAddress('fe80::1%eth0', 80)).toBe('http://[fe80::1%25eth0]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInterfaceIp', () => {
|
||||
it('prefers IPv4 over IPv6', () => {
|
||||
expect(getInterfaceIp({ ip_addresses: ['10.0.0.1', 'fe80::1'] })).toBe('10.0.0.1');
|
||||
});
|
||||
it('skips IPv6 link-local when IPv4 present', () => {
|
||||
expect(getInterfaceIp({ ip_addresses: ['192.168.1.1', 'fe80::1'] })).toBe('192.168.1.1');
|
||||
});
|
||||
it('falls back to IPv6 global without zone', () => {
|
||||
expect(getInterfaceIp({ ip_addresses: ['2001:db8::1'] })).toBe('2001:db8::1');
|
||||
});
|
||||
it('returns undefined when no addresses', () => {
|
||||
expect(getInterfaceIp({ ip_addresses: [] })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isIpInCidr', () => {
|
||||
it('matches IP inside CIDR', () => {
|
||||
expect(isIpInCidr('192.168.1.5', '192.168.1.0/24')).toBe(true);
|
||||
});
|
||||
it('rejects IP outside CIDR', () => {
|
||||
expect(isIpInCidr('10.0.0.1', '192.168.1.0/24')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseSubnetMask', () => {
|
||||
it('returns prefix length for valid mask', () => {
|
||||
expect(parseSubnetMask('255.255.255.0')).toBe(24);
|
||||
});
|
||||
it('returns null for invalid mask string', () => {
|
||||
expect(parseSubnetMask('not-a-mask')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('subnetMaskToBitMask', () => {
|
||||
it('computes prefix length from dotted mask', () => {
|
||||
expect(subnetMaskToBitMask('255.255.255.0')).toBe(24);
|
||||
});
|
||||
});
|
||||
199
client_v2/src/__tests__/helpers/helpers-stats.spec.ts
Normal file
199
client_v2/src/__tests__/helpers/helpers-stats.spec.ts
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
normalizeTopStats,
|
||||
addClientInfo,
|
||||
normalizeTopClients,
|
||||
normalizeFilters,
|
||||
normalizeFilteringStatus,
|
||||
getParamsForClientsSearch,
|
||||
checkFiltered,
|
||||
checkBlockedService,
|
||||
getPathWithQueryString,
|
||||
getSpecialFilterName,
|
||||
getServiceName,
|
||||
normalizeWhois,
|
||||
normalizeLogs,
|
||||
} from '../../helpers/helpers';
|
||||
import type { ClientsFindEntry } from '../../api/model/clientsFindEntry';
|
||||
import type { FilteringReason } from '../../api/model/filteringReason';
|
||||
|
||||
describe('normalizeTopStats', () => {
|
||||
it('converts {name -> count} objects to {name, count} array', () => {
|
||||
expect(normalizeTopStats([{ '192.168.1.1': 42 }, { 'example.com': 5 }])).toStrictEqual([
|
||||
{ name: '192.168.1.1', count: 42 },
|
||||
{ name: 'example.com', count: 5 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addClientInfo', () => {
|
||||
it('resolves client info by param key', () => {
|
||||
const data = [{ name: '192.168.1.1', count: 1 }];
|
||||
const clients: ClientsFindEntry[] = [{ '192.168.1.1': { name: 'MyPhone' } }];
|
||||
expect(addClientInfo(data, clients, 'name')).toStrictEqual([
|
||||
{ name: '192.168.1.1', count: 1, info: { name: 'MyPhone' } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeTopClients', () => {
|
||||
it('splits into auto/configured by name and info', () => {
|
||||
const r = normalizeTopClients([
|
||||
{ name: '192.168.1.1', count: 7, info: { name: 'MyPhone' } },
|
||||
]);
|
||||
expect(r.auto).toStrictEqual({ '192.168.1.1': 7 });
|
||||
expect(r.configured).toStrictEqual({ MyPhone: 7 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeFilters', () => {
|
||||
it('maps snake_case API fields to camelCase with defaults', () => {
|
||||
expect(
|
||||
normalizeFilters([
|
||||
{
|
||||
id: 1,
|
||||
url: 'http://example.com/list.txt',
|
||||
enabled: true,
|
||||
last_updated: '2024-01-01',
|
||||
name: 'My List',
|
||||
rules_count: 100,
|
||||
},
|
||||
]),
|
||||
).toStrictEqual([
|
||||
{
|
||||
id: 1,
|
||||
url: 'http://example.com/list.txt',
|
||||
enabled: true,
|
||||
lastUpdated: '2024-01-01',
|
||||
name: 'My List',
|
||||
rulesCount: 100,
|
||||
},
|
||||
]);
|
||||
});
|
||||
it('returns [] for falsy input', () => {
|
||||
expect(normalizeFilters(undefined)).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeFilteringStatus', () => {
|
||||
it('normalizes full status with user_rules', () => {
|
||||
const r = normalizeFilteringStatus({
|
||||
enabled: true,
|
||||
filters: [],
|
||||
whitelist_filters: [],
|
||||
user_rules: ['rule1', 'rule2'],
|
||||
interval: 24,
|
||||
});
|
||||
expect(r.enabled).toBe(true);
|
||||
expect(r.interval).toBe(24);
|
||||
expect(r.userRules).toBe('rule1\nrule2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getParamsForClientsSearch', () => {
|
||||
it('collects unique client ids from TopStat[]', () => {
|
||||
expect(
|
||||
getParamsForClientsSearch(
|
||||
[
|
||||
{ name: 'client-a', count: 1 },
|
||||
{ name: 'client-b', count: 2 },
|
||||
],
|
||||
'name',
|
||||
),
|
||||
).toStrictEqual({ clients: [{ id: 'client-a' }, { id: 'client-b' }] });
|
||||
});
|
||||
it('includes additional param when provided', () => {
|
||||
const r = getParamsForClientsSearch([{ name: 'a', count: 1 }], 'name', 'count');
|
||||
expect(r.clients).toStrictEqual([{ id: 'a' }, { id: 1 }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkFiltered / checkBlockedService', () => {
|
||||
it('checkFiltered returns true for Filtered* reasons', () => {
|
||||
expect(checkFiltered('FilteredBlackList' as FilteringReason)).toBe(true);
|
||||
});
|
||||
it('checkFiltered returns false for NotFiltered* reasons', () => {
|
||||
expect(checkFiltered('NotFilteredNotFound' as FilteringReason)).toBe(false);
|
||||
});
|
||||
it('checkBlockedService returns true for FilteredBlockedService', () => {
|
||||
expect(checkBlockedService('FilteredBlockedService' as FilteringReason)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPathWithQueryString', () => {
|
||||
it('serializes params, skips empty/undefined values, repeats arrays', () => {
|
||||
const r = getPathWithQueryString('/endpoint', {
|
||||
a: '1',
|
||||
b: '',
|
||||
c: undefined,
|
||||
d: ['x', 'y'],
|
||||
});
|
||||
expect(r).toBe('/endpoint?a=1&d=x&d=y');
|
||||
});
|
||||
it('handles null params gracefully', () => {
|
||||
const r = getPathWithQueryString('/p', { a: '1', b: null });
|
||||
expect(r).toBe('/p?a=1');
|
||||
});
|
||||
it('handles undefined params arg', () => {
|
||||
expect(getPathWithQueryString('/p', undefined)).toBe('/p?');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSpecialFilterName', () => {
|
||||
it('returns localized name for known special filter IDs', () => {
|
||||
expect(typeof getSpecialFilterName(0)).toBe('string');
|
||||
expect(typeof getSpecialFilterName(-1)).toBe('string');
|
||||
expect(typeof getSpecialFilterName(-5)).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getServiceName', () => {
|
||||
it('returns name for matching service id', () => {
|
||||
expect(getServiceName([{ id: 'svc1', name: 'My Service' }], 'svc1')).toBe('My Service');
|
||||
});
|
||||
it('returns undefined for unknown id', () => {
|
||||
expect(getServiceName([{ id: 'svc1', name: 'My Service' }], 'svc-unknown')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeWhois', () => {
|
||||
it('derives location from city and country', () => {
|
||||
expect(normalizeWhois({ city: 'NY', country: 'US', orgname: 'Example' })).toMatchObject({
|
||||
location: 'US, NY',
|
||||
orgname: 'Example',
|
||||
});
|
||||
});
|
||||
it('uses only country when city absent', () => {
|
||||
expect(normalizeWhois({ country: 'DE', orgname: 'Org' })).toMatchObject({
|
||||
location: 'DE',
|
||||
});
|
||||
});
|
||||
it('returns placeholder defaults for empty whois', () => {
|
||||
expect(normalizeWhois({})).toMatchObject({
|
||||
location: 'New York, US',
|
||||
orgname: 'Example Organization',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeLogs', () => {
|
||||
it('maps query log item to normalized shape', () => {
|
||||
const [item] = normalizeLogs([
|
||||
{
|
||||
time: '2024-01-01T00:00:00Z',
|
||||
question: {
|
||||
name: 'example.com',
|
||||
unicode_name: 'example.com',
|
||||
type: 'A',
|
||||
},
|
||||
answer: [{ value: '1.2.3.4', type: 'A', ttl: 60 }],
|
||||
status: 'processed',
|
||||
},
|
||||
]);
|
||||
expect(item.domain).toBe('example.com');
|
||||
expect(item.unicodeName).toBe('example.com');
|
||||
expect(item.type).toBe('A');
|
||||
expect(item.response).toStrictEqual([{ value: '1.2.3.4', type: 'A', ttl: 60 }]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
vi.mock('panel/common/intl', () => ({
|
||||
default: {
|
||||
getMessage: vi.fn((key: string, values?: Record<string, string | number>) => {
|
||||
if (key === 'form_error_format_line') {
|
||||
return `Invalid format on line ${values?.line}`;
|
||||
}
|
||||
if (key === 'form_error_format_lines') {
|
||||
return `Invalid format on lines ${values?.lines}`;
|
||||
}
|
||||
if (key === 'form_error_format') {
|
||||
return 'Invalid format';
|
||||
}
|
||||
return key;
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
import { validateDomainsPerLine } from 'panel/helpers/validators';
|
||||
|
||||
describe('validateDomainsPerLine', () => {
|
||||
it('returns undefined for empty string', () => {
|
||||
expect(validateDomainsPerLine('')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for plain domain', () => {
|
||||
expect(validateDomainsPerLine('example.org')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for wildcard domain', () => {
|
||||
expect(validateDomainsPerLine('*.example.org')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for AdGuard URL filter rule', () => {
|
||||
expect(validateDomainsPerLine('||example.org^')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for regex pattern', () => {
|
||||
expect(validateDomainsPerLine('/regex.pattern/')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for comment line', () => {
|
||||
expect(validateDomainsPerLine('# this is a comment')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects !-prefixed filter rule that would otherwise pass dot check', () => {
|
||||
expect(validateDomainsPerLine('! ||example.org^')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('rejects only ! lines as invalid', () => {
|
||||
expect(validateDomainsPerLine('! first\n! second')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns undefined for mixed valid lines with comments', () => {
|
||||
expect(
|
||||
validateDomainsPerLine('# comment\nexample.org\n||ads.example.org^'),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns "Invalid format" for entry without dot', () => {
|
||||
expect(validateDomainsPerLine('notadomain')).toBe('Invalid format');
|
||||
});
|
||||
|
||||
it('returns "Invalid format on line N" when specific line has no dot', () => {
|
||||
expect(validateDomainsPerLine('example.org\nnodot')).toBe('Invalid format on line 2');
|
||||
});
|
||||
|
||||
it('returns "Invalid format on lines N, M" when multiple lines invalid', () => {
|
||||
expect(validateDomainsPerLine('nodot1\nexample.org\nnodot2')).toBe(
|
||||
'Invalid format on lines 1, 3',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns "Invalid format" for single invalid line with trailing newline', () => {
|
||||
expect(validateDomainsPerLine('notadomain\n')).toBe('Invalid format');
|
||||
});
|
||||
|
||||
it('returns "Invalid format" for single invalid line with leading newline', () => {
|
||||
expect(validateDomainsPerLine('\nnotadomain')).toBe('Invalid format');
|
||||
});
|
||||
|
||||
it('returns "Invalid format on lines 1, 2" when both lines invalid', () => {
|
||||
expect(validateDomainsPerLine('nodot1\nnodot2')).toBe('Invalid format on lines 1, 2');
|
||||
});
|
||||
|
||||
it('returns "Invalid format on line 2" when second line invalid in multi-content input', () => {
|
||||
expect(validateDomainsPerLine('example.org\nnodot')).toBe('Invalid format on line 2');
|
||||
});
|
||||
|
||||
it('returns undefined for all-blank input', () => {
|
||||
expect(validateDomainsPerLine('\n\n')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('handles blank line between two invalid lines', () => {
|
||||
expect(validateDomainsPerLine('nodot1\n\nnodot2')).toBe('Invalid format on lines 1, 3');
|
||||
});
|
||||
|
||||
it('returns "Invalid format" for comment-then-invalid (one content line)', () => {
|
||||
expect(validateDomainsPerLine('# comment\nnotadomain')).toBe('Invalid format');
|
||||
});
|
||||
|
||||
it('returns "Invalid format" for invalid-then-comment (one content line)', () => {
|
||||
expect(validateDomainsPerLine('notadomain\n# comment')).toBe('Invalid format');
|
||||
});
|
||||
});
|
||||
|
|
@ -113,4 +113,34 @@ describe('queryLogs store', () => {
|
|||
expect(queryLog).toHaveBeenCalledTimes(2);
|
||||
expect(queryLogsState.processingGetLogs).toBe(false);
|
||||
});
|
||||
|
||||
it('always sends limit=20 to prevent loading all records at once', async () => {
|
||||
(queryLog as any).mockReset();
|
||||
(queryLog as any)
|
||||
.mockResolvedValueOnce({
|
||||
data: Array.from({ length: 20 }, () => ({ reason: 'Rewrite', question: {} })),
|
||||
oldest: 'cursor1',
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: [{ reason: 'Rewrite', question: {} }],
|
||||
oldest: '',
|
||||
});
|
||||
|
||||
await setFilteredLogs({ search: '', status: 'rewritten', reason: 'all' });
|
||||
|
||||
for (const call of (queryLog as any).mock.calls) {
|
||||
expect(call[0]).toHaveProperty('limit', 20);
|
||||
}
|
||||
expect(queryLog).not.toHaveBeenCalledWith(expect.not.objectContaining({ limit: 20 }));
|
||||
|
||||
(queryLog as any).mockReset();
|
||||
(queryLog as any).mockResolvedValueOnce({
|
||||
data: [{ reason: 'Rewrite', question: {} }],
|
||||
oldest: '',
|
||||
});
|
||||
|
||||
await getAdditionalLogs();
|
||||
|
||||
expect(queryLog).toHaveBeenCalledWith(expect.objectContaining({ limit: 20 }));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ export interface DNSConfig {
|
|||
* @minimum 0
|
||||
* @maximum 32
|
||||
*/
|
||||
ratelimit_subnet_subnet_len_ipv4?: number;
|
||||
ratelimit_subnet_len_ipv4?: number;
|
||||
/**
|
||||
* Length of the subnet mask for IPv6 addresses.
|
||||
* @minimum 0
|
||||
* @maximum 128
|
||||
*/
|
||||
ratelimit_subnet_subnet_len_ipv6?: number;
|
||||
ratelimit_subnet_len_ipv6?: number;
|
||||
/** List of IP addresses excluded from rate limiting. */
|
||||
ratelimit_whitelist?: string[];
|
||||
blocking_mode?: DNSConfigBlockingMode;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export type ParentalStatus200 = {
|
||||
enable?: boolean;
|
||||
enabled?: boolean;
|
||||
sensitivity?: number;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createSignal } from 'solid-js';
|
||||
|
||||
import { I18nInterface, translate } from '@adguard/translate';
|
||||
import { I18nInterface, Locale, translate } from '@adguard/translate';
|
||||
import { BASE_LOCALE } from 'panel/helpers/twosky';
|
||||
|
||||
import en from 'panel/__locales/en.json';
|
||||
|
|
@ -38,6 +38,19 @@ const LOCALES = {
|
|||
|
||||
const messages: LocalesTypes = LOCALES;
|
||||
|
||||
/**
|
||||
* Converts a hyphenated twosky locale code to the underscore format that
|
||||
* {@link https://github.com/AdguardTeam/translate @adguard/translate}
|
||||
* expects for plural-form lookups (e.g. pt-br → pt_br).
|
||||
*/
|
||||
const toTranslateLocale = (code: string): Locale => {
|
||||
// zh-hk / sr-cs → parent locale
|
||||
if (code === 'zh-hk') return 'zh' as Locale;
|
||||
if (code === 'sr-cs') return 'sr' as Locale;
|
||||
|
||||
return code.replace(/-/g, '_') as Locale;
|
||||
};
|
||||
|
||||
const resolveLanguage = (lng: string): LocalesType => {
|
||||
const l = lng.toLowerCase();
|
||||
|
||||
|
|
@ -74,7 +87,7 @@ export const i18n = (lang: LocalesType) => {
|
|||
const resolved = resolveLanguage(lang);
|
||||
return {
|
||||
getMessage: (key: string) => messages[resolved]?.[key] || '',
|
||||
getUILanguage: () => resolved,
|
||||
getUILanguage: () => toTranslateLocale(resolved),
|
||||
getBaseMessage: (key: string) => messages.en![key] || key,
|
||||
getBaseUILanguage: () => BASE_LOCALE as LocalesType,
|
||||
};
|
||||
|
|
@ -82,7 +95,7 @@ export const i18n = (lang: LocalesType) => {
|
|||
|
||||
const detectedLanguage = ((typeof window !== 'undefined' &&
|
||||
typeof localStorage !== 'undefined' &&
|
||||
LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.LANGUAGE)) ||
|
||||
LocalStorageHelper.getItem<string>(LOCAL_STORAGE_KEYS.LANGUAGE)) ||
|
||||
(typeof navigator !== 'undefined' && (navigator.language as string)) ||
|
||||
BASE_LOCALE) as LocalesType;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import cn from 'clsx';
|
|||
import theme from 'panel/lib/theme';
|
||||
import { Dropdown } from 'panel/common/ui/Dropdown';
|
||||
import { Icon } from 'panel/common/ui/Icon';
|
||||
import intl, { LocalesType } from 'panel/common/intl';
|
||||
import intl from 'panel/common/intl';
|
||||
|
||||
import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from 'panel/helpers/localStorageHelper';
|
||||
import { LanguageDropdown } from '../LanguageDropdown/LanguageDropdown';
|
||||
|
|
@ -19,6 +19,8 @@ import {
|
|||
import { dashboardState } from 'panel/stores/dashboard';
|
||||
|
||||
import s from './styles.module.pcss';
|
||||
import { Lang } from 'panel/api/model/lang';
|
||||
import { ProfileInfoTheme } from 'panel/api/model/profileInfoTheme';
|
||||
|
||||
export const Footer = () => {
|
||||
const currentTheme = () => dashboardState.theme || THEMES.auto;
|
||||
|
|
@ -50,7 +52,7 @@ export const Footer = () => {
|
|||
return 'theme_light';
|
||||
};
|
||||
|
||||
const changeLanguage = async (newLang: LocalesType) => {
|
||||
const changeLanguage = async (newLang: Lang) => {
|
||||
setHtmlLangAttr(newLang);
|
||||
try {
|
||||
await changeLanguageAction(newLang);
|
||||
|
|
@ -61,7 +63,7 @@ export const Footer = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const onThemeChange = (value: string) => {
|
||||
const onThemeChange = (value: ProfileInfoTheme) => {
|
||||
if (isLoggedIn()) {
|
||||
changeTheme(value);
|
||||
} else {
|
||||
|
|
@ -127,7 +129,7 @@ export const Footer = () => {
|
|||
onOpenChange={setThemeDropdownOpen}
|
||||
menu={
|
||||
<div class={theme.dropdown.menu}>
|
||||
<For each={Object.values(THEMES)}>
|
||||
<For each={Object.values(THEMES) as ProfileInfoTheme[]}>
|
||||
{(v) => (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -163,7 +165,7 @@ export const Footer = () => {
|
|||
value={currentLanguage()}
|
||||
languages={LANGUAGES}
|
||||
languageNames={LANGUAGE_NAMES}
|
||||
onChange={(lang: string) => changeLanguage(lang as LocalesType)}
|
||||
onChange={(lang: Lang) => changeLanguage(lang)}
|
||||
class={s.dropdown}
|
||||
position="bottomRight"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -420,7 +420,7 @@ type Props = {
|
|||
|
||||
export const Guide = (props: Props) => {
|
||||
const serverName = () => encryptionState.server_name;
|
||||
const portHttps = () => encryptionState.port_https;
|
||||
const portHttps = () => Number(encryptionState.port_https) || 0;
|
||||
|
||||
const [activeTabLabel, setActiveTabLabel] = createSignal('Router');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { type JSX } from 'solid-js';
|
||||
import { Logo } from 'panel/common/ui/Sidebar';
|
||||
import intl, { type LocalesType } from 'panel/common/intl';
|
||||
import intl from 'panel/common/intl';
|
||||
import { LanguageDropdown } from 'panel/common/ui/LanguageDropdown/LanguageDropdown';
|
||||
import { setHtmlLangAttr } from 'panel/helpers/helpers';
|
||||
import { changeLanguage as changeLanguageAction, dashboardState } from 'panel/stores/dashboard';
|
||||
|
|
@ -8,6 +8,7 @@ import { changeLanguage as changeLanguageAction, dashboardState } from 'panel/st
|
|||
import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from 'panel/helpers/localStorageHelper';
|
||||
import { LANGUAGES, LANGUAGE_NAMES } from 'panel/helpers/twosky';
|
||||
import styles from './PublicHeader.module.pcss';
|
||||
import { Lang } from 'panel/api/model/lang';
|
||||
|
||||
type Props = {
|
||||
dropdownClass?: string;
|
||||
|
|
@ -17,7 +18,7 @@ type Props = {
|
|||
};
|
||||
|
||||
export const PublicHeader = (props: Props) => {
|
||||
const changeLanguage = async (newLang: LocalesType) => {
|
||||
const changeLanguage = async (newLang: Lang) => {
|
||||
setHtmlLangAttr(newLang);
|
||||
|
||||
if (props.useLocalLanguage) {
|
||||
|
|
@ -49,7 +50,7 @@ export const PublicHeader = (props: Props) => {
|
|||
value={currentLanguage()}
|
||||
languages={LANGUAGES}
|
||||
languageNames={LANGUAGE_NAMES}
|
||||
onChange={changeLanguage}
|
||||
onChange={(lang: Lang) => changeLanguage(lang)}
|
||||
class={props.dropdownClass}
|
||||
position={props.dropdownPosition ?? 'bottomRight'}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
|
|||
import { PageLoader } from 'panel/common/ui/Loader';
|
||||
import { Select } from 'panel/common/controls/Select';
|
||||
import { updateClientFormField, clientFormState } from 'panel/stores/clientForm';
|
||||
import type { ClientFormState } from 'panel/initialState';
|
||||
import { getBlockedServices, updateBlockedServices, servicesState } from 'panel/stores/services';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
|
|
@ -47,7 +48,7 @@ export const InactivitySchedule = (props: Props) => {
|
|||
const schedule = createMemo<ScheduleData | undefined>(() => {
|
||||
return props.clientScope
|
||||
? (clientFormState.blocked_services_schedule as unknown as ScheduleData)
|
||||
: servicesState.list?.schedule;
|
||||
: (servicesState.list?.schedule as ScheduleData | undefined);
|
||||
});
|
||||
|
||||
const currentTimezone = () => schedule()?.time_zone;
|
||||
|
|
@ -108,7 +109,11 @@ export const InactivitySchedule = (props: Props) => {
|
|||
}
|
||||
});
|
||||
if (props.clientScope) {
|
||||
updateClientFormField('blocked_services_schedule', newSchedule, true);
|
||||
updateClientFormField(
|
||||
'blocked_services_schedule',
|
||||
newSchedule as ClientFormState['blocked_services_schedule'],
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
updateBlockedServices({ ids: servicesState.list?.ids || [], schedule: newSchedule });
|
||||
}
|
||||
|
|
@ -124,7 +129,11 @@ export const InactivitySchedule = (props: Props) => {
|
|||
});
|
||||
newSchedule[day] = { start, end };
|
||||
if (props.clientScope) {
|
||||
updateClientFormField('blocked_services_schedule', newSchedule, true);
|
||||
updateClientFormField(
|
||||
'blocked_services_schedule',
|
||||
newSchedule as ClientFormState['blocked_services_schedule'],
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
updateBlockedServices({ ids: servicesState.list?.ids || [], schedule: newSchedule });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export const Clients = () => {
|
|||
initClientForm(buildFormPayload(client));
|
||||
navigate(
|
||||
linkPathBuilder(RoutePath.ClientsEdit, {
|
||||
clientName: encodeURIComponent(client.name),
|
||||
clientName: encodeURIComponent(client.name ?? ''),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
@ -79,7 +79,7 @@ export const Clients = () => {
|
|||
const serviceMap = createMemo(() => {
|
||||
const map = new Map<string, WebService>();
|
||||
(servicesState.allServices || []).forEach((svc) => {
|
||||
map.set(svc.id, svc);
|
||||
map.set(svc.id, svc as WebService);
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ type Props = {
|
|||
|
||||
export const PersistentClientsTable = (props: Props) => {
|
||||
const pageSize = createMemo(
|
||||
() => LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.CLIENTS_PAGE_SIZE) || undefined,
|
||||
() => LocalStorageHelper.getItem<number>(LOCAL_STORAGE_KEYS.CLIENTS_PAGE_SIZE) || undefined,
|
||||
);
|
||||
|
||||
const handleCopy = (text: string) => {
|
||||
|
|
@ -51,10 +51,11 @@ export const PersistentClientsTable = (props: Props) => {
|
|||
text: intl.getMessage('client_identifier'),
|
||||
className: s.headerCell,
|
||||
},
|
||||
accessor: (row: Client) => row.ids.filter((id) => id.trim() !== '').join(','),
|
||||
accessor: (row: Client) =>
|
||||
(row.ids ?? []).filter((id) => id.trim() !== '').join(','),
|
||||
sortable: true,
|
||||
render: (_value: string, row: Client) => {
|
||||
const { ids } = row;
|
||||
const ids = row.ids ?? [];
|
||||
// Filter out empty strings — the backend may return trailing empty
|
||||
// entries that would inflate hiddenCount and cause a spurious comma.
|
||||
const nonEmpty = ids.filter((id) => id.trim() !== '');
|
||||
|
|
@ -196,7 +197,7 @@ export const PersistentClientsTable = (props: Props) => {
|
|||
text: intl.getMessage('upstreams'),
|
||||
className: s.headerCell,
|
||||
},
|
||||
accessor: (row: Client) => row.upstreams.length > 0,
|
||||
accessor: (row: Client) => (row.upstreams ?? []).length > 0,
|
||||
sortable: true,
|
||||
render: (_value: boolean, row: Client) => (
|
||||
<div class={theme.table.cell}>
|
||||
|
|
@ -204,7 +205,7 @@ export const PersistentClientsTable = (props: Props) => {
|
|||
|
||||
<div class={theme.table.cellValueText}>
|
||||
<span class={theme.common.textOverflow}>
|
||||
{row.upstreams.length > 0
|
||||
{(row.upstreams ?? []).length > 0
|
||||
? intl.getMessage('settings_custom')
|
||||
: intl.getMessage('settings_global')}
|
||||
</span>
|
||||
|
|
@ -218,10 +219,10 @@ export const PersistentClientsTable = (props: Props) => {
|
|||
text: intl.getMessage('tags_title'),
|
||||
className: s.headerCell,
|
||||
},
|
||||
accessor: (row: Client) => row.tags.join(','),
|
||||
accessor: (row: Client) => (row.tags ?? []).join(','),
|
||||
sortable: true,
|
||||
render: (_value: string, row: Client) => (
|
||||
<TagCell tags={row.tags} onCopy={handleCopy} />
|
||||
<TagCell tags={row.tags ?? []} onCopy={handleCopy} />
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
@ -230,7 +231,8 @@ export const PersistentClientsTable = (props: Props) => {
|
|||
text: intl.getMessage('requests_table_header'),
|
||||
className: s.headerCell,
|
||||
},
|
||||
accessor: (row: Client) => props.normalizedTopClients?.configured[row.name] || 0,
|
||||
accessor: (row: Client) =>
|
||||
props.normalizedTopClients?.configured[row.name ?? ''] || 0,
|
||||
sortable: true,
|
||||
render: (_value: unknown, row: Client) => (
|
||||
<div class={theme.table.cell}>
|
||||
|
|
@ -241,7 +243,7 @@ export const PersistentClientsTable = (props: Props) => {
|
|||
<div class={theme.table.cellValueText}>
|
||||
<span class={theme.common.textOverflow}>
|
||||
{(
|
||||
props.normalizedTopClients?.configured[row.name] || 0
|
||||
props.normalizedTopClients?.configured[row.name ?? ''] || 0
|
||||
).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -278,7 +280,7 @@ export const PersistentClientsTable = (props: Props) => {
|
|||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onDelete(row.name)}
|
||||
onClick={() => props.onDelete(row.name ?? '')}
|
||||
disabled={props.deleteDisabled}
|
||||
class={cn(theme.table.action, theme.table.action_danger)}
|
||||
title={intl.getMessage('delete_table_action')}
|
||||
|
|
@ -312,7 +314,7 @@ export const PersistentClientsTable = (props: Props) => {
|
|||
loading={props.loading}
|
||||
pageSize={pageSize()}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
getRowId={(row) => row.name}
|
||||
getRowId={(row) => row.name ?? ''}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ type Props = {
|
|||
|
||||
export const RuntimeClientsTable = (props: Props) => {
|
||||
const pageSize = createMemo(
|
||||
() => LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.AUTO_CLIENTS_PAGE_SIZE) || undefined,
|
||||
() =>
|
||||
LocalStorageHelper.getItem<number>(LOCAL_STORAGE_KEYS.AUTO_CLIENTS_PAGE_SIZE) ||
|
||||
undefined,
|
||||
);
|
||||
|
||||
const columns = createMemo<TableColumn<AutoClient>[]>(() => [
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ export const Dashboard = () => {
|
|||
/>
|
||||
|
||||
<TopClients
|
||||
topClients={statsState.topClients}
|
||||
topClients={statsState.topClients as any[]}
|
||||
numDnsQueries={statsState.numDnsQueries}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import theme from 'panel/lib/theme';
|
|||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Input } from 'panel/common/controls/Input';
|
||||
import { dhcpState } from 'panel/stores/dhcp';
|
||||
import { calculateDhcpPlaceholdersIpv4 } from 'panel/helpers/helpers';
|
||||
import {
|
||||
validateIpv4,
|
||||
validateIpv4RangeEnd,
|
||||
|
|
@ -60,6 +61,21 @@ export const DhcpV4Modal = (props: Props) => {
|
|||
}
|
||||
});
|
||||
|
||||
const v4Placeholders = createMemo(() => {
|
||||
const iface = dhcpState.interfaces?.[props.selectedInterface()];
|
||||
const firstIpv4 = iface?.ipv4_addresses?.[0];
|
||||
if (firstIpv4) {
|
||||
return calculateDhcpPlaceholdersIpv4(firstIpv4, iface.gateway_ip);
|
||||
}
|
||||
return {
|
||||
gateway_ip: '192.168.1.1',
|
||||
subnet_mask: '255.255.255.0',
|
||||
range_start: '192.168.1.100',
|
||||
range_end: '192.168.1.200',
|
||||
lease_duration: '86400',
|
||||
};
|
||||
});
|
||||
|
||||
const hasIpv4 = createMemo(
|
||||
() =>
|
||||
!!(
|
||||
|
|
@ -167,7 +183,7 @@ export const DhcpV4Modal = (props: Props) => {
|
|||
onBlur={onGatewayBlur}
|
||||
id="v4_gateway_ip"
|
||||
label={intl.getMessage('dhcp_form_gateway_address')}
|
||||
placeholder="192.168.1.1"
|
||||
placeholder={v4Placeholders().gateway_ip}
|
||||
disabled={!hasIpv4()}
|
||||
errorMessage={gatewayIpError()}
|
||||
size="large"
|
||||
|
|
@ -186,7 +202,7 @@ export const DhcpV4Modal = (props: Props) => {
|
|||
}
|
||||
onBlur={onRangeStartBlur}
|
||||
id="v4_range_start"
|
||||
placeholder="192.168.1.2"
|
||||
placeholder={v4Placeholders().range_start}
|
||||
disabled={!hasIpv4()}
|
||||
errorMessage={rangeStartError()}
|
||||
size="large"
|
||||
|
|
@ -200,7 +216,7 @@ export const DhcpV4Modal = (props: Props) => {
|
|||
}
|
||||
onBlur={onRangeEndBlur}
|
||||
id="v4_range_end"
|
||||
placeholder="192.168.1.254"
|
||||
placeholder={v4Placeholders().range_end}
|
||||
disabled={!hasIpv4()}
|
||||
errorMessage={rangeEndError()}
|
||||
size="large"
|
||||
|
|
@ -215,7 +231,7 @@ export const DhcpV4Modal = (props: Props) => {
|
|||
onBlur={onSubnetBlur}
|
||||
id="v4_subnet_mask"
|
||||
label={intl.getMessage('dhcp_form_subnet_input')}
|
||||
placeholder="255.255.255.0"
|
||||
placeholder={v4Placeholders().subnet_mask}
|
||||
disabled={!hasIpv4()}
|
||||
errorMessage={subnetMaskError()}
|
||||
size="large"
|
||||
|
|
@ -229,7 +245,7 @@ export const DhcpV4Modal = (props: Props) => {
|
|||
id="v4_lease_duration"
|
||||
inputMode="numeric"
|
||||
label={intl.getMessage('dhcp_form_lease_title')}
|
||||
placeholder="86400"
|
||||
placeholder={v4Placeholders().lease_duration}
|
||||
disabled={!hasIpv4()}
|
||||
size="large"
|
||||
inputError={leaseDurationError()}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import theme from 'panel/lib/theme';
|
|||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Input } from 'panel/common/controls/Input';
|
||||
import { dhcpState } from 'panel/stores/dhcp';
|
||||
import { calculateDhcpPlaceholdersIpv6 } from 'panel/helpers/helpers';
|
||||
import { validateIpv6, validateLeaseTime } from 'panel/helpers/validators';
|
||||
|
||||
export type V6Config = {
|
||||
|
|
@ -36,6 +37,8 @@ export const DhcpV6Modal = (props: Props) => {
|
|||
}
|
||||
});
|
||||
|
||||
const v6Placeholders = createMemo(() => calculateDhcpPlaceholdersIpv6());
|
||||
|
||||
const hasIpv6 = createMemo(
|
||||
() =>
|
||||
!!(
|
||||
|
|
@ -81,7 +84,9 @@ export const DhcpV6Modal = (props: Props) => {
|
|||
<Input
|
||||
id="v6_range_start"
|
||||
label={intl.getMessage('dhcp_form_range_title')}
|
||||
placeholder={intl.getMessage('dhcp_form_range_start')}
|
||||
placeholder={
|
||||
v6Placeholders().range_start || intl.getMessage('dhcp_form_range_start')
|
||||
}
|
||||
value={rangeStart()}
|
||||
onChange={(e: Event) => setRangeStart((e.target as HTMLInputElement).value)}
|
||||
onBlur={validateRangeStart}
|
||||
|
|
@ -94,7 +99,7 @@ export const DhcpV6Modal = (props: Props) => {
|
|||
id="v6_lease_duration"
|
||||
type="number"
|
||||
label={intl.getMessage('dhcp_form_lease_title')}
|
||||
placeholder="86400"
|
||||
placeholder={v6Placeholders().lease_duration || '86400'}
|
||||
value={leaseDuration()}
|
||||
onChange={(e: Event) => setLeaseDuration((e.target as HTMLInputElement).value)}
|
||||
onBlur={() => validateLeaseDuration()}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
|||
import { Input } from 'panel/common/controls/Input';
|
||||
import { Radio } from 'panel/common/controls/Radio';
|
||||
import { BLOCKING_MODES, UINT32_RANGE } from 'panel/helpers/constants';
|
||||
import type { DNSConfigBlockingMode } from 'panel/api/model';
|
||||
import { getBlockingModeOptions } from '../../helpers';
|
||||
import {
|
||||
validateRequiredValue,
|
||||
|
|
@ -90,7 +91,7 @@ export const BlockingModeDialog = (props: Props) => {
|
|||
name="blocking_mode"
|
||||
options={blockingModeOptions}
|
||||
value={blockingMode()}
|
||||
handleChange={(v: string) => setBlockingMode(v)}
|
||||
handleChange={(v: DNSConfigBlockingMode) => setBlockingMode(v)}
|
||||
inModal
|
||||
/>
|
||||
<Show when={blockingMode() === BLOCKING_MODES.custom_ip}>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export const BootstrapDnsDialog = (props: Props) => {
|
|||
description={
|
||||
<>
|
||||
<p>{intl.getMessage('dns_bootstrap_dns_desc')}</p>
|
||||
<p>{intl.getMessage('dns_bootstrap_dns_desc_2')}</p>
|
||||
</>
|
||||
}
|
||||
onClose={props.onClose}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { validateUpstreams } from 'panel/helpers/validators';
|
|||
import { useField } from 'panel/hooks/useField';
|
||||
import { Examples } from './Examples';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { UPSTREAM_CONFIGURATION_WIKI_LINK } from 'panel/helpers/constants';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
|
|
@ -32,7 +33,7 @@ export const FallbackDnsDialog = (props: Props) => {
|
|||
{intl.getMessage('dns_fallback_dns_desc_2', {
|
||||
a: (text: string) => (
|
||||
<a
|
||||
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams"
|
||||
href={UPSTREAM_CONFIGURATION_WIKI_LINK}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class={theme.link.link}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { useField } from 'panel/hooks/useField';
|
|||
import { Examples } from './Examples';
|
||||
import { ServerAddressesFileDialog } from './ServerAddressesFileDialog';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { UPSTREAM_CONFIGURATION_WIKI_LINK } from 'panel/helpers/constants';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
|
|
@ -39,7 +40,7 @@ export const ServerAddressesDialog = (props: Props) => {
|
|||
{intl.getMessage('dns_server_addresses_desc_2', {
|
||||
a: (text: string) => (
|
||||
<a
|
||||
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams"
|
||||
href={UPSTREAM_CONFIGURATION_WIKI_LINK}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class={theme.link.link}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import intl from 'panel/common/intl';
|
|||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Radio } from 'panel/common/controls/Radio';
|
||||
import { getUpstreamModeOptions } from '../../helpers';
|
||||
import type { DNSConfigUpstreamMode } from 'panel/api/model';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
import s from './UpstreamModeDialog.module.pcss';
|
||||
|
|
@ -55,7 +56,7 @@ export const UpstreamModeDialog = (props: Props) => {
|
|||
name="upstream_mode"
|
||||
options={upstreamModeOptions()}
|
||||
value={upstreamMode()}
|
||||
handleChange={(v: string) => setUpstreamMode(v)}
|
||||
handleChange={(v: DNSConfigUpstreamMode) => setUpstreamMode(v)}
|
||||
inModal
|
||||
/>
|
||||
</ConfigDialog>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { DNS_REQUEST_OPTIONS, BLOCKING_MODES, EDNS_MODES } from 'panel/helpers/constants';
|
||||
import intl from 'panel/common/intl';
|
||||
import type { DNSConfigBlockingMode, DNSConfigUpstreamMode } from 'panel/api/model';
|
||||
|
||||
export const getUpstreamModeSummary = (mode: string): string => {
|
||||
export const getUpstreamModeSummary = (mode: DNSConfigUpstreamMode): string => {
|
||||
switch (mode) {
|
||||
case DNS_REQUEST_OPTIONS.PARALLEL:
|
||||
return intl.getMessage('upstream_dns_parallel_requests');
|
||||
|
|
@ -27,7 +28,7 @@ export const getRateLimitSummary = (ratelimit: number): string => {
|
|||
return intl.getMessage('dns_rate_limit_value', { value: ratelimit });
|
||||
};
|
||||
|
||||
export const getBlockingModeSummary = (mode: string): string => {
|
||||
export const getBlockingModeSummary = (mode: DNSConfigBlockingMode): string => {
|
||||
switch (mode) {
|
||||
case BLOCKING_MODES.refused:
|
||||
return 'REFUSED';
|
||||
|
|
|
|||
|
|
@ -132,9 +132,9 @@ export const Encryption = () => {
|
|||
serve_plain_dns: encryptionState.serve_plain_dns,
|
||||
server_name: encryptionState.server_name,
|
||||
force_https: encryptionState.force_https,
|
||||
port_https: encryptionState.port_https,
|
||||
port_dns_over_tls: encryptionState.port_dns_over_tls,
|
||||
port_dns_over_quic: encryptionState.port_dns_over_quic,
|
||||
port_https: Number(encryptionState.port_https) || 0,
|
||||
port_dns_over_tls: Number(encryptionState.port_dns_over_tls) || 0,
|
||||
port_dns_over_quic: Number(encryptionState.port_dns_over_quic) || 0,
|
||||
certificate_chain: encryptionState.certificate_chain,
|
||||
private_key: encryptionState.private_key,
|
||||
certificate_path: encryptionState.certificate_path,
|
||||
|
|
|
|||
|
|
@ -69,9 +69,9 @@ export const AddTlsCertModal = (props: Props) => {
|
|||
enabled: encryptionState.enabled,
|
||||
serve_plain_dns: encryptionState.serve_plain_dns,
|
||||
server_name: encryptionState.server_name,
|
||||
port_https: encryptionState.port_https || 0,
|
||||
port_dns_over_tls: encryptionState.port_dns_over_tls || 0,
|
||||
port_dns_over_quic: encryptionState.port_dns_over_quic || 0,
|
||||
port_https: Number(encryptionState.port_https) || 0,
|
||||
port_dns_over_tls: Number(encryptionState.port_dns_over_tls) || 0,
|
||||
port_dns_over_quic: Number(encryptionState.port_dns_over_quic) || 0,
|
||||
certificate_chain: certChain(),
|
||||
private_key: privateKey(),
|
||||
certificate_path: certPath(),
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ export const ServerSettingsModal = (props: Props) => {
|
|||
(open) => {
|
||||
if (open) {
|
||||
setServerName(encryptionState.server_name || '');
|
||||
setPortHttps(encryptionState.port_https || 0);
|
||||
setPortDot(encryptionState.port_dns_over_tls || 0);
|
||||
setPortDoq(encryptionState.port_dns_over_quic || 0);
|
||||
setPortHttps(Number(encryptionState.port_https) || 0);
|
||||
setPortDot(Number(encryptionState.port_dns_over_tls) || 0);
|
||||
setPortDoq(Number(encryptionState.port_dns_over_quic) || 0);
|
||||
setErrors({});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ export const DNSRewrites = () => {
|
|||
<Show when={rewritesState.list.length > 0}>
|
||||
<div class={cn(s.group, s.tableGroup)}>
|
||||
<RewritesTable
|
||||
list={rewritesState.list}
|
||||
list={rewritesState.list as Rewrite[]}
|
||||
processing={rewritesState.processing}
|
||||
processingAdd={rewritesState.processingAdd}
|
||||
processingUpdate={rewritesState.processingUpdate}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { closeModal } from 'panel/stores/modals';
|
|||
import theme from 'panel/lib/theme';
|
||||
import { Button } from 'panel/common/ui/Button';
|
||||
import { addFilter, editFilter, filteringState } from 'panel/stores/filtering';
|
||||
import type { FilterSetUrlData } from 'panel/api/model/filterSetUrlData';
|
||||
import { Input } from 'panel/common/controls/Input';
|
||||
import { validatePath, validateRequiredValue } from 'panel/helpers/validators';
|
||||
|
||||
|
|
@ -72,7 +73,7 @@ export const ConfigureAllowlistModal = (props: Props) => {
|
|||
break;
|
||||
}
|
||||
case MODAL_TYPE.EDIT_ALLOWLIST: {
|
||||
editFilter(props.filterToEdit!.url, values, true);
|
||||
editFilter(props.filterToEdit!.url, values as FilterSetUrlData, true);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
filteringState,
|
||||
} from 'panel/stores/filtering';
|
||||
import type { Filter } from 'panel/helpers/helpers';
|
||||
import type { FilterSetUrlData } from 'panel/api/model/filterSetUrlData';
|
||||
import { validatePath, validateRequiredValue } from 'panel/helpers/validators';
|
||||
import { ManualFilterForm } from 'panel/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/ManualFilterForm';
|
||||
import { Tabs } from 'panel/common/ui/Tabs';
|
||||
|
|
@ -170,7 +171,7 @@ export const ConfigureBlocklistModal = (props: Props) => {
|
|||
break;
|
||||
}
|
||||
case MODAL_TYPE.EDIT_BLOCKLIST: {
|
||||
editFilter(props.filterToEdit!.url, values, false);
|
||||
editFilter(props.filterToEdit!.url, values as FilterSetUrlData, false);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,11 @@ export const ConfigureRewritesModal = (props: Props) => {
|
|||
validateRequiredValue(answer()) ||
|
||||
validateAnswer(answer()) ||
|
||||
validateRewriteNotSame(domain(), answer()) ||
|
||||
validateRewriteNotExists(domain(), rewritesState.list, props.rewriteToEdit?.domain);
|
||||
validateRewriteNotExists(
|
||||
domain(),
|
||||
rewritesState.list as { domain: string }[],
|
||||
props.rewriteToEdit?.domain,
|
||||
);
|
||||
setAnswerError(err || undefined);
|
||||
return !err;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ export const ListsTable = (props: Props) => {
|
|||
const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc');
|
||||
|
||||
const pageSize = createMemo(
|
||||
() => LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.BLOCKLIST_PAGE_SIZE) || undefined,
|
||||
() =>
|
||||
LocalStorageHelper.getItem<number>(LOCAL_STORAGE_KEYS.BLOCKLIST_PAGE_SIZE) || undefined,
|
||||
);
|
||||
|
||||
const sortedFilters = createMemo(() => {
|
||||
|
|
@ -93,9 +94,7 @@ export const ListsTable = (props: Props) => {
|
|||
|
||||
return (
|
||||
<div class={theme.table.cell}>
|
||||
<span class={theme.table.cellLabel}>
|
||||
{intl.getMessage('name_label')}
|
||||
</span>
|
||||
<span class={theme.table.cellLabel}>{intl.getMessage('name_label')}</span>
|
||||
|
||||
<div class={cn(theme.table.cellValueText, s.domainCellValue)}>
|
||||
<span class={theme.common.textOverflow}>{value}</span>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ export const RewritesTable = (props: Props) => {
|
|||
const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc');
|
||||
|
||||
const pageSize = createMemo(
|
||||
() => LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.BLOCKLIST_PAGE_SIZE) || undefined,
|
||||
() =>
|
||||
LocalStorageHelper.getItem<number>(LOCAL_STORAGE_KEYS.BLOCKLIST_PAGE_SIZE) || undefined,
|
||||
);
|
||||
|
||||
const sortedList = createMemo(() => {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import { getLogsUrlParams } from 'panel/helpers/helpers';
|
|||
import { RoutePath, linkPathBuilder } from 'panel/components/Routes/Paths';
|
||||
|
||||
import { filterLogsByStatus } from './helpers';
|
||||
import { LogEntry } from './types';
|
||||
import { LogEntry, ResponseEntry } from './types';
|
||||
import { Header } from './blocks/Header';
|
||||
import { EmptyState, type EmptyStateMode } from './blocks/EmptyState/EmptyState';
|
||||
import { LogTable } from './blocks/LogTable';
|
||||
|
|
@ -109,7 +109,11 @@ export const QueryLog = () => {
|
|||
(dashboardState.clients || []).flatMap(
|
||||
(persistentClient: any) => persistentClient.ids ?? [],
|
||||
);
|
||||
const visibleLogs = () => filterLogsByStatus(queryLogsState.logs || [], currentStatus());
|
||||
const visibleLogs = () =>
|
||||
filterLogsByStatus(
|
||||
(queryLogsState.logs || []) as { reason: string; originalResponse?: ResponseEntry[] }[],
|
||||
currentStatus(),
|
||||
);
|
||||
const emptyStateMode = () => getEmptyStateMode(queryLogsState.enabled, queryLogsState.interval);
|
||||
const hasMore = () => !queryLogsState.isEntireLog;
|
||||
const logs = () => queryLogsState.logs || [];
|
||||
|
|
@ -232,7 +236,7 @@ export const QueryLog = () => {
|
|||
|
||||
<div class={s.desktopView}>
|
||||
<LogTable
|
||||
logs={visibleLogs()}
|
||||
logs={visibleLogs() as LogEntry[]}
|
||||
emptyStateMode={emptyStateMode()}
|
||||
hasMore={hasMore()}
|
||||
isLoadingMore={isLoadingMore()}
|
||||
|
|
@ -271,7 +275,7 @@ export const QueryLog = () => {
|
|||
<For each={visibleLogs()}>
|
||||
{(entry) => (
|
||||
<LogCard
|
||||
entry={entry}
|
||||
entry={entry as LogEntry}
|
||||
onRowClick={handleRowClick}
|
||||
onBlock={handleBlockDomain}
|
||||
onUnblock={handleUnblockDomain}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { FilteringReason } from 'panel/api/model/filteringReason';
|
||||
|
||||
export type ResponseEntry = {
|
||||
value: string;
|
||||
type?: string;
|
||||
|
|
@ -47,7 +49,7 @@ export type LogEntry = {
|
|||
unicodeName?: string;
|
||||
type: string;
|
||||
response: ResponseEntry[];
|
||||
reason: string;
|
||||
reason: FilteringReason;
|
||||
client: string;
|
||||
client_info: ClientInfo | null;
|
||||
tracker: TrackerInfo | null;
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export const Settings = () => {
|
|||
const ss = safesearch();
|
||||
if (!ss) return '';
|
||||
const selected = Object.keys(SAFE_SEARCH_PROVIDERS)
|
||||
.filter((key) => ss[key])
|
||||
.filter((key) => (ss as Record<string, boolean>)[key])
|
||||
.map(getSafeSearchProviderTitle);
|
||||
return selected.join(', ');
|
||||
});
|
||||
|
|
@ -214,7 +214,9 @@ export const Settings = () => {
|
|||
<SafeSearchModal
|
||||
open={safesearchProvidersOpen()}
|
||||
onClose={() => setSafesearchProvidersOpen(false)}
|
||||
providers={settingsState.settingsList.safesearch}
|
||||
providers={
|
||||
settingsState.settingsList.safesearch as Record<string, boolean>
|
||||
}
|
||||
enabled={safesearchEnabled()}
|
||||
processing={safesearchProcessing()}
|
||||
onSave={handleSafeSearchSave}
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ export const MobileConfigForm = (props: Props) => {
|
|||
onChange={handleHostChange}
|
||||
error={!!hostError()}
|
||||
errorMessage={hostError()}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
<Show when={protocol() === MOBILE_CONFIG_LINKS.DOH}>
|
||||
|
|
@ -138,6 +139,7 @@ export const MobileConfigForm = (props: Props) => {
|
|||
onChange={handlePortChange}
|
||||
error={!!portError()}
|
||||
errorMessage={portError()}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
|
@ -169,6 +171,7 @@ export const MobileConfigForm = (props: Props) => {
|
|||
onChange={handleClientIdChange}
|
||||
error={!!clientIdError()}
|
||||
errorMessage={clientIdError()}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { RulesEditor } from './blocks/RulesEditor';
|
|||
import { DNS_RECORD_TYPE_OPTIONS } from './types';
|
||||
import { useUserRulesActions } from './useUserRulesActions';
|
||||
|
||||
import type { CheckFormValues } from './types';
|
||||
import type { CheckFormValues, CheckResultData } from './types';
|
||||
|
||||
import s from './UserRules.module.pcss';
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ export const UserRules = () => {
|
|||
openDeleteRewriteModal,
|
||||
resetCurrentRewrite,
|
||||
} = useUserRulesActions({
|
||||
checkResult: () => filteringState.check,
|
||||
checkResult: () => filteringState.check as CheckResultData,
|
||||
filteringEnabled: () => filteringState.enabled,
|
||||
settingsList: () => settingsState.settingsList,
|
||||
persistentClients: () => dashboardState.clients || [],
|
||||
|
|
@ -83,7 +83,7 @@ export const UserRules = () => {
|
|||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (filteringState.check?.hostname) {
|
||||
if ((filteringState.check as CheckResultData)?.hostname) {
|
||||
setIsResultVisible(true);
|
||||
}
|
||||
});
|
||||
|
|
@ -130,7 +130,10 @@ export const UserRules = () => {
|
|||
|
||||
const showResultLoader = createMemo(() => isResultVisible() && isResultRefreshing());
|
||||
const showResultCard = createMemo(
|
||||
() => isResultVisible() && !isResultRefreshing() && Boolean(filteringState.check?.hostname),
|
||||
() =>
|
||||
isResultVisible() &&
|
||||
!isResultRefreshing() &&
|
||||
Boolean((filteringState.check as CheckResultData)?.hostname),
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -182,7 +185,7 @@ export const UserRules = () => {
|
|||
|
||||
<Show when={showResultCard()}>
|
||||
<CheckResult
|
||||
checkResult={filteringState.check}
|
||||
checkResult={filteringState.check as CheckResultData}
|
||||
processingRules={isActionProcessing()}
|
||||
onDismiss={() => setIsResultVisible(false)}
|
||||
onAction={handleAction}
|
||||
|
|
@ -198,13 +201,17 @@ export const UserRules = () => {
|
|||
|
||||
<ConfigureRewritesModal
|
||||
modalId={MODAL_TYPE.EDIT_REWRITE}
|
||||
rewriteToEdit={currentRewrite()}
|
||||
rewriteToEdit={
|
||||
currentRewrite() as { answer: string; domain: string; enabled: boolean }
|
||||
}
|
||||
onSubmit={handleRewriteUpdate}
|
||||
onClose={resetCurrentRewrite}
|
||||
/>
|
||||
|
||||
<DeleteRewriteModal
|
||||
rewriteToDelete={currentRewrite()}
|
||||
rewriteToDelete={
|
||||
currentRewrite() as { answer: string; domain: string; enabled: boolean }
|
||||
}
|
||||
setRewriteToDelete={setCurrentRewrite}
|
||||
onConfirm={handleRewriteDelete}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import intl from 'panel/common/intl';
|
||||
import { FILTERED_STATUS, SPECIAL_FILTER_ID } from 'panel/helpers/constants';
|
||||
import { checkFiltered, getFilterName, type Filter } from 'panel/helpers/helpers';
|
||||
import type { FilteringReason } from 'panel/api/model/filteringReason';
|
||||
|
||||
import { CheckResultData, ResultAction, ResultActionKind } from './types';
|
||||
|
||||
|
|
@ -244,7 +245,7 @@ export const getCheckResultMeta = ({
|
|||
source: intl.getMessage('system_host_files'),
|
||||
};
|
||||
default: {
|
||||
const isFilteredReason = reason ? checkFiltered(reason) : false;
|
||||
const isFilteredReason = reason ? checkFiltered(reason as FilteringReason) : false;
|
||||
|
||||
return {
|
||||
tone: isFilteredReason ? 'blocked' : 'processed',
|
||||
|
|
|
|||
|
|
@ -26,11 +26,11 @@ export const findPersistentClient = (clients: Client[], identifier?: string) =>
|
|||
|
||||
const normalizedIdentifier = normalizeClientIdentifier(identifier);
|
||||
const matches = clients.filter((client) => {
|
||||
if (normalizeClientIdentifier(client.name) === normalizedIdentifier) {
|
||||
if (normalizeClientIdentifier(client.name ?? '') === normalizedIdentifier) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return client.ids.some(
|
||||
return (client.ids ?? []).some(
|
||||
(clientId) => normalizeClientIdentifier(clientId) === normalizedIdentifier,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ export type ResultAction = {
|
|||
};
|
||||
|
||||
export type RewriteEntry = {
|
||||
domain: string;
|
||||
answer: string;
|
||||
enabled: boolean;
|
||||
domain?: string;
|
||||
answer?: string;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export type RewriteDialogState = {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { DNSConfigBlockingMode } from 'panel/api/model';
|
||||
|
||||
export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/[^/\s]+(\/.*)?$/;
|
||||
|
||||
// matches hostname or *.wildcard
|
||||
|
|
@ -5,9 +7,6 @@ export const R_HOST = /^(\*\.)?[\w.-]+$/;
|
|||
|
||||
export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/;
|
||||
|
||||
export const R_IPV6 =
|
||||
/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
|
||||
|
||||
export const R_CIDR =
|
||||
/^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$/;
|
||||
|
||||
|
|
@ -34,10 +33,6 @@ export const R_CLIENT_ID = /^[a-z0-9-]{1,63}$/;
|
|||
|
||||
export const R_HOSTNAME = /^[a-z0-9-]+$/;
|
||||
|
||||
export const R_IPV4_SUBNET = /^([0-9]|[1-2][0-9]|3[0-2])?$/;
|
||||
|
||||
export const R_IPV6_SUBNET = /^([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])?$/;
|
||||
|
||||
export const MIN_PASSWORD_LENGTH = 8;
|
||||
export const MAX_PASSWORD_LENGTH = 72;
|
||||
|
||||
|
|
@ -57,13 +52,6 @@ export const STATS_NAMES = {
|
|||
replaced_safesearch: 'enforced_save_search',
|
||||
};
|
||||
|
||||
export const STATUS_COLORS = {
|
||||
blue: '#467fcf',
|
||||
red: '#cd201f',
|
||||
green: '#5eba00',
|
||||
yellow: '#f1c40f',
|
||||
};
|
||||
|
||||
export const REPOSITORY = {
|
||||
URL: 'https://github.com/AdguardTeam/AdGuardHome',
|
||||
TRACKERS_DB:
|
||||
|
|
@ -83,26 +71,16 @@ export const TERMS_LINK =
|
|||
export const UPSTREAM_CONFIGURATION_WIKI_LINK =
|
||||
'https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams';
|
||||
|
||||
export const FILTERS_RELATIVE_LINK = '#filters';
|
||||
|
||||
export const ADDRESS_IN_USE_TEXT = 'address already in use';
|
||||
|
||||
export const INSTALL_FIRST_STEP = 1;
|
||||
export const INSTALL_TOTAL_STEPS = 6;
|
||||
|
||||
export const SETTINGS_NAMES = {
|
||||
filtering: 'filtering',
|
||||
safebrowsing: 'safebrowsing',
|
||||
parental: 'parental',
|
||||
safesearch: 'safesearch',
|
||||
};
|
||||
|
||||
export const STANDARD_DNS_PORT = 53;
|
||||
export const STANDARD_WEB_PORT = 80;
|
||||
export const STANDARD_HTTPS_PORT = 443;
|
||||
export const DNS_OVER_TLS_PORT = 853;
|
||||
export const DNS_OVER_QUIC_PORT = 853;
|
||||
export const MIN_PORT = 1;
|
||||
export const MAX_PORT = 65535;
|
||||
|
||||
export const EMPTY_DATE = '0001-01-01T00:00:00Z';
|
||||
|
|
@ -110,9 +88,6 @@ export const EMPTY_DATE = '0001-01-01T00:00:00Z';
|
|||
export const DEBOUNCE_TIMEOUT = 300;
|
||||
export const DEBOUNCE_FILTER_TIMEOUT = 500;
|
||||
export const CHECK_TIMEOUT = 1000;
|
||||
export const HIDE_TOOLTIP_DELAY = 300;
|
||||
export const SHOW_TOOLTIP_DELAY = 200;
|
||||
export const MODAL_OPEN_TIMEOUT = 150;
|
||||
|
||||
export const UNSAFE_PORTS = [
|
||||
1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 77, 79, 87, 95, 101, 102, 103,
|
||||
|
|
@ -157,11 +132,6 @@ export const TAB_TYPE = {
|
|||
MANUAL: 'manual',
|
||||
} as const;
|
||||
|
||||
export const CLIENT_ID = {
|
||||
MAC: 'mac',
|
||||
IP: 'ip',
|
||||
};
|
||||
|
||||
export const ENCRYPTION_SOURCE = {
|
||||
PATH: 'path',
|
||||
CONTENT: 'content',
|
||||
|
|
@ -169,9 +139,6 @@ export const ENCRYPTION_SOURCE = {
|
|||
};
|
||||
|
||||
export const FILTERED = 'Filtered';
|
||||
export const NOT_FILTERED = 'NotFiltered';
|
||||
|
||||
export const DISABLED_STATS_INTERVAL = 0;
|
||||
|
||||
export const HOUR = 60 * 60 * 1000;
|
||||
|
||||
|
|
@ -183,15 +150,7 @@ export const QUERY_LOG_INTERVALS_DAYS = [HOUR * 6, DAY, DAY * 7, DAY * 30, DAY *
|
|||
|
||||
export const RETENTION_CUSTOM = 1;
|
||||
|
||||
export const RETENTION_CUSTOM_INPUT = 'custom_retention_input';
|
||||
|
||||
export const CUSTOM_INTERVAL = 'customInterval';
|
||||
|
||||
export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168];
|
||||
|
||||
// Note that translation strings contain these modes (blocking_mode_CONSTANT)
|
||||
// i.e. blocking_mode_default, blocking_mode_null_ip
|
||||
export const BLOCKING_MODES = {
|
||||
export const BLOCKING_MODES: { readonly [K in DNSConfigBlockingMode]: K } = {
|
||||
default: 'default',
|
||||
refused: 'refused',
|
||||
nxdomain: 'nxdomain',
|
||||
|
|
@ -204,8 +163,6 @@ export const EDNS_MODES = {
|
|||
custom: 'custom',
|
||||
};
|
||||
|
||||
// Note that translation strings contain these modes (theme_CONSTANT)
|
||||
// i.e. theme_auto, theme_light.
|
||||
export const THEMES = {
|
||||
auto: 'auto',
|
||||
dark: 'dark',
|
||||
|
|
@ -251,12 +208,14 @@ export const DEFAULT_LOGS_FILTER = {
|
|||
reason: 'all',
|
||||
};
|
||||
|
||||
export const DEFAULT_LANGUAGE = 'en';
|
||||
export type QueryLogFilter = {
|
||||
search: string;
|
||||
status: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
export const QUERY_LOGS_PAGE_LIMIT = 20;
|
||||
|
||||
export const LEASES_TABLE_DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
export const FILTERED_STATUS = {
|
||||
FILTERED_BLACK_LIST: 'FilteredBlackList',
|
||||
NOT_FILTERED_WHITE_LIST: 'NotFilteredWhiteList',
|
||||
|
|
@ -344,10 +303,6 @@ export const QUERY_LOG_REASON_FILTER_QUERIES = Object.values(QUERY_LOG_REASON_FI
|
|||
{},
|
||||
);
|
||||
|
||||
export const RESPONSE_FILTER = QUERY_LOG_REASON_FILTER;
|
||||
|
||||
export const RESPONSE_FILTER_QUERIES = QUERY_LOG_REASON_FILTER_QUERIES;
|
||||
|
||||
export const QUERY_STATUS_COLORS = {
|
||||
BLUE: 'blue',
|
||||
GREEN: 'green',
|
||||
|
|
@ -431,38 +386,7 @@ export const DNS_REQUEST_OPTIONS = {
|
|||
PARALLEL: 'parallel',
|
||||
FASTEST_ADDR: 'fastest_addr',
|
||||
LOAD_BALANCING: 'load_balance',
|
||||
};
|
||||
|
||||
export const DHCP_FORM_NAMES = {
|
||||
DHCPv4: 'dhcpv4',
|
||||
DHCPv6: 'dhcpv6',
|
||||
DHCP_INTERFACES: 'dhcpInterfaces',
|
||||
};
|
||||
|
||||
export const FORM_NAME = {
|
||||
UPSTREAM: 'upstream',
|
||||
DOMAIN_CHECK: 'domainCheck',
|
||||
FILTER: 'filter',
|
||||
REWRITES: 'rewrites',
|
||||
LOGS_FILTER: 'logsFilter',
|
||||
CLIENT: 'client',
|
||||
LEASE: 'lease',
|
||||
ACCESS: 'access',
|
||||
BLOCKING_MODE: 'blockingMode',
|
||||
ENCRYPTION: 'encryption',
|
||||
FILTER_CONFIG: 'filterConfig',
|
||||
LOG_CONFIG: 'logConfig',
|
||||
SERVICES: 'services',
|
||||
STATS_CONFIG: 'statsConfig',
|
||||
INSTALL: 'install',
|
||||
LOGIN: 'login',
|
||||
CACHE: 'cache',
|
||||
MOBILE_CONFIG: 'mobileConfig',
|
||||
...DHCP_FORM_NAMES,
|
||||
};
|
||||
|
||||
export const SMALL_SCREEN_SIZE = 767;
|
||||
export const MEDIUM_SCREEN_SIZE = 1024;
|
||||
} as const;
|
||||
|
||||
export const SECONDS_IN_DAY = 60 * 60 * 24;
|
||||
|
||||
|
|
@ -521,8 +445,6 @@ export const DHCP_VALUES_PLACEHOLDERS = {
|
|||
},
|
||||
};
|
||||
|
||||
export const TOAST_TRANSITION_TIMEOUT = 500;
|
||||
|
||||
export const TOAST_TYPES = {
|
||||
SUCCESS: 'success',
|
||||
ERROR: 'error',
|
||||
|
|
@ -549,12 +471,6 @@ export const ADDRESS_TYPES = {
|
|||
UNKNOWN: 'UNKNOWN',
|
||||
};
|
||||
|
||||
export const CACHE_CONFIG_FIELDS = {
|
||||
cache_size: 'cache_size',
|
||||
cache_ttl_min: 'cache_ttl_min',
|
||||
cache_ttl_max: 'cache_ttl_max',
|
||||
};
|
||||
|
||||
export const COMMENT_LINE_DEFAULT_TOKEN = '#';
|
||||
export const COMMENT_LINE_TOKENS = ['#', '!'] as const;
|
||||
export type CommentLineToken = (typeof COMMENT_LINE_TOKENS)[number];
|
||||
|
|
@ -573,14 +489,8 @@ export const DISABLE_PROTECTION_TIMINGS = {
|
|||
TOMORROW: 24 * 60 * 60 * 1000,
|
||||
};
|
||||
|
||||
export const LOCAL_TIMEZONE_VALUE = 'Local';
|
||||
|
||||
export const TABLES_MIN_ROWS = 5;
|
||||
|
||||
export const MOBILE_TABLE_MAX_ROWS = 5;
|
||||
|
||||
export const DASHBOARD_TABLES_DEFAULT_PAGE_SIZE = 100;
|
||||
|
||||
export const TIME_UNITS = {
|
||||
HOURS: 'hours',
|
||||
DAYS: 'days',
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import { R_MAC_WITHOUT_COLON, R_UNIX_ABSOLUTE_PATH, R_WIN_ABSOLUTE_PATH } from '
|
|||
* @param {string} ip
|
||||
* @returns {*}
|
||||
*/
|
||||
export const ip4ToInt = (ip: any) => {
|
||||
const intIp = ip.split('.').reduce((int: any, oct: any) => int * 256 + parseInt(oct, 10), 0);
|
||||
export const ip4ToInt = (ip: string): number => {
|
||||
const intIp = ip
|
||||
.split('.')
|
||||
.reduce((int: number, oct: string) => int * 256 + parseInt(oct, 10), 0);
|
||||
return Number.isNaN(intIp) ? 0 : intIp;
|
||||
};
|
||||
|
||||
|
|
@ -14,20 +16,20 @@ export const ip4ToInt = (ip: any) => {
|
|||
* @param value {string}
|
||||
* @returns {*|number}
|
||||
*/
|
||||
export const toNumber = (value: any) => value && parseInt(value, 10);
|
||||
export const toNumber = (value: string): number | undefined => value && parseInt(value, 10);
|
||||
|
||||
/**
|
||||
* @param value {string}
|
||||
* @returns {*|number}
|
||||
*/
|
||||
|
||||
export const toFloatNumber = (value: any) => value && parseFloat(value);
|
||||
export const toFloatNumber = (value: string): number | undefined => value && parseFloat(value);
|
||||
|
||||
/**
|
||||
* @param value {string}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isValidAbsolutePath = (value: any) =>
|
||||
export const isValidAbsolutePath = (value: string): boolean =>
|
||||
R_WIN_ABSOLUTE_PATH.test(value) || R_UNIX_ABSOLUTE_PATH.test(value);
|
||||
|
||||
/**
|
||||
|
|
@ -38,7 +40,7 @@ export const isValidAbsolutePath = (value: any) =>
|
|||
* @example normalizeMac("AA-BB-CC-DD-EE-FF") // "AA:BB:CC:DD:EE:FF"
|
||||
* @example normalizeMac("aa:bb:cc:dd:ee:ff") // "AA:BB:CC:DD:EE:FF"
|
||||
*/
|
||||
export const normalizeMac = (value: any) => {
|
||||
export const normalizeMac = (value: string): string => {
|
||||
if (!value || typeof value !== 'string') return value;
|
||||
|
||||
// Handle separator-less bare hex (12 or 16 chars)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { parseISO, format as dateFormat } from 'date-fns';
|
||||
import round from 'lodash/round';
|
||||
import ipaddr, { IPv4, IPv6 } from 'ipaddr.js';
|
||||
import queryString from 'qs';
|
||||
import intl from 'panel/common/intl';
|
||||
|
|
@ -10,13 +8,11 @@ import {
|
|||
CHECK_TIMEOUT,
|
||||
COMMENT_LINE_DEFAULT_TOKEN,
|
||||
DEFAULT_DATE_FORMAT_OPTIONS,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
DETAILED_DATE_FORMAT_OPTIONS,
|
||||
DHCP_VALUES_PLACEHOLDERS,
|
||||
FILTERED,
|
||||
FILTERED_STATUS,
|
||||
R_CLIENT_ID,
|
||||
STANDARD_DNS_PORT,
|
||||
STANDARD_HTTPS_PORT,
|
||||
STANDARD_WEB_PORT,
|
||||
SPECIAL_FILTER_ID,
|
||||
|
|
@ -24,17 +20,51 @@ import {
|
|||
SHORT_DATE_FORMAT_OPTIONS,
|
||||
} from './constants';
|
||||
import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from './localStorageHelper';
|
||||
import { DhcpInterfaces, InstallInterface } from '../initialState';
|
||||
import type { NetInterfaces } from '../api/model/netInterfaces';
|
||||
import { DhcpInterfaces } from '../initialState';
|
||||
import type { NetInterfaces } from 'panel/api/model/netInterfaces';
|
||||
import type { DnsAnswer } from 'panel/api/model/dnsAnswer';
|
||||
import type { ResultRule } from 'panel/api/model/resultRule';
|
||||
import type { FilteringReason } from 'panel/api/model/filteringReason';
|
||||
import type { FilterStatus } from 'panel/api/model/filterStatus';
|
||||
import type { TopArrayEntry } from 'panel/api/model/topArrayEntry';
|
||||
import type { ClientsFindEntry } from 'panel/api/model/clientsFindEntry';
|
||||
import type { ClientFindSubEntry } from 'panel/api/model/clientFindSubEntry';
|
||||
import type { QueryLogItemClient } from 'panel/api/model/queryLogItemClient';
|
||||
import type { QueryLogItemClientWhois } from 'panel/api/model/queryLogItemClientWhois';
|
||||
import type { QueryLogItemClientProto } from 'panel/api/model/queryLogItemClientProto';
|
||||
import type { QueryLogItem } from 'panel/api/model/queryLogItem';
|
||||
|
||||
/**
|
||||
* @param time {string} The time to format
|
||||
* @param options {string}
|
||||
* @returns {string} Returns the time in the format HH:mm:ss
|
||||
*/
|
||||
export const formatTime = (time: any, options = DEFAULT_TIME_FORMAT) => {
|
||||
const parsedTime = parseISO(time);
|
||||
return dateFormat(parsedTime, options);
|
||||
export type NormalizedDnsResponse = {
|
||||
value?: string;
|
||||
type?: string;
|
||||
ttl?: number;
|
||||
};
|
||||
|
||||
export type NormalizedQueryLogItem = {
|
||||
time: string;
|
||||
domain: string;
|
||||
unicodeName: string;
|
||||
type: string;
|
||||
response: NormalizedDnsResponse[];
|
||||
reason?: FilteringReason;
|
||||
client: string;
|
||||
client_proto?: QueryLogItemClientProto;
|
||||
client_id?: string;
|
||||
client_info: QueryLogItemClient | null;
|
||||
filterId?: number; // @deprecated
|
||||
rule?: string; // @deprecated
|
||||
rules: ResultRule[];
|
||||
status?: string;
|
||||
service_name?: string;
|
||||
serviceName?: string;
|
||||
originalAnswer?: DnsAnswer[];
|
||||
originalResponse: NormalizedDnsResponse[];
|
||||
tracker: Record<string, unknown> | null;
|
||||
answer_dnssec?: boolean;
|
||||
elapsedMs?: string;
|
||||
upstream?: string;
|
||||
cached?: boolean;
|
||||
ecs?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -69,8 +99,8 @@ export const formatDetailedDateTime = (dateTime: string) =>
|
|||
export const formatShortDateTime = (dateTime: string) =>
|
||||
formatDateTime(dateTime, SHORT_DATE_FORMAT_OPTIONS);
|
||||
|
||||
export const normalizeLogs = (logs: any) =>
|
||||
logs.map((log: any) => {
|
||||
export const normalizeLogs = (logs: QueryLogItem[]): NormalizedQueryLogItem[] =>
|
||||
logs.map((log) => {
|
||||
const {
|
||||
answer,
|
||||
answer_dnssec,
|
||||
|
|
@ -95,9 +125,9 @@ export const normalizeLogs = (logs: any) =>
|
|||
|
||||
const { name: domain, unicode_name: unicodeName, type } = question || {};
|
||||
|
||||
const processResponse = (data: any) =>
|
||||
const processResponse = (data: DnsAnswer[] | undefined): NormalizedDnsResponse[] =>
|
||||
Array.isArray(data)
|
||||
? data.map((response: any) => {
|
||||
? data.map((response: DnsAnswer) => {
|
||||
const { value, type, ttl } = response;
|
||||
|
||||
return {
|
||||
|
|
@ -153,28 +183,25 @@ export const normalizeLogs = (logs: any) =>
|
|||
};
|
||||
});
|
||||
|
||||
// TODO (ik) type will fixed in query log task
|
||||
export const normalizeHistory = (history: any) =>
|
||||
history.map((item: any, idx: number) => ({
|
||||
x: idx,
|
||||
y: item,
|
||||
}));
|
||||
|
||||
export const normalizeTopStats = (stats: any) =>
|
||||
stats.map((item: any) => ({
|
||||
export const normalizeTopStats = (stats: TopArrayEntry[]): TopStat[] =>
|
||||
stats.map((item: TopArrayEntry) => ({
|
||||
name: Object.keys(item)[0],
|
||||
|
||||
count: Object.values(item)[0],
|
||||
count: Object.values(item)[0] as number,
|
||||
}));
|
||||
|
||||
export const addClientInfo = (data: any, clients: any, ...params: any[]) =>
|
||||
data.map((row: any) => {
|
||||
let info = '';
|
||||
export const addClientInfo = (
|
||||
data: TopStat[],
|
||||
clients: ClientsFindEntry[],
|
||||
...params: string[]
|
||||
): (TopStat & { info: ClientFindSubEntry })[] =>
|
||||
data.map((row: TopStat) => {
|
||||
let info: ClientFindSubEntry | null = null;
|
||||
params.find((param) => {
|
||||
const id = row[param];
|
||||
const id = row[param as keyof TopStat];
|
||||
if (id) {
|
||||
const client = clients.find((item: any) => item[id]) || '';
|
||||
info = client?.[id] ?? '';
|
||||
const clientData = clients.find((item: ClientsFindEntry) => item[String(id)]);
|
||||
info = clientData?.[String(id)] ?? null;
|
||||
}
|
||||
|
||||
return info;
|
||||
|
|
@ -182,13 +209,13 @@ export const addClientInfo = (data: any, clients: any, ...params: any[]) =>
|
|||
|
||||
return {
|
||||
...row,
|
||||
info,
|
||||
info: info ?? {},
|
||||
};
|
||||
});
|
||||
|
||||
export const normalizeFilters = (filters: any) =>
|
||||
export const normalizeFilters = (filters: FilterStatus['filters']) =>
|
||||
filters
|
||||
? filters.map((filter: any) => {
|
||||
? filters.map((filter) => {
|
||||
const {
|
||||
id,
|
||||
url,
|
||||
|
|
@ -209,7 +236,15 @@ export const normalizeFilters = (filters: any) =>
|
|||
})
|
||||
: [];
|
||||
|
||||
export const normalizeFilteringStatus = (filteringStatus: any) => {
|
||||
export const normalizeFilteringStatus = (
|
||||
filteringStatus: FilterStatus,
|
||||
): {
|
||||
enabled: boolean | undefined;
|
||||
userRules: string;
|
||||
filters: Filter[];
|
||||
whitelistFilters: Filter[];
|
||||
interval: number | undefined;
|
||||
} => {
|
||||
const {
|
||||
enabled,
|
||||
filters,
|
||||
|
|
@ -228,21 +263,20 @@ export const normalizeFilteringStatus = (filteringStatus: any) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const getPercent = (amount: any, number: any) => {
|
||||
if (amount > 0 && number > 0) {
|
||||
return round(100 / (amount / number), 2);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const captitalizeWords = (text: any) =>
|
||||
export const captitalizeWords = (text: string): string =>
|
||||
text
|
||||
.split(/[ -_]/g)
|
||||
.map((str: any) => str.charAt(0).toUpperCase() + str.substr(1))
|
||||
.map((str: string) => str.charAt(0).toUpperCase() + str.substr(1))
|
||||
.join(' ');
|
||||
|
||||
export const getInterfaceIp = (option: any) => {
|
||||
const addresses = (option?.ip_addresses ?? []).filter((ip: any) => typeof ip === 'string');
|
||||
type InterfaceWithIpAddresses = { ip_addresses?: string[] };
|
||||
|
||||
type TopStat = { name: string; count: number };
|
||||
|
||||
type ServiceEntry = { id: string; name: string };
|
||||
|
||||
export const getInterfaceIp = (option: InterfaceWithIpAddresses): string | undefined => {
|
||||
const addresses = (option?.ip_addresses ?? []).filter((ip: string) => typeof ip === 'string');
|
||||
|
||||
const isIpv6 = (ip: string) => ip.includes(':');
|
||||
const isIpv6LinkLocal = (ip: string) => ip.toLowerCase().startsWith('fe80:');
|
||||
|
|
@ -264,34 +298,6 @@ export const getInterfaceIp = (option: any) => {
|
|||
return ipv6NoZone || addresses[0];
|
||||
};
|
||||
|
||||
export const getIpList = (interfaces: InstallInterface[]) =>
|
||||
Object.values(interfaces)
|
||||
.reduce(
|
||||
(acc: string[], curr: InstallInterface) => acc.concat(curr.ip_addresses),
|
||||
[] as string[],
|
||||
)
|
||||
.sort();
|
||||
|
||||
/**
|
||||
* @param {string} ip
|
||||
* @param {number} [port]
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getDnsAddress = (ip: any, port = 0) => {
|
||||
const isStandardDnsPort = port === STANDARD_DNS_PORT;
|
||||
let address = ip;
|
||||
|
||||
if (port) {
|
||||
if (ip.includes(':') && !isStandardDnsPort) {
|
||||
address = `[${ip}]:${port}`;
|
||||
} else if (!isStandardDnsPort) {
|
||||
address = `${ip}:${port}`;
|
||||
}
|
||||
}
|
||||
|
||||
return address;
|
||||
};
|
||||
|
||||
const normalizeHost = (host: string) => {
|
||||
const isIpv6 = host.includes(':');
|
||||
if (!isIpv6) {
|
||||
|
|
@ -313,7 +319,7 @@ const normalizeHost = (host: string) => {
|
|||
* @param {number} [port]
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getWebAddress = (ip: any, port = 0) => {
|
||||
export const getWebAddress = (ip: string, port: number = 0): string => {
|
||||
const isStandardWebPort = port === STANDARD_WEB_PORT;
|
||||
const rawHost = String(ip);
|
||||
|
||||
|
|
@ -323,7 +329,7 @@ export const getWebAddress = (ip: any, port = 0) => {
|
|||
return `http://${host}${portPart}`;
|
||||
};
|
||||
|
||||
export const checkRedirect = (url: any, attempts: number = 1) => {
|
||||
export const checkRedirect = (url: string, attempts: number = 1): boolean => {
|
||||
let count = attempts || 1;
|
||||
|
||||
if (count > 10) {
|
||||
|
|
@ -331,11 +337,13 @@ export const checkRedirect = (url: any, attempts: number = 1) => {
|
|||
return false;
|
||||
}
|
||||
|
||||
const rmTimeout = (t: any) => t && clearTimeout(t);
|
||||
const setRecursiveTimeout = (time: any, ...args: any[]) =>
|
||||
setTimeout(checkRedirect, time, ...args);
|
||||
const rmTimeout = (t: ReturnType<typeof setTimeout> | undefined) => t && clearTimeout(t);
|
||||
const setRecursiveTimeout = (
|
||||
time: number,
|
||||
...args: [string, number]
|
||||
): ReturnType<typeof setTimeout> => setTimeout(checkRedirect, time, ...args);
|
||||
|
||||
let timeout: any;
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
fetch(url)
|
||||
.then((response) => {
|
||||
|
|
@ -354,7 +362,13 @@ export const checkRedirect = (url: any, attempts: number = 1) => {
|
|||
return false;
|
||||
};
|
||||
|
||||
export const redirectToCurrentProtocol = (values: any, httpPort = 80) => {
|
||||
type RedirectValues = {
|
||||
enabled?: boolean;
|
||||
force_https?: boolean;
|
||||
port_https?: number;
|
||||
};
|
||||
|
||||
export const redirectToCurrentProtocol = (values: RedirectValues, httpPort = 80) => {
|
||||
const { protocol, hostname, hash, port } = window.location;
|
||||
const { enabled, force_https, port_https } = values;
|
||||
const httpsPort = port_https !== STANDARD_HTTPS_PORT ? `:${port_https}` : '';
|
||||
|
|
@ -377,36 +391,21 @@ export const redirectToCurrentProtocol = (values: any, httpPort = 80) => {
|
|||
* @param {string} text
|
||||
* @returns []string
|
||||
*/
|
||||
export const splitByNewLine = (text: any) => {
|
||||
export const splitByNewLine = (text: string | undefined | null): string[] => {
|
||||
if (!text) {
|
||||
return [];
|
||||
}
|
||||
return text.split('\n').filter((n: any) => n.trim());
|
||||
return text.split('\n').filter((n: string) => n.trim());
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
export const trimMultilineString = (text: any) =>
|
||||
splitByNewLine(text)
|
||||
.map((line: any) => line.trim())
|
||||
.join('\n');
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
export const removeEmptyLines = (text: any) => splitByNewLine(text).join('\n');
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @returns {string}
|
||||
*/
|
||||
export const trimLinesAndRemoveEmpty = (input: any) =>
|
||||
export const trimLinesAndRemoveEmpty = (input: string): string =>
|
||||
input
|
||||
.split('\n')
|
||||
.map((line: any) => line.trim())
|
||||
.map((line: string) => line.trim())
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
|
|
@ -422,9 +421,14 @@ export const trimLinesAndRemoveEmpty = (input: any) =>
|
|||
* @returns {Object.<string, number>} normalizedTopClients.auto - auto clients
|
||||
* @returns {Object.<string, number>} normalizedTopClients.configured - configured clients
|
||||
*/
|
||||
export const normalizeTopClients = (topClients: any) =>
|
||||
export const normalizeTopClients = (
|
||||
topClients: (TopStat & { info: ClientFindSubEntry })[],
|
||||
): { auto: Record<string, number>; configured: Record<string, number> } =>
|
||||
topClients.reduce(
|
||||
(acc: any, clientObj: any) => {
|
||||
(
|
||||
acc: { auto: Record<string, number>; configured: Record<string, number> },
|
||||
clientObj: TopStat & { info: ClientFindSubEntry },
|
||||
) => {
|
||||
const {
|
||||
name,
|
||||
count,
|
||||
|
|
@ -440,35 +444,14 @@ export const normalizeTopClients = (topClients: any) =>
|
|||
},
|
||||
);
|
||||
|
||||
export const sortClients = (clients: any) => {
|
||||
const compare = (a: any, b: any) => {
|
||||
const nameA = a.name.toUpperCase();
|
||||
const nameB = b.name.toUpperCase();
|
||||
export const msToSeconds = (milliseconds: number): number => Math.floor(milliseconds / 1000);
|
||||
|
||||
if (nameA > nameB) {
|
||||
return 1;
|
||||
}
|
||||
if (nameA < nameB) {
|
||||
return -1;
|
||||
}
|
||||
export const msToMinutes = (milliseconds: number): number => Math.floor(milliseconds / 1000 / 60);
|
||||
|
||||
return 0;
|
||||
};
|
||||
export const msToHours = (milliseconds: number): number =>
|
||||
Math.floor(milliseconds / 1000 / 60 / 60);
|
||||
|
||||
return clients.sort(compare);
|
||||
};
|
||||
|
||||
export const toggleAllServices = (services: any, change: any, isSelected: any) => {
|
||||
services.forEach((service: any) => change(`blocked_services.${service.id}`, isSelected));
|
||||
};
|
||||
|
||||
export const msToSeconds = (milliseconds: any) => Math.floor(milliseconds / 1000);
|
||||
|
||||
export const msToMinutes = (milliseconds: any) => Math.floor(milliseconds / 1000 / 60);
|
||||
|
||||
export const msToHours = (milliseconds: any) => Math.floor(milliseconds / 1000 / 60 / 60);
|
||||
|
||||
export const secondsToMilliseconds = (seconds: any) => {
|
||||
export const secondsToMilliseconds = (seconds: number): number => {
|
||||
if (seconds) {
|
||||
return seconds * 1000;
|
||||
}
|
||||
|
|
@ -476,12 +459,12 @@ export const secondsToMilliseconds = (seconds: any) => {
|
|||
return seconds;
|
||||
};
|
||||
|
||||
export const msToDays = (milliseconds: any) => Math.floor(milliseconds / 1000 / 60 / 60 / 24);
|
||||
|
||||
export const normalizeRulesTextarea = (text: any) =>
|
||||
export const normalizeRulesTextarea = (text: string): string | undefined =>
|
||||
text?.replace(/^\n/g, '').replace(/\n\s*\n/g, '\n');
|
||||
|
||||
export const normalizeWhois = (whois: any) => {
|
||||
export const normalizeWhois = (
|
||||
whois: QueryLogItemClientWhois,
|
||||
): Partial<QueryLogItemClientWhois> & { location?: string } => {
|
||||
if (Object.keys(whois).length > 0) {
|
||||
const { city, country, ...values } = whois;
|
||||
let location = country || '';
|
||||
|
|
@ -508,7 +491,10 @@ export const normalizeWhois = (whois: any) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const getPathWithQueryString = (path: any, params: any) => {
|
||||
export const getPathWithQueryString = (
|
||||
path: string,
|
||||
params: Record<string, string | string[] | undefined | null> | undefined,
|
||||
): string => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
Object.entries(params || {}).forEach(([key, value]) => {
|
||||
|
|
@ -530,12 +516,16 @@ export const getPathWithQueryString = (path: any, params: any) => {
|
|||
return `${path}?${searchParams.toString()}`;
|
||||
};
|
||||
|
||||
export const getParamsForClientsSearch = (data: any, param: any, additionalParam?: any) => {
|
||||
const clients = new Set();
|
||||
data.forEach((e: any) => {
|
||||
clients.add(e[param]);
|
||||
if (e[additionalParam]) {
|
||||
clients.add(e[additionalParam]);
|
||||
export const getParamsForClientsSearch = (
|
||||
data: Record<string, unknown>[],
|
||||
param: string,
|
||||
additionalParam?: string,
|
||||
): { clients: { id: string }[] } => {
|
||||
const clients = new Set<string | number>();
|
||||
data.forEach((e: Record<string, unknown>) => {
|
||||
clients.add(e[param] as string | number);
|
||||
if (e[additionalParam as string]) {
|
||||
clients.add(e[additionalParam as string] as string | number);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -544,67 +534,10 @@ export const getParamsForClientsSearch = (data: any, param: any, additionalParam
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates onBlur handler that can normalize input if normalization function is specified
|
||||
*
|
||||
* @param {Object} event
|
||||
* @param {Object} event.target
|
||||
* @param {string} event.target.value
|
||||
* @param {Object} input
|
||||
* @param {function} input.onBlur
|
||||
* @param {function} [normalizeOnBlur]
|
||||
* @returns {function}
|
||||
*/
|
||||
|
||||
export const checkFiltered = (reason: any) => reason.indexOf(FILTERED) === 0;
|
||||
export const checkRewrite = (reason: any) => reason === FILTERED_STATUS.REWRITE;
|
||||
export const checkRewriteHosts = (reason: any) => reason === FILTERED_STATUS.REWRITE_HOSTS;
|
||||
export const checkBlackList = (reason: any) => reason === FILTERED_STATUS.FILTERED_BLACK_LIST;
|
||||
export const checkWhiteList = (reason: any) => reason === FILTERED_STATUS.NOT_FILTERED_WHITE_LIST;
|
||||
// eslint-disable-next-line max-len
|
||||
export const checkNotFilteredNotFound = (reason: any) =>
|
||||
reason === FILTERED_STATUS.NOT_FILTERED_NOT_FOUND;
|
||||
export const checkSafeSearch = (reason: any) => reason === FILTERED_STATUS.FILTERED_SAFE_SEARCH;
|
||||
export const checkSafeBrowsing = (reason: any) => reason === FILTERED_STATUS.FILTERED_SAFE_BROWSING;
|
||||
export const checkParental = (reason: any) => reason === FILTERED_STATUS.FILTERED_PARENTAL;
|
||||
export const checkBlockedService = (reason: any) =>
|
||||
export const checkFiltered = (reason: FilteringReason): boolean => reason.indexOf(FILTERED) === 0;
|
||||
export const checkBlockedService = (reason: FilteringReason): boolean =>
|
||||
reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
|
||||
|
||||
export const getCurrentFilter = (url: any, filters: any) => {
|
||||
const filter = filters?.find((item: any) => url === item.url);
|
||||
|
||||
if (filter) {
|
||||
const { enabled, name, url } = filter;
|
||||
return {
|
||||
enabled,
|
||||
name,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
name: '',
|
||||
url: '',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {object} initialValues
|
||||
* @param {object} values
|
||||
* @returns {object} Returns different values of objects
|
||||
*/
|
||||
|
||||
export const getObjDiff = (initialValues: any, values: any) =>
|
||||
Object.entries(values)
|
||||
|
||||
.reduce((acc: any, [key, value]) => {
|
||||
if (value !== initialValues[key]) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* @param num {number} to format
|
||||
* @returns {string} Returns a string with a language-sensitive representation of this number
|
||||
|
|
@ -641,24 +574,12 @@ export const formatCompactNumber = (num: number, decimals: number = 1): string =
|
|||
return sign + formatted + suffix;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param arr {array}
|
||||
* @param key {string}
|
||||
* @param value {string}
|
||||
* @returns {object}
|
||||
*/
|
||||
export const getMap = (arr: any, key: any, value: any) =>
|
||||
arr.reduce((acc: any, curr: any) => {
|
||||
acc[curr[key]] = curr[value];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* @param parsedIp {object} ipaddr.js IPv4 or IPv6 object
|
||||
* @param parsedCidr {array} ipaddr.js CIDR array
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isIpMatchCidr = (parsedIp: any, parsedCidr: any) => {
|
||||
const isIpMatchCidr = (parsedIp: IPv4 | IPv6, parsedCidr: [IPv4 | IPv6, number]): boolean => {
|
||||
try {
|
||||
const cidrIpVersion = parsedCidr[0].kind();
|
||||
const ipVersion = parsedIp.kind();
|
||||
|
|
@ -669,7 +590,7 @@ const isIpMatchCidr = (parsedIp: any, parsedCidr: any) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const isIpInCidr = (ip: any, cidr: any) => {
|
||||
export const isIpInCidr = (ip: string, cidr: string): boolean => {
|
||||
try {
|
||||
const parsedIp = ipaddr.parse(ip);
|
||||
const parsedCidr = ipaddr.parseCIDR(cidr);
|
||||
|
|
@ -699,7 +620,7 @@ export const isValidIpv6 = (value: string): boolean => {
|
|||
* @param {string} subnetMask
|
||||
* @returns {IPv4 | null}
|
||||
*/
|
||||
export const parseSubnetMask = (subnetMask: any) => {
|
||||
export const parseSubnetMask = (subnetMask: string): number | null => {
|
||||
try {
|
||||
return ipaddr.parse(subnetMask).prefixLengthFromSubnetMask();
|
||||
} catch (e) {
|
||||
|
|
@ -713,8 +634,10 @@ export const parseSubnetMask = (subnetMask: any) => {
|
|||
* @param {string} subnetMask
|
||||
* @returns {*}
|
||||
*/
|
||||
export const subnetMaskToBitMask = (subnetMask: any) =>
|
||||
subnetMask.split('.').reduce((acc: any, cur: any) => acc - Math.log2(256 - Number(cur)), 32);
|
||||
export const subnetMaskToBitMask = (subnetMask: string): number =>
|
||||
subnetMask
|
||||
.split('.')
|
||||
.reduce((acc: number, cur: string) => acc - Math.log2(256 - Number(cur)), 32);
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -722,7 +645,7 @@ export const subnetMaskToBitMask = (subnetMask: any) =>
|
|||
* @returns {'IP' | 'CIDR' | 'CLIENT_ID' | 'UNKNOWN'}
|
||||
*
|
||||
*/
|
||||
export const findAddressType = (address: any) => {
|
||||
export const findAddressType = (address: string): string => {
|
||||
try {
|
||||
const cidrMaybe = address.includes('/');
|
||||
|
||||
|
|
@ -746,9 +669,11 @@ export const findAddressType = (address: any) => {
|
|||
* @param ids {string[]}
|
||||
* @returns {Object}
|
||||
*/
|
||||
export const separateIpsAndCidrs = (ids: any) =>
|
||||
export const separateIpsAndCidrs = (
|
||||
ids: string[],
|
||||
): { ips: string[]; cidrs: string[]; clientIds: string[] } =>
|
||||
ids.reduce(
|
||||
(acc: any, curr: any) => {
|
||||
(acc: { ips: string[]; cidrs: string[]; clientIds: string[] }, curr: string) => {
|
||||
const addressType = findAddressType(curr);
|
||||
|
||||
if (addressType === ADDRESS_TYPES.IP) {
|
||||
|
|
@ -765,25 +690,28 @@ export const separateIpsAndCidrs = (ids: any) =>
|
|||
{ ips: [], cidrs: [], clientIds: [] },
|
||||
);
|
||||
|
||||
export const countClientsStatistics = (ids: any, autoClients: any) => {
|
||||
export const countClientsStatistics = (
|
||||
ids: string[],
|
||||
autoClients: Record<string, number>,
|
||||
): number => {
|
||||
const { ips, cidrs, clientIds } = separateIpsAndCidrs(ids);
|
||||
|
||||
const ipsCount = ips.reduce((acc: any, curr: any) => {
|
||||
const ipsCount = ips.reduce((acc: number, curr: string) => {
|
||||
const count = autoClients[curr] || 0;
|
||||
return acc + count;
|
||||
}, 0);
|
||||
|
||||
const clientIdsCount = clientIds.reduce((acc: any, curr: any) => {
|
||||
const clientIdsCount = clientIds.reduce((acc: number, curr: string) => {
|
||||
const count = autoClients[curr] || 0;
|
||||
return acc + count;
|
||||
}, 0);
|
||||
|
||||
const cidrsCount = Object.entries(autoClients).reduce((acc: any, curr: any) => {
|
||||
const cidrsCount = Object.entries(autoClients).reduce((acc: number, curr: [string, number]) => {
|
||||
const [id, count] = curr;
|
||||
if (!ipaddr.isValid(id)) {
|
||||
return acc;
|
||||
}
|
||||
if (cidrs.some((cidr: any) => isIpInCidr(id, cidr))) {
|
||||
if (cidrs.some((cidr: string) => isIpInCidr(id, cidr))) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
acc += count;
|
||||
}
|
||||
|
|
@ -814,7 +742,7 @@ export const formatElapsedMs = (elapsedMs: string, millisecondsLabel: string) =>
|
|||
/**
|
||||
* @param language {string}
|
||||
*/
|
||||
export const setHtmlLangAttr = (language: any) => {
|
||||
export const setHtmlLangAttr = (language: string): void => {
|
||||
window.document.documentElement.lang = language;
|
||||
};
|
||||
|
||||
|
|
@ -823,7 +751,7 @@ export const setHtmlLangAttr = (language: any) => {
|
|||
*
|
||||
* @param {string} theme
|
||||
*/
|
||||
export const setTheme = (theme: any) => {
|
||||
export const setTheme = (theme: string): void => {
|
||||
LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.THEME, theme);
|
||||
};
|
||||
|
||||
|
|
@ -833,14 +761,15 @@ export const setTheme = (theme: any) => {
|
|||
* @returns {string}
|
||||
*/
|
||||
|
||||
export const getTheme = () => LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.THEME) || THEMES.light;
|
||||
export const getTheme = () =>
|
||||
LocalStorageHelper.getItem<string>(LOCAL_STORAGE_KEYS.THEME) || THEMES.light;
|
||||
|
||||
/**
|
||||
* Sets UI theme.
|
||||
*
|
||||
* @param theme
|
||||
*/
|
||||
export const setUITheme = (theme: any) => {
|
||||
export const setUITheme = (theme?: string): void => {
|
||||
let currentTheme = theme || getTheme();
|
||||
|
||||
if (currentTheme === THEMES.auto) {
|
||||
|
|
@ -853,25 +782,6 @@ export const setUITheme = (theme: any) => {
|
|||
document.documentElement.style.colorScheme = currentTheme;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param values {object}
|
||||
* @returns {object}
|
||||
*/
|
||||
|
||||
export const replaceEmptyStringsWithZeroes = (values: any) =>
|
||||
Object.entries(values)
|
||||
|
||||
.reduce((acc: any, [key, value]) => {
|
||||
acc[key] = value === '' ? 0 : value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* @param value {number || string}
|
||||
* @returns {string}
|
||||
*/
|
||||
export const replaceZeroWithEmptyString = (value: any) => (parseInt(value, 10) === 0 ? '' : value);
|
||||
|
||||
/**
|
||||
* @param {string} search
|
||||
* @param {string} status
|
||||
|
|
@ -885,34 +795,11 @@ export const getLogsUrlParams = (search: string, status: string, reason: string)
|
|||
reason: reason || undefined,
|
||||
})}`;
|
||||
|
||||
export const processContent = (content: any) =>
|
||||
Array.isArray(content)
|
||||
? content.filter(([, value]) => value).reduce((acc, val) => acc.concat(val), [])
|
||||
: content;
|
||||
|
||||
// TODO check getObjectKeysSorted
|
||||
type NestedObject = {
|
||||
[key: string]: any;
|
||||
order: number;
|
||||
};
|
||||
|
||||
export const getObjectKeysSorted = <
|
||||
T extends Record<string, NestedObject>,
|
||||
K extends keyof NestedObject,
|
||||
>(
|
||||
object: T,
|
||||
sortKey: K,
|
||||
): string[] => {
|
||||
return Object.entries(object)
|
||||
.sort(([, a], [, b]) => (a[sortKey] as number) - (b[sortKey] as number))
|
||||
.map(([key]) => key);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param ip
|
||||
* @returns {[IPv4|IPv6, 33|129]}
|
||||
*/
|
||||
const getParsedIpWithPrefixLength = (ip: any) => {
|
||||
const getParsedIpWithPrefixLength = (ip: string): [IPv4 | IPv6, number] => {
|
||||
const MAX_PREFIX_LENGTH_V4 = 32;
|
||||
const MAX_PREFIX_LENGTH_V6 = 128;
|
||||
|
||||
|
|
@ -928,7 +815,7 @@ const getParsedIpWithPrefixLength = (ip: any) => {
|
|||
* @param item - ip or cidr
|
||||
* @returns {number[]}
|
||||
*/
|
||||
const getAddressesComparisonBytes = (item: any) => {
|
||||
const getAddressesComparisonBytes = (item: string): number[] => {
|
||||
// Sort ipv4 before ipv6
|
||||
const IP_V4_COMPARISON_CODE = 0;
|
||||
const IP_V6_COMPARISON_CODE = 1;
|
||||
|
|
@ -979,7 +866,7 @@ export const sortIp = (a: string, b: string): number => {
|
|||
* @param {number} filterId
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getSpecialFilterName = (filterId: any) => {
|
||||
export const getSpecialFilterName = (filterId: number): string => {
|
||||
switch (filterId) {
|
||||
case SPECIAL_FILTER_ID.CUSTOM_FILTERING_RULES:
|
||||
return intl.getMessage('custom_rules');
|
||||
|
|
@ -1030,97 +917,10 @@ export const getFilterName = (
|
|||
};
|
||||
|
||||
export const getFilterNames = (rules: Rule[], filters: Filter[], whitelistFilters: Filter[]) =>
|
||||
rules.map(({ filter_list_id }: any) =>
|
||||
rules.map(({ filter_list_id }: Rule) =>
|
||||
getFilterName(filters, whitelistFilters, filter_list_id),
|
||||
);
|
||||
|
||||
export const getRuleNames = (rules: Rule[]) => rules.map(({ text }: Rule) => text);
|
||||
|
||||
export const getFilterNameToRulesMap = (
|
||||
rules: Rule[],
|
||||
filters: Filter[],
|
||||
whitelistFilters: Filter[],
|
||||
) =>
|
||||
rules.reduce((acc: any, { text, filter_list_id }: Rule) => {
|
||||
const filterName = getFilterName(filters, whitelistFilters, filter_list_id);
|
||||
|
||||
acc[filterName] = (acc[filterName] || []).concat(text);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
export const getRulesToFilterList = (
|
||||
rules: Rule[],
|
||||
filters: Filter[],
|
||||
whitelistFilters: Filter[],
|
||||
classes = {
|
||||
list: 'filteringRules',
|
||||
rule: 'filteringRules__rule font-monospace',
|
||||
filter: 'filteringRules__filter',
|
||||
},
|
||||
) => {
|
||||
const filterNameToRulesMap: { string: string[] } = getFilterNameToRulesMap(
|
||||
rules,
|
||||
filters,
|
||||
whitelistFilters,
|
||||
);
|
||||
|
||||
return (
|
||||
<dl class={classes.list}>
|
||||
{Object.entries(filterNameToRulesMap).reduce(
|
||||
(acc: any, [filterName, rulesArr]) =>
|
||||
acc
|
||||
.concat(
|
||||
rulesArr.map((rule: any, _i: any) => (
|
||||
<dd class={classes.rule}>{rule}</dd>
|
||||
)),
|
||||
)
|
||||
.concat(<dt class={classes.filter}>{filterName}</dt>),
|
||||
[],
|
||||
)}
|
||||
</dl>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param ip {string}
|
||||
* @param gateway_ip {string}
|
||||
* @returns {{range_end: string, subnet_mask: string, range_start: string,
|
||||
* lease_duration: string, gateway_ip: string}}
|
||||
*/
|
||||
export const calculateDhcpPlaceholdersIpv4 = (ip: string, gateway_ip: string) => {
|
||||
const LAST_OCTET_IDX = 3;
|
||||
const LAST_OCTET_RANGE_START = 100;
|
||||
const LAST_OCTET_RANGE_END = 200;
|
||||
|
||||
const addr = ipaddr.parse(ip) as IPv4;
|
||||
|
||||
addr.octets[LAST_OCTET_IDX] = LAST_OCTET_RANGE_START;
|
||||
const range_start = addr.toString();
|
||||
|
||||
addr.octets[LAST_OCTET_IDX] = LAST_OCTET_RANGE_END;
|
||||
const range_end = addr.toString();
|
||||
|
||||
const { subnet_mask, lease_duration } = DHCP_VALUES_PLACEHOLDERS.ipv4;
|
||||
|
||||
return {
|
||||
gateway_ip: gateway_ip || ip,
|
||||
subnet_mask,
|
||||
range_start,
|
||||
range_end,
|
||||
lease_duration,
|
||||
};
|
||||
};
|
||||
|
||||
export const calculateDhcpPlaceholdersIpv6 = () => {
|
||||
const { range_start, range_end, lease_duration } = DHCP_VALUES_PLACEHOLDERS.ipv6;
|
||||
|
||||
return {
|
||||
range_start,
|
||||
range_end,
|
||||
lease_duration,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Add ip_addresses property - concatenated ipv4_addresses and ipv6_addresses for every interface
|
||||
* @param interfaces
|
||||
|
|
@ -1138,61 +938,73 @@ export const enrichWithConcatenatedIpAddresses = (interfaces: NetInterfaces): Dh
|
|||
return acc;
|
||||
}, {});
|
||||
|
||||
export const isScrolledIntoView = (el: any) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const elemTop = rect.top;
|
||||
const elemBottom = rect.bottom;
|
||||
|
||||
return elemTop < window.innerHeight && elemBottom >= 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* If this is a manually created client, return its name.
|
||||
* If this is a "runtime" client, return it's IP address.
|
||||
* @param clients {Array.<object>}
|
||||
* @param ip {string}
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getBlockingClientName = (clients: any, ip: any) => {
|
||||
for (let i = 0; i < clients.length; i += 1) {
|
||||
const client = clients[i];
|
||||
|
||||
if (client.ids.includes(ip)) {
|
||||
return client.name;
|
||||
}
|
||||
}
|
||||
return ip;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string[]} lines
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export const filterOutComments = (lines: any) =>
|
||||
lines.filter((line: any) => !line.startsWith(COMMENT_LINE_DEFAULT_TOKEN));
|
||||
export const filterOutComments = (lines: string[]): string[] =>
|
||||
lines.filter((line: string) => !line.startsWith(COMMENT_LINE_DEFAULT_TOKEN));
|
||||
|
||||
export const isCommentLine = (line: string) => /^\s*[#!]/.test(line);
|
||||
/**
|
||||
* Computes DHCP v4 placeholder values from the interface IP address.
|
||||
* Replaces the last octet with 100 for range_start and 200 for range_end.
|
||||
* @param ip - The interface's IPv4 address (e.g. "192.168.1.1")
|
||||
* @param gatewayIp - The interface's gateway IP (falls back to `ip` if empty)
|
||||
* @returns Pre-filled v4 config values
|
||||
*/
|
||||
export const calculateDhcpPlaceholdersIpv4 = (ip: string, gatewayIp: string) => {
|
||||
const LAST_OCTET_IDX = 3;
|
||||
const LAST_OCTET_RANGE_START = 100;
|
||||
const LAST_OCTET_RANGE_END = 200;
|
||||
|
||||
const addr = ipaddr.parse(ip) as IPv4;
|
||||
|
||||
addr.octets[LAST_OCTET_IDX] = LAST_OCTET_RANGE_START;
|
||||
const range_start = addr.toString();
|
||||
|
||||
addr.octets[LAST_OCTET_IDX] = LAST_OCTET_RANGE_END;
|
||||
const range_end = addr.toString();
|
||||
|
||||
const { subnet_mask, lease_duration } = DHCP_VALUES_PLACEHOLDERS.ipv4;
|
||||
|
||||
return {
|
||||
gateway_ip: gatewayIp || ip,
|
||||
subnet_mask,
|
||||
range_start,
|
||||
range_end,
|
||||
lease_duration,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes DHCP v6 placeholder values (static defaults).
|
||||
* @returns Pre-filled v6 config values
|
||||
*/
|
||||
export const calculateDhcpPlaceholdersIpv6 = () => {
|
||||
const { range_start, range_end, lease_duration } = DHCP_VALUES_PLACEHOLDERS.ipv6;
|
||||
|
||||
return {
|
||||
range_start,
|
||||
range_end,
|
||||
lease_duration,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {array} services
|
||||
* @param {string} id
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getService = (services: any, id: any) => services.find((s: any) => s.id === id);
|
||||
export const getService = (services: ServiceEntry[], id: string): ServiceEntry | undefined =>
|
||||
services.find((s: ServiceEntry) => s.id === id);
|
||||
|
||||
/**
|
||||
* @param {array} services
|
||||
* @param {string} id
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getServiceName = (services: any, id: any) => getService(services, id)?.name;
|
||||
|
||||
/**
|
||||
* @param {array} services
|
||||
* @param {string} id
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getServiceIcon = (services: any, id: any) => getService(services, id)?.icon_svg;
|
||||
export const getServiceName = (services: ServiceEntry[], id: string): string | undefined =>
|
||||
getService(services, id)?.name;
|
||||
|
||||
/**
|
||||
* Decodes a base64-encoded SVG string. Returns an empty string on failure.
|
||||
|
|
|
|||
|
|
@ -9,29 +9,29 @@ export const LOCAL_STORAGE_KEYS = {
|
|||
};
|
||||
|
||||
export const LocalStorageHelper = {
|
||||
setItem(key: any, value: any) {
|
||||
setItem(key: string, value: unknown) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch (error) {
|
||||
console.error(`Error setting ${key} in local storage: ${error.message}`);
|
||||
console.error(`Error setting ${key} in local storage: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
|
||||
getItem(key: any) {
|
||||
getItem<T = unknown>(key: string): T | null {
|
||||
try {
|
||||
const item = localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : null;
|
||||
return item ? (JSON.parse(item) as T) : null;
|
||||
} catch (error) {
|
||||
console.error(`Error getting ${key} from local storage: ${error.message}`);
|
||||
console.error(`Error getting ${key} from local storage: ${(error as Error).message}`);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
removeItem(key: any) {
|
||||
removeItem(key: string) {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch (error) {
|
||||
console.error(`Error removing ${key} from local storage: ${error.message}`);
|
||||
console.error(`Error removing ${key} from local storage: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ export const LocalStorageHelper = {
|
|||
try {
|
||||
localStorage.clear();
|
||||
} catch (error) {
|
||||
console.error(`Error clearing local storage: ${error.message}`);
|
||||
console.error(`Error clearing local storage: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,23 +1,31 @@
|
|||
import { Show } from 'solid-js';
|
||||
import type { JSXElement } from 'solid-js';
|
||||
|
||||
import { A } from '@solidjs/router';
|
||||
|
||||
import { normalizeWhois } from './helpers';
|
||||
import { WHOIS_ICONS } from './constants';
|
||||
import type { QueryLogItemClientWhois } from 'panel/api/model/queryLogItemClientWhois';
|
||||
|
||||
const getFormattedWhois = (whois: any) => {
|
||||
type ClientCellInfo = {
|
||||
name?: string;
|
||||
whois_info?: QueryLogItemClientWhois;
|
||||
};
|
||||
|
||||
const getFormattedWhois = (whois: QueryLogItemClientWhois) => {
|
||||
const whoisInfo = normalizeWhois(whois);
|
||||
return Object.keys(whoisInfo).map((key) => {
|
||||
return Object.entries(whoisInfo).map(([key, value]) => {
|
||||
const icon = WHOIS_ICONS[key as keyof typeof WHOIS_ICONS];
|
||||
const strValue = String(value ?? '');
|
||||
return (
|
||||
<span class="logs__whois text-muted" title={whoisInfo[key]}>
|
||||
<span class="logs__whois text-muted" title={strValue}>
|
||||
<Show when={icon}>
|
||||
<svg class="logs__whois-icon icons icon--18">
|
||||
<use href={`#${icon}`} />
|
||||
</svg>
|
||||
|
||||
</Show>
|
||||
{whoisInfo[key]}
|
||||
{strValue}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
|
@ -33,13 +41,13 @@ const getFormattedWhois = (whois: any) => {
|
|||
* @returns {JSXElement}
|
||||
*/
|
||||
export const renderFormattedClientCell = (
|
||||
value: any,
|
||||
info: any,
|
||||
value: string,
|
||||
info: ClientCellInfo | null,
|
||||
isDetailed = false,
|
||||
isLogs = false,
|
||||
) => {
|
||||
let whoisContainer = null;
|
||||
let nameContainer: any = value;
|
||||
let whoisContainer: JSXElement = null;
|
||||
let nameContainer: JSXElement = value;
|
||||
|
||||
if (info) {
|
||||
const { name, whois_info } = info;
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
import { createSignal, createEffect, onCleanup } from 'solid-js';
|
||||
|
||||
const useDebounce = (value: any, delay: any) => {
|
||||
const [debouncedValue, setDebouncedValue] = createSignal(value);
|
||||
|
||||
createEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
onCleanup(() => {
|
||||
clearTimeout(handler);
|
||||
});
|
||||
});
|
||||
|
||||
return [debouncedValue, setDebouncedValue];
|
||||
};
|
||||
|
||||
export default useDebounce;
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
* @param right {string} - right version
|
||||
* @return {boolean} true if versions are equal
|
||||
*/
|
||||
export const areEqualVersions = (left: any, right: any) => {
|
||||
export const areEqualVersions = (left: string, right: string): boolean => {
|
||||
if (!left || !right) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,29 @@ import {
|
|||
TIME_UNITS,
|
||||
} from './helpers/constants';
|
||||
import { DEFAULT_BLOCKING_IPV4, DEFAULT_BLOCKING_IPV6 } from './stores/dnsConfig';
|
||||
import { Filter } from './helpers/helpers';
|
||||
import { SafeSearchConfig } from './api/model/safeSearchConfig';
|
||||
import { Filter, type NormalizedQueryLogItem } from './helpers/helpers';
|
||||
import type { WhoisInfo } from './api/model/whoisInfo';
|
||||
import type { ClientAuto as AutoClient } from './api/model/clientAuto';
|
||||
import type { Client } from './api/model/client';
|
||||
import type { NetInterface } from './api/model/netInterface';
|
||||
import type { NetInterfaces } from './api/model/netInterfaces';
|
||||
import type { TlsConfig } from './api/model/tlsConfig';
|
||||
import type { TlsConfigKeyType } from './api/model/tlsConfigKeyType';
|
||||
import type { DnsInfo200 } from './api/model/dnsInfo200';
|
||||
import type { DNSConfigBlockingMode, DNSConfigUpstreamMode } from './api/model';
|
||||
import type { FilterStatus } from './api/model/filterStatus';
|
||||
import type { DhcpStaticLease } from './api/model/dhcpStaticLease';
|
||||
import type { DhcpSearchResult } from './api/model/dhcpSearchResult';
|
||||
import type { Stats } from './api/model/stats';
|
||||
import type { GetStatsConfigResponse } from './api/model/getStatsConfigResponse';
|
||||
import type { GetQueryLogConfigResponse } from './api/model/getQueryLogConfigResponse';
|
||||
import type { RewriteEntry } from './api/model/rewriteEntry';
|
||||
import type { RewriteSettings } from './api/model/rewriteSettings';
|
||||
import type { BlockedServicesSchedule } from './api/model/blockedServicesSchedule';
|
||||
import type { BlockedService } from './api/model/blockedService';
|
||||
import type { ServiceGroup } from './api/model/serviceGroup';
|
||||
import type { QueryLogFilter } from './helpers/constants';
|
||||
import type { ToastNotice } from './stores/toasts';
|
||||
|
||||
export type InstallInterface = {
|
||||
flags: string;
|
||||
|
|
@ -52,77 +73,26 @@ export type InstallData = {
|
|||
dnsVersion: string;
|
||||
};
|
||||
|
||||
export type EncryptionData = {
|
||||
export type EncryptionData = Partial<
|
||||
Omit<TlsConfig, 'port_https' | 'port_dns_over_tls' | 'port_dns_over_quic' | 'dns_names'>
|
||||
> & {
|
||||
// UI-only fields NOT in API model:
|
||||
processing: boolean;
|
||||
processingConfig: boolean;
|
||||
processingValidate: boolean;
|
||||
enabled: boolean;
|
||||
serve_plain_dns: boolean;
|
||||
dns_names: any;
|
||||
force_https: boolean;
|
||||
issuer: string;
|
||||
key_type: string;
|
||||
not_after: string;
|
||||
not_before: string;
|
||||
port_dns_over_tls?: number;
|
||||
port_dns_over_quic?: number;
|
||||
port_https?: number;
|
||||
port_dnscrypt?: number;
|
||||
subject: string;
|
||||
valid_chain: boolean;
|
||||
valid_key: boolean;
|
||||
valid_cert: boolean;
|
||||
valid_pair: boolean;
|
||||
status_cert: string;
|
||||
status_key: string;
|
||||
private_key: string;
|
||||
server_name: string;
|
||||
warning_validation: string;
|
||||
certificate_chain: string;
|
||||
certificate_path: string;
|
||||
private_key_path: string;
|
||||
private_key_saved: boolean;
|
||||
allow_unencrypted_doh?: boolean;
|
||||
dnscrypt_config_file?: string;
|
||||
status_cert: string; // UI concatenation
|
||||
status_key: string; // UI concatenation
|
||||
allow_unencrypted_doh: boolean;
|
||||
// Port fields: number from API, string from form input (initialized as ''):
|
||||
port_https: number | string;
|
||||
port_dns_over_tls: number | string;
|
||||
port_dns_over_quic: number | string;
|
||||
port_dnscrypt: number | string;
|
||||
// Store initializes as null, API returns string[]:
|
||||
dns_names: string[] | null;
|
||||
};
|
||||
|
||||
export type Client = {
|
||||
blocked_services: string[];
|
||||
blocked_services_schedule: {
|
||||
sun?: { start: number; end: number };
|
||||
mon?: { start: number; end: number };
|
||||
tue?: { start: number; end: number };
|
||||
wed?: { start: number; end: number };
|
||||
thu?: { start: number; end: number };
|
||||
fri?: { start: number; end: number };
|
||||
sat?: { start: number; end: number };
|
||||
time_zone: string;
|
||||
};
|
||||
filtering_enabled: boolean;
|
||||
ids: string[];
|
||||
ignore_querylog: boolean;
|
||||
ignore_statistics: boolean;
|
||||
name: string;
|
||||
parental_enabled: boolean;
|
||||
safe_search: SafeSearchConfig;
|
||||
safebrowsing_enabled: boolean;
|
||||
safesearch_enabled: boolean;
|
||||
tags: string[];
|
||||
upstreams: string[];
|
||||
upstreams_cache_enabled: boolean;
|
||||
upstreams_cache_size: number;
|
||||
use_global_blocked_services: boolean;
|
||||
use_global_settings: boolean;
|
||||
};
|
||||
|
||||
export type WhoisInfo = Record<string, string>;
|
||||
|
||||
export type AutoClient = {
|
||||
ip: string;
|
||||
name: string;
|
||||
source: string;
|
||||
whois_info: WhoisInfo;
|
||||
};
|
||||
export { type WhoisInfo, type AutoClient, type Client };
|
||||
|
||||
export type DashboardData = {
|
||||
processing: boolean;
|
||||
|
|
@ -132,7 +102,7 @@ export type DashboardData = {
|
|||
processingUpdate: boolean;
|
||||
processingProfile: boolean;
|
||||
protectionEnabled: boolean;
|
||||
protectionDisabledDuration: any;
|
||||
protectionDisabledDuration: number | null;
|
||||
protectionCountdownActive: boolean;
|
||||
processingProtection: boolean;
|
||||
httpPort: number;
|
||||
|
|
@ -167,7 +137,7 @@ export type SettingsData = {
|
|||
};
|
||||
};
|
||||
|
||||
export type RewritesData = {
|
||||
export type RewritesData = RewriteSettings & {
|
||||
processing: boolean;
|
||||
processingAdd: boolean;
|
||||
processingDelete: boolean;
|
||||
|
|
@ -175,17 +145,8 @@ export type RewritesData = {
|
|||
processingSettings: boolean;
|
||||
isModalOpen: boolean;
|
||||
modalType: string;
|
||||
currentRewrite?: {
|
||||
answer: string;
|
||||
domain: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
list: {
|
||||
answer: string;
|
||||
domain: string;
|
||||
enabled: boolean;
|
||||
}[];
|
||||
enabled: boolean;
|
||||
currentRewrite?: RewriteEntry;
|
||||
list: RewriteEntry[];
|
||||
};
|
||||
|
||||
export type NormalizedTopClients = {
|
||||
|
|
@ -193,37 +154,31 @@ export type NormalizedTopClients = {
|
|||
configured: Record<string, number>;
|
||||
};
|
||||
|
||||
export type StatsData = {
|
||||
processingGetConfig: boolean;
|
||||
processingSetConfig: boolean;
|
||||
processingStats: boolean;
|
||||
processingReset: boolean;
|
||||
interval: number;
|
||||
customInterval?: number;
|
||||
dnsQueries: number[];
|
||||
blockedFiltering: number[];
|
||||
replacedParental: number[];
|
||||
replacedSafebrowsing: number[];
|
||||
topBlockedDomains: { name: string; count: number }[];
|
||||
topClients: {
|
||||
name: string;
|
||||
count: number;
|
||||
info: any;
|
||||
}[];
|
||||
normalizedTopClients?: NormalizedTopClients;
|
||||
topQueriedDomains: { name: string; count: number }[];
|
||||
numBlockedFiltering: number;
|
||||
numDnsQueries: number;
|
||||
numReplacedParental: number;
|
||||
numReplacedSafebrowsing: number;
|
||||
numReplacedSafesearch: number;
|
||||
avgProcessingTime: number;
|
||||
timeUnits: string;
|
||||
enabled: boolean;
|
||||
topUpstreamsAvgTime: { name: string; count: number }[];
|
||||
topUpstreamsResponses: { name: string; count: number }[];
|
||||
ignored: string[];
|
||||
};
|
||||
export type StatsData = Omit<
|
||||
Stats,
|
||||
| 'top_queried_domains'
|
||||
| 'top_clients'
|
||||
| 'top_blocked_domains'
|
||||
| 'top_upstreams_responses'
|
||||
| 'top_upstreams_avg_time'
|
||||
| 'time_units'
|
||||
> &
|
||||
Omit<GetStatsConfigResponse, 'interval'> & {
|
||||
processingGetConfig: boolean;
|
||||
processingSetConfig: boolean;
|
||||
processingStats: boolean;
|
||||
processingReset: boolean;
|
||||
interval: number;
|
||||
customInterval?: number | null;
|
||||
// Normalized top stats (from normalizeTopStats):
|
||||
topBlockedDomains: { name: string; count: number }[];
|
||||
topClients: { name: string; count: number; info: string }[]; // info is string!
|
||||
topQueriedDomains: { name: string; count: number }[];
|
||||
topUpstreamsAvgTime: { name: string; count: number }[];
|
||||
topUpstreamsResponses: { name: string; count: number }[];
|
||||
normalizedTopClients?: NormalizedTopClients;
|
||||
timeUnits: string;
|
||||
};
|
||||
|
||||
export type ClientsData = {
|
||||
processing: boolean;
|
||||
|
|
@ -243,16 +198,7 @@ export type AccessData = {
|
|||
blocked_hosts: string;
|
||||
};
|
||||
|
||||
export type DhcpInterface = {
|
||||
name: string;
|
||||
flags: string;
|
||||
gateway_ip: string;
|
||||
ip_addresses: string[];
|
||||
ipv4_addresses: string[];
|
||||
ipv6_addresses: string[];
|
||||
hardware_address: string;
|
||||
};
|
||||
|
||||
export type DhcpInterface = NetInterface & { ip_addresses: string[] };
|
||||
export type DhcpInterfaces = Record<string, DhcpInterface>;
|
||||
|
||||
export type DhcpData = {
|
||||
|
|
@ -266,16 +212,9 @@ export type DhcpData = {
|
|||
processingUpdating: boolean;
|
||||
enabled: boolean;
|
||||
interface_name: string;
|
||||
check?: {
|
||||
v4?: {
|
||||
other_server?: { found: string; error?: string };
|
||||
static_ip?: { static: string; ip: string };
|
||||
};
|
||||
v6?: {
|
||||
other_server?: { found: string; error?: string };
|
||||
static_ip?: { static: string; ip: string };
|
||||
};
|
||||
};
|
||||
// Use generated DhcpSearchResult:
|
||||
check: DhcpSearchResult | null;
|
||||
// Keep inline v4/v6 (required — always present after init):
|
||||
v4: {
|
||||
gateway_ip: string;
|
||||
subnet_mask: string;
|
||||
|
|
@ -287,61 +226,43 @@ export type DhcpData = {
|
|||
range_start: string;
|
||||
lease_duration: number;
|
||||
};
|
||||
leases: {
|
||||
hostname: string;
|
||||
ip: string;
|
||||
mac: string;
|
||||
}[];
|
||||
staticLeases: {
|
||||
hostname: string;
|
||||
ip: string;
|
||||
mac: string;
|
||||
}[];
|
||||
// UI-normalized leases (flat without expires):
|
||||
leases: { hostname: string; ip: string; mac: string }[];
|
||||
staticLeases: DhcpStaticLease[];
|
||||
isModalOpen: boolean;
|
||||
leaseModalConfig?: {
|
||||
hostname: string;
|
||||
ip: string;
|
||||
mac: string;
|
||||
};
|
||||
leaseModalConfig?: { hostname: string; ip: string; mac: string };
|
||||
modalType: string;
|
||||
dhcp_available: boolean;
|
||||
interfaces?: DhcpInterfaces;
|
||||
interfaces?: NetInterfaces;
|
||||
};
|
||||
|
||||
export type DnsConfigData = {
|
||||
export type DnsConfigData = Omit<
|
||||
DnsInfo200,
|
||||
| 'upstream_dns'
|
||||
| 'fallback_dns'
|
||||
| 'bootstrap_dns'
|
||||
| 'local_ptr_upstreams'
|
||||
| 'ratelimit_whitelist'
|
||||
| 'blocking_mode'
|
||||
| 'upstream_mode'
|
||||
| 'protection_enabled'
|
||||
| 'protection_disabled_until'
|
||||
> & {
|
||||
// UI-only processing flags:
|
||||
processingGetConfig: boolean;
|
||||
processingSetConfig: boolean;
|
||||
blocking_mode: string;
|
||||
ratelimit: number;
|
||||
blocking_ipv4: string;
|
||||
blocking_ipv6: string;
|
||||
blocked_response_ttl: number;
|
||||
upstream_timeout: number;
|
||||
edns_cs_enabled: boolean;
|
||||
disable_ipv6: boolean;
|
||||
dnssec_enabled: boolean;
|
||||
upstream_dns_file: string;
|
||||
// Normalized fields (string[] → newline-joined string):
|
||||
blocking_mode: DNSConfigBlockingMode;
|
||||
upstream_mode: DNSConfigUpstreamMode;
|
||||
upstream_dns: string;
|
||||
fallback_dns: string;
|
||||
bootstrap_dns: string;
|
||||
local_ptr_upstreams: string;
|
||||
ratelimit_whitelist: string;
|
||||
upstream_mode: string;
|
||||
resolve_clients: boolean;
|
||||
use_private_ptr_resolvers: boolean;
|
||||
default_local_ptr_upstreams: string[];
|
||||
ratelimit_subnet_len_ipv4?: number;
|
||||
ratelimit_subnet_len_ipv6?: number;
|
||||
edns_cs_use_custom?: boolean;
|
||||
edns_cs_custom_ip?: string;
|
||||
cache_size?: number;
|
||||
cache_ttl_max?: number;
|
||||
cache_ttl_min?: number;
|
||||
cache_optimistic?: boolean;
|
||||
cache_enabled?: boolean;
|
||||
};
|
||||
|
||||
export type FilteringData = {
|
||||
export type FilteringData = Omit<FilterStatus, 'filters' | 'whitelist_filters' | 'user_rules'> & {
|
||||
// UI-only fields:
|
||||
isModalOpen: boolean;
|
||||
processingFilters: boolean;
|
||||
processingRules: boolean;
|
||||
|
|
@ -354,42 +275,37 @@ export type FilteringData = {
|
|||
isFilterAdded: boolean;
|
||||
isFilterRemoved: boolean;
|
||||
isFilterEdited: boolean;
|
||||
filters: Filter[];
|
||||
whitelistFilters: any[];
|
||||
userRules: string;
|
||||
interval: number;
|
||||
enabled: boolean;
|
||||
modalType: string;
|
||||
modalFilterUrl: string;
|
||||
check: any;
|
||||
check: Record<string, unknown> | Record<string, never>;
|
||||
// Normalized fields (camelCase from normalizeFilteringStatus):
|
||||
filters: Filter[];
|
||||
whitelistFilters: Filter[]; // Note: whitelist (no underscore) — matches store
|
||||
userRules: string;
|
||||
};
|
||||
|
||||
export type QueryLogsData = {
|
||||
export type QueryLogsData = Omit<GetQueryLogConfigResponse, 'interval'> & {
|
||||
processingGetLogs: boolean;
|
||||
processingClear: boolean;
|
||||
processingGetConfig: boolean;
|
||||
processingSetConfig: boolean;
|
||||
processingAdditionalLogs: boolean;
|
||||
interval: any;
|
||||
logs: any[];
|
||||
enabled: boolean;
|
||||
interval: number;
|
||||
customInterval: number | null;
|
||||
logs: NormalizedQueryLogItem[];
|
||||
oldest: string;
|
||||
filter: any;
|
||||
filter: QueryLogFilter;
|
||||
isFiltered: boolean;
|
||||
anonymize_client_ip: boolean;
|
||||
isDetailed: boolean;
|
||||
isEntireLog: boolean;
|
||||
customInterval: any;
|
||||
ignored: string[];
|
||||
};
|
||||
|
||||
export type ServicesData = {
|
||||
export type ServicesData = BlockedServicesSchedule & {
|
||||
processing: boolean;
|
||||
processingAll: boolean;
|
||||
processingSet: boolean;
|
||||
list: any;
|
||||
allServices: any[];
|
||||
allGroups: any[];
|
||||
allServices: BlockedService[];
|
||||
allGroups: ServiceGroup[];
|
||||
};
|
||||
|
||||
export type ModalsData = {
|
||||
|
|
@ -414,6 +330,7 @@ export type ClientFormState = {
|
|||
duckduckgo: boolean;
|
||||
yandex: boolean;
|
||||
pixabay: boolean;
|
||||
ecosia: boolean;
|
||||
};
|
||||
ignore_querylog: boolean;
|
||||
ignore_statistics: boolean;
|
||||
|
|
@ -454,6 +371,7 @@ export const getInitialClientFormState = (): ClientFormState => ({
|
|||
duckduckgo: false,
|
||||
yandex: false,
|
||||
pixabay: false,
|
||||
ecosia: false,
|
||||
},
|
||||
ignore_querylog: false,
|
||||
ignore_statistics: false,
|
||||
|
|
@ -483,14 +401,14 @@ export type RootState = {
|
|||
settings?: SettingsData;
|
||||
stats?: StatsData;
|
||||
install?: InstallData;
|
||||
toasts: { notices: any[] };
|
||||
toasts: { notices: ToastNotice[] };
|
||||
modals: ModalsData;
|
||||
clientForm: ClientFormState;
|
||||
};
|
||||
|
||||
export type InstallState = {
|
||||
install: InstallData;
|
||||
toasts: { notices: any[] };
|
||||
toasts: { notices: ToastNotice[] };
|
||||
};
|
||||
|
||||
export type LoginState = {
|
||||
|
|
@ -500,7 +418,7 @@ export type LoginState = {
|
|||
password: string;
|
||||
error: unknown;
|
||||
};
|
||||
toasts: { notices: any[] };
|
||||
toasts: { notices: ToastNotice[] };
|
||||
};
|
||||
|
||||
export const initialState: RootState = {
|
||||
|
|
@ -609,7 +527,7 @@ export const initialState: RootState = {
|
|||
dns_names: null,
|
||||
force_https: false,
|
||||
issuer: '',
|
||||
key_type: '',
|
||||
key_type: '' as TlsConfigKeyType,
|
||||
not_after: '',
|
||||
not_before: '',
|
||||
subject: '',
|
||||
|
|
@ -619,6 +537,7 @@ export const initialState: RootState = {
|
|||
valid_pair: false,
|
||||
status_cert: '',
|
||||
status_key: '',
|
||||
allow_unencrypted_doh: false,
|
||||
certificate_chain: '',
|
||||
private_key: '',
|
||||
server_name: '',
|
||||
|
|
@ -626,6 +545,10 @@ export const initialState: RootState = {
|
|||
certificate_path: '',
|
||||
private_key_path: '',
|
||||
private_key_saved: false,
|
||||
port_https: '',
|
||||
port_dns_over_tls: '',
|
||||
port_dns_over_quic: '',
|
||||
port_dnscrypt: '',
|
||||
},
|
||||
filtering: {
|
||||
isModalOpen: false,
|
||||
|
|
@ -682,10 +605,9 @@ export const initialState: RootState = {
|
|||
processing: true,
|
||||
processingAll: true,
|
||||
processingSet: false,
|
||||
list: {},
|
||||
allServices: [],
|
||||
allGroups: [],
|
||||
},
|
||||
} as ServicesData,
|
||||
settings: {
|
||||
processing: true,
|
||||
processingTestUpstream: false,
|
||||
|
|
@ -698,19 +620,19 @@ export const initialState: RootState = {
|
|||
processingReset: false,
|
||||
interval: DAY,
|
||||
customInterval: null,
|
||||
dnsQueries: [],
|
||||
blockedFiltering: [],
|
||||
replacedParental: [],
|
||||
replacedSafebrowsing: [],
|
||||
dns_queries: [],
|
||||
blocked_filtering: [],
|
||||
replaced_parental: [],
|
||||
replaced_safebrowsing: [],
|
||||
topBlockedDomains: [],
|
||||
topClients: [],
|
||||
topQueriedDomains: [],
|
||||
numBlockedFiltering: 0,
|
||||
numDnsQueries: 0,
|
||||
numReplacedParental: 0,
|
||||
numReplacedSafebrowsing: 0,
|
||||
numReplacedSafesearch: 0,
|
||||
avgProcessingTime: 0,
|
||||
num_blocked_filtering: 0,
|
||||
num_dns_queries: 0,
|
||||
num_replaced_parental: 0,
|
||||
num_replaced_safebrowsing: 0,
|
||||
num_replaced_safesearch: 0,
|
||||
avg_processing_time: 0,
|
||||
timeUnits: TIME_UNITS.HOURS,
|
||||
enabled: true,
|
||||
topUpstreamsAvgTime: [],
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { accessList, accessSet } from 'panel/api/generated';
|
|||
import { addErrorToast, addSuccessToast } from './toasts';
|
||||
import { splitByNewLine } from 'panel/helpers/helpers';
|
||||
import intl from 'panel/common/intl';
|
||||
import type { AccessList } from 'panel/api/model/accessList';
|
||||
|
||||
type AccessState = {
|
||||
processing: boolean;
|
||||
|
|
@ -39,20 +40,27 @@ export const getAccessList = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const setAccessList = async (values: any) => {
|
||||
export const setAccessList = async (values: {
|
||||
allowed_clients?: string;
|
||||
disallowed_clients?: string;
|
||||
blocked_hosts?: string;
|
||||
}) => {
|
||||
setState('processingSet', true);
|
||||
try {
|
||||
const config = { ...values };
|
||||
|
||||
if (Object.hasOwn(config, 'allowed_clients')) {
|
||||
config.allowed_clients = splitByNewLine(config.allowed_clients);
|
||||
}
|
||||
if (Object.hasOwn(config, 'disallowed_clients')) {
|
||||
config.disallowed_clients = splitByNewLine(config.disallowed_clients);
|
||||
}
|
||||
if (Object.hasOwn(config, 'blocked_hosts')) {
|
||||
config.blocked_hosts = splitByNewLine(config.blocked_hosts);
|
||||
}
|
||||
const config: AccessList = {
|
||||
allowed_clients:
|
||||
values.allowed_clients !== undefined
|
||||
? splitByNewLine(values.allowed_clients)
|
||||
: undefined,
|
||||
disallowed_clients:
|
||||
values.disallowed_clients !== undefined
|
||||
? splitByNewLine(values.disallowed_clients)
|
||||
: undefined,
|
||||
blocked_hosts:
|
||||
values.blocked_hosts !== undefined
|
||||
? splitByNewLine(values.blocked_hosts)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
await accessSet(config);
|
||||
setState({ ...values, processingSet: false });
|
||||
|
|
@ -63,12 +71,6 @@ export const setAccessList = async (values: any) => {
|
|||
}
|
||||
};
|
||||
|
||||
type AccessList = {
|
||||
allowed_clients?: string[];
|
||||
disallowed_clients?: string[];
|
||||
blocked_hosts?: string[];
|
||||
};
|
||||
|
||||
const addUnique = (items: string[], value: string) =>
|
||||
items.includes(value) ? items : items.concat(value);
|
||||
const removeValue = (items: string[], value: string) => items.filter((i) => i !== value);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const getInitialClientFormState = (): ClientFormState => ({
|
|||
duckduckgo: false,
|
||||
yandex: false,
|
||||
pixabay: false,
|
||||
ecosia: false,
|
||||
},
|
||||
ignore_querylog: false,
|
||||
ignore_statistics: false,
|
||||
|
|
@ -57,18 +58,20 @@ export const initClientForm = (client?: Partial<ClientFormState> | null) => {
|
|||
};
|
||||
|
||||
export const updateClientFormField = (
|
||||
fieldOrObj: keyof ClientFormState | { field: keyof ClientFormState; value: any },
|
||||
maybeValue?: any,
|
||||
fieldOrObj:
|
||||
| keyof ClientFormState
|
||||
| { field: keyof ClientFormState; value: ClientFormState[keyof ClientFormState] },
|
||||
maybeValue?: ClientFormState[keyof ClientFormState],
|
||||
replace?: boolean,
|
||||
) => {
|
||||
const field = typeof fieldOrObj === 'string' ? fieldOrObj : fieldOrObj.field;
|
||||
const value = typeof fieldOrObj === 'string' ? maybeValue : fieldOrObj.value;
|
||||
setState(field as keyof ClientFormState, replace ? reconcile(value) : value);
|
||||
setState(field, replace ? reconcile(value) : value);
|
||||
// Clear the error for this field
|
||||
if (state.formErrors[field as string]) {
|
||||
if (typeof field === 'string' && state.formErrors[field]) {
|
||||
setState('formErrors', (prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[field as string];
|
||||
delete next[field];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
|
@ -98,21 +101,24 @@ export const buildFormPayload = (client: Client): Partial<ClientFormState> => ({
|
|||
filtering_enabled: client.filtering_enabled || false,
|
||||
safebrowsing_enabled: client.safebrowsing_enabled || false,
|
||||
parental_enabled: client.parental_enabled || false,
|
||||
safe_search: (client.safe_search || {
|
||||
enabled: false,
|
||||
google: false,
|
||||
youtube: false,
|
||||
bing: false,
|
||||
duckduckgo: false,
|
||||
yandex: false,
|
||||
pixabay: false,
|
||||
}) as ClientFormState['safe_search'],
|
||||
safe_search: {
|
||||
enabled: client.safe_search?.enabled ?? false,
|
||||
google: client.safe_search?.google ?? false,
|
||||
youtube: client.safe_search?.youtube ?? false,
|
||||
bing: client.safe_search?.bing ?? false,
|
||||
duckduckgo: client.safe_search?.duckduckgo ?? false,
|
||||
yandex: client.safe_search?.yandex ?? false,
|
||||
pixabay: client.safe_search?.pixabay ?? false,
|
||||
ecosia: client.safe_search?.ecosia ?? false,
|
||||
},
|
||||
ignore_querylog: client.ignore_querylog || false,
|
||||
ignore_statistics: client.ignore_statistics || false,
|
||||
blocked_services: client.blocked_services || [],
|
||||
use_global_blocked_services: client.use_global_blocked_services || false,
|
||||
blocked_services_schedule: client.blocked_services_schedule || {
|
||||
time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
blocked_services_schedule: {
|
||||
time_zone:
|
||||
client.blocked_services_schedule?.time_zone ??
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
upstreams: (client.upstreams || []).join('\n'),
|
||||
upstreams_cache_enabled: client.upstreams_cache_enabled || false,
|
||||
|
|
@ -170,14 +176,15 @@ export const saveClient = async (): Promise<boolean> => {
|
|||
|
||||
const existingClientIds = computeExistingClientIds();
|
||||
|
||||
const idErrors = state.ids.map((id: string, index: number) => {
|
||||
const idErrors: (string | undefined)[] = state.ids.map((id: string, index: number) => {
|
||||
if (!id.trim()) {
|
||||
return intl.getMessage('form_error_required');
|
||||
}
|
||||
return validateIdentifier(id, state.ids, index, existingClientIds);
|
||||
});
|
||||
if (idErrors.some((e: string | undefined) => e !== undefined)) {
|
||||
errors.ids = idErrors as string[];
|
||||
const filteredErrors = idErrors.filter((e): e is string => e !== undefined);
|
||||
if (filteredErrors.length > 0) {
|
||||
errors.ids = filteredErrors;
|
||||
}
|
||||
|
||||
// Validate cache size when per-client cache is enabled (and not using global settings).
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { clientsAdd, clientsDelete, clientsUpdate } from 'panel/api/generated';
|
|||
import { addErrorToast, addSuccessToast } from './toasts';
|
||||
import intl from 'panel/common/intl';
|
||||
import { getClients } from './dashboard';
|
||||
import type { Client } from 'panel/api/model/client';
|
||||
|
||||
type ClientsState = {
|
||||
processing: boolean;
|
||||
|
|
@ -39,7 +40,7 @@ export const toggleClientModal = (payload?: { type?: string; name?: string }) =>
|
|||
}
|
||||
};
|
||||
|
||||
export const addClient = async (config: any) => {
|
||||
export const addClient = async (config: Client) => {
|
||||
setState('processingAdding', true);
|
||||
try {
|
||||
await clientsAdd(config);
|
||||
|
|
@ -66,7 +67,7 @@ export const deleteClient = async (name: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const updateClient = async (name: string, data: any): Promise<boolean> => {
|
||||
export const updateClient = async (name: string, data: Client): Promise<boolean> => {
|
||||
setState('processingUpdating', true);
|
||||
try {
|
||||
await clientsUpdate({ name, data });
|
||||
|
|
|
|||
|
|
@ -16,11 +16,13 @@ import type { ServerStatus } from 'panel/api/model/serverStatus';
|
|||
import type { VersionInfo } from 'panel/api/model/versionInfo';
|
||||
import type { Clients } from 'panel/api/model/clients';
|
||||
import type { ProfileInfo } from 'panel/api/model/profileInfo';
|
||||
import intl, { LocalesType } from 'panel/common/intl';
|
||||
import intl from 'panel/common/intl';
|
||||
import { addErrorToast, addSuccessToast, addNoticeToast } from './toasts';
|
||||
import { getTlsStatus } from './encryption';
|
||||
import { getUpdateFailedMessage } from './dashboard/noticeOptions';
|
||||
import type { Client, AutoClient } from 'panel/initialState';
|
||||
import type { Client as ClientModel } from 'panel/api/model/client';
|
||||
import type { ClientAuto } from 'panel/api/model/clientAuto';
|
||||
|
||||
type DashboardState = {
|
||||
processing: boolean;
|
||||
|
|
@ -134,7 +136,7 @@ export const getDnsStatus = async () => {
|
|||
window.location.reload();
|
||||
};
|
||||
|
||||
const handleRequestSuccess = (response: any) => {
|
||||
const handleRequestSuccess = (response: { status: number; data: ServerStatus }) => {
|
||||
const dnsStatus = response.data;
|
||||
if (dnsStatus.protection_disabled_duration === 0) {
|
||||
dnsStatus.protection_disabled_duration = null;
|
||||
|
|
@ -174,7 +176,7 @@ export const getTimerStatus = async () => {
|
|||
window.location.reload();
|
||||
};
|
||||
|
||||
const handleRequestSuccess = (response: any) => {
|
||||
const handleRequestSuccess = (response: { status: number; data: ServerStatus }) => {
|
||||
const dnsStatus = response.data;
|
||||
if (dnsStatus.protection_disabled_duration === 0) {
|
||||
dnsStatus.protection_disabled_duration = null;
|
||||
|
|
@ -202,7 +204,7 @@ export const getVersion = async (recheck = false) => {
|
|||
try {
|
||||
const data: VersionInfo = await getVersionJson({ recheck_now: recheck });
|
||||
const currentVersion =
|
||||
untrack(() => state.dnsVersion) === 'undefined' ? 0 : untrack(() => state.dnsVersion);
|
||||
untrack(() => state.dnsVersion) === 'undefined' ? '' : untrack(() => state.dnsVersion);
|
||||
if (data && !data.disabled && !areEqualVersions(currentVersion, data.new_version)) {
|
||||
setState({
|
||||
announcementUrl: data.announcement_url,
|
||||
|
|
@ -234,7 +236,7 @@ export const getUpdate = async () => {
|
|||
addNoticeToast(getUpdateFailedMessage());
|
||||
setState('processingUpdate', false);
|
||||
};
|
||||
const handleRequestSuccess = (response: any) => {
|
||||
const handleRequestSuccess = (response: { status: number; data: ServerStatus }) => {
|
||||
const responseVersion = response.data?.version;
|
||||
if (untrack(() => state.dnsVersion) !== responseVersion) {
|
||||
setState('processingUpdate', false);
|
||||
|
|
@ -271,11 +273,11 @@ export const setDisableDurationTime = (timeToEnableProtection: number) => {
|
|||
setState('protectionDisabledDuration', timeToEnableProtection);
|
||||
};
|
||||
|
||||
const sortClients = (clients: any[]) => {
|
||||
const sortClients = (clients: (ClientModel | ClientAuto)[]) => {
|
||||
if (!Array.isArray(clients)) return [];
|
||||
return [...clients].sort((a, b) => {
|
||||
const nameA = (a.name || a.ip || '').toString().toLowerCase();
|
||||
const nameB = (b.name || b.ip || '').toString().toLowerCase();
|
||||
const nameA = (a.name || ('ip' in a ? a.ip : '') || '').toString().toLowerCase();
|
||||
const nameB = (b.name || ('ip' in b ? b.ip : '') || '').toString().toLowerCase();
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
};
|
||||
|
|
@ -311,10 +313,10 @@ export const getProfileData = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const changeLanguage = async (lang: LocalesType) => {
|
||||
export const changeLanguage = async (lang: ProfileInfo['language']) => {
|
||||
try {
|
||||
const profile = await getProfile();
|
||||
profile.language = lang as ProfileInfo['language'];
|
||||
profile.language = lang;
|
||||
await updateProfile(profile);
|
||||
setState('language', lang);
|
||||
} catch (error) {
|
||||
|
|
@ -322,10 +324,10 @@ export const changeLanguage = async (lang: LocalesType) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const changeTheme = async (theme: string) => {
|
||||
export const changeTheme = async (theme: ProfileInfo['theme']) => {
|
||||
try {
|
||||
const profile = await getProfile();
|
||||
profile.theme = theme as ProfileInfo['theme'];
|
||||
profile.theme = theme;
|
||||
await updateProfile(profile);
|
||||
setState('theme', theme);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ import intl from 'panel/common/intl';
|
|||
import { STATUS_RESPONSE } from 'panel/helpers/constants';
|
||||
import { Paths } from 'panel/components/Routes/Paths';
|
||||
import { enrichWithConcatenatedIpAddresses } from 'panel/helpers/helpers';
|
||||
import type { DhcpStaticLease } from 'panel/api/model/dhcpStaticLease';
|
||||
import type { DhcpSearchResult } from 'panel/api/model/dhcpSearchResult';
|
||||
import type { DhcpInterfaces } from 'panel/initialState';
|
||||
import type { DhcpConfig } from 'panel/api/model/dhcpConfig';
|
||||
|
||||
type Lease = { hostname: string; ip: string; mac: string };
|
||||
|
||||
|
|
@ -34,7 +38,7 @@ type DhcpState = {
|
|||
processingReset: boolean;
|
||||
enabled: boolean;
|
||||
interface_name: string;
|
||||
check: any;
|
||||
check: DhcpSearchResult | null;
|
||||
v4: {
|
||||
gateway_ip: string;
|
||||
subnet_mask: string;
|
||||
|
|
@ -47,13 +51,13 @@ type DhcpState = {
|
|||
lease_duration: number;
|
||||
};
|
||||
leases: Lease[];
|
||||
staticLeases: Lease[];
|
||||
staticLeases: DhcpStaticLease[];
|
||||
isModalOpen: boolean;
|
||||
leaseModalConfig: Lease | undefined;
|
||||
modalType: LeaseModalType | '';
|
||||
dhcp_available: boolean;
|
||||
staticIpError: boolean;
|
||||
interfaces?: Record<string, any>;
|
||||
interfaces?: DhcpInterfaces;
|
||||
};
|
||||
|
||||
const initialState: DhcpState = {
|
||||
|
|
@ -228,7 +232,7 @@ export const findActiveDhcp = async (interfaceName: string, navigate?: (path: st
|
|||
}
|
||||
};
|
||||
|
||||
export const setDhcpConfig = async (values: any) => {
|
||||
export const setDhcpConfig = async (values: DhcpConfig) => {
|
||||
setState('processingConfig', true);
|
||||
try {
|
||||
await dhcpSetConfig(values);
|
||||
|
|
@ -246,7 +250,7 @@ export const setDhcpConfig = async (values: any) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const toggleDhcp = async (config?: any) => {
|
||||
export const toggleDhcp = async (config?: DhcpConfig) => {
|
||||
setState('processingConfig', true);
|
||||
try {
|
||||
const values = config || {};
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import { dnsInfo, dnsConfig, cacheClear } from 'panel/api/generated';
|
|||
import { addErrorToast, addSuccessToast } from './toasts';
|
||||
import intl from 'panel/common/intl';
|
||||
import { splitByNewLine } from 'panel/helpers/helpers';
|
||||
import { DNS_REQUEST_OPTIONS } from 'panel/helpers/constants';
|
||||
import { DNS_REQUEST_OPTIONS, BLOCKING_MODES } from 'panel/helpers/constants';
|
||||
import type { DNSConfig, DNSConfigBlockingMode, DNSConfigUpstreamMode } from 'panel/api/model';
|
||||
|
||||
type DnsConfigState = {
|
||||
processingGetConfig: boolean;
|
||||
processingSetConfig: boolean;
|
||||
blocking_mode: string;
|
||||
blocking_mode: DNSConfigBlockingMode;
|
||||
ratelimit: number;
|
||||
blocking_ipv4: string;
|
||||
blocking_ipv6: string;
|
||||
|
|
@ -24,7 +25,7 @@ type DnsConfigState = {
|
|||
bootstrap_dns: string;
|
||||
local_ptr_upstreams: string;
|
||||
ratelimit_whitelist: string;
|
||||
upstream_mode: string;
|
||||
upstream_mode: DNSConfigUpstreamMode;
|
||||
resolve_clients: boolean;
|
||||
use_private_ptr_resolvers: boolean;
|
||||
default_local_ptr_upstreams: string[];
|
||||
|
|
@ -41,7 +42,6 @@ type DnsConfigState = {
|
|||
|
||||
export const DEFAULT_BLOCKING_IPV4 = '0.0.0.0';
|
||||
export const DEFAULT_BLOCKING_IPV6 = '::';
|
||||
const BLOCKING_MODES = { default: 'default' };
|
||||
|
||||
const initialState: DnsConfigState = {
|
||||
processingGetConfig: true,
|
||||
|
|
@ -139,29 +139,41 @@ export const toggleEdnsCsEnabled = () => {
|
|||
setDnsConfig({ edns_cs_enabled: !state.edns_cs_enabled }, { silent: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Splits a newline-delimited string into an array.
|
||||
* Returns `undefined` when the field wasn't provided (so the backend keeps
|
||||
* the existing value), as opposed to `[]` (which clears it).
|
||||
*/
|
||||
const splitLines = (value: string | undefined): string[] | undefined =>
|
||||
value !== undefined ? splitByNewLine(value) : undefined;
|
||||
|
||||
export const setDnsConfig = async (
|
||||
values: any,
|
||||
values: Partial<DnsConfigState>,
|
||||
opts?: { toastMessage?: string; silent?: boolean },
|
||||
) => {
|
||||
setState('processingSetConfig', true);
|
||||
try {
|
||||
const config = { ...values };
|
||||
const {
|
||||
bootstrap_dns,
|
||||
fallback_dns,
|
||||
local_ptr_upstreams,
|
||||
upstream_dns,
|
||||
ratelimit_whitelist,
|
||||
// Strip UI-only fields that are NOT part of DNSConfig
|
||||
processingGetConfig: _processingGetConfig,
|
||||
processingSetConfig: _processingSetConfig,
|
||||
default_local_ptr_upstreams: _defaultLocalPtrUpstreams,
|
||||
...rest
|
||||
} = values;
|
||||
|
||||
if (Object.hasOwn(config, 'bootstrap_dns')) {
|
||||
config.bootstrap_dns = splitByNewLine(config.bootstrap_dns);
|
||||
}
|
||||
if (Object.hasOwn(config, 'fallback_dns')) {
|
||||
config.fallback_dns = splitByNewLine(config.fallback_dns);
|
||||
}
|
||||
if (Object.hasOwn(config, 'local_ptr_upstreams')) {
|
||||
config.local_ptr_upstreams = splitByNewLine(config.local_ptr_upstreams);
|
||||
}
|
||||
if (Object.hasOwn(config, 'upstream_dns')) {
|
||||
config.upstream_dns = splitByNewLine(config.upstream_dns);
|
||||
}
|
||||
if (Object.hasOwn(config, 'ratelimit_whitelist')) {
|
||||
config.ratelimit_whitelist = splitByNewLine(config.ratelimit_whitelist);
|
||||
}
|
||||
const config: DNSConfig = {
|
||||
...rest,
|
||||
bootstrap_dns: splitLines(bootstrap_dns),
|
||||
fallback_dns: splitLines(fallback_dns),
|
||||
local_ptr_upstreams: splitLines(local_ptr_upstreams),
|
||||
upstream_dns: splitLines(upstream_dns),
|
||||
ratelimit_whitelist: splitLines(ratelimit_whitelist),
|
||||
};
|
||||
|
||||
await dnsConfig(config);
|
||||
setState(reconcile({ ...untrack(() => state), ...values, processingSetConfig: false }));
|
||||
|
|
|
|||
|
|
@ -5,38 +5,25 @@ import { addErrorToast, addSuccessToast } from './toasts';
|
|||
import { dashboardState } from './dashboard';
|
||||
import { redirectToCurrentProtocol } from '../helpers/helpers';
|
||||
import intl from 'panel/common/intl';
|
||||
import type { TlsConfig } from 'panel/api/model/tlsConfig';
|
||||
import type { TlsConfigBody } from 'panel/api/model/tlsConfigBody';
|
||||
|
||||
type EncryptionState = {
|
||||
type EncryptionState = Partial<
|
||||
Omit<TlsConfig, 'port_https' | 'port_dns_over_tls' | 'port_dns_over_quic' | 'dns_names'>
|
||||
> & {
|
||||
processing: boolean;
|
||||
processingConfig: boolean;
|
||||
processingValidate: boolean;
|
||||
enabled: boolean;
|
||||
serve_plain_dns: boolean;
|
||||
dns_names: any;
|
||||
force_https: boolean;
|
||||
issuer: string;
|
||||
key_type: string;
|
||||
not_after: string;
|
||||
not_before: string;
|
||||
port_dns_over_tls: any;
|
||||
port_dns_over_quic: any;
|
||||
port_https: any;
|
||||
port_dnscrypt: any;
|
||||
subject: string;
|
||||
valid_chain: boolean;
|
||||
valid_key: boolean;
|
||||
valid_cert: boolean;
|
||||
valid_pair: boolean;
|
||||
status_cert: string;
|
||||
status_key: string;
|
||||
certificate_chain: string;
|
||||
private_key: string;
|
||||
server_name: string;
|
||||
warning_validation: string;
|
||||
certificate_path: string;
|
||||
private_key_path: string;
|
||||
private_key_saved: boolean;
|
||||
allow_unencrypted_doh: boolean;
|
||||
// All four port fields: number from API, string from form input (initialized as ''):
|
||||
port_https: number | string;
|
||||
port_dns_over_tls: number | string;
|
||||
port_dns_over_quic: number | string;
|
||||
port_dnscrypt: number | string;
|
||||
// Store initializes as null, API returns string[]:
|
||||
dns_names: string[] | null;
|
||||
};
|
||||
|
||||
const initialState: EncryptionState = {
|
||||
|
|
@ -48,7 +35,7 @@ const initialState: EncryptionState = {
|
|||
dns_names: null,
|
||||
force_https: false,
|
||||
issuer: '',
|
||||
key_type: '',
|
||||
key_type: 'RSA',
|
||||
not_after: '',
|
||||
not_before: '',
|
||||
port_dns_over_tls: '',
|
||||
|
|
@ -74,13 +61,14 @@ const initialState: EncryptionState = {
|
|||
|
||||
const [state, setState] = createStore<EncryptionState>(initialState);
|
||||
|
||||
const decodeResponse = (data: any) => {
|
||||
const fields = ['certificate_chain', 'private_key'];
|
||||
const decoded = { ...data };
|
||||
const decodeResponse = (data: TlsConfig): TlsConfig => {
|
||||
const decoded: TlsConfig = { ...data };
|
||||
const fields = ['certificate_chain', 'private_key'] as const;
|
||||
fields.forEach((field) => {
|
||||
if (decoded[field]) {
|
||||
const value = decoded[field];
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
decoded[field] = atob(decoded[field]);
|
||||
decoded[field] = atob(value);
|
||||
} catch {
|
||||
// keep as is
|
||||
}
|
||||
|
|
@ -89,12 +77,12 @@ const decodeResponse = (data: any) => {
|
|||
return decoded;
|
||||
};
|
||||
|
||||
const encodeRequest = (values: any) => {
|
||||
const encoded = { ...values };
|
||||
if (encoded.certificate_chain) {
|
||||
const encodeRequest = (values: TlsConfig): TlsConfig => {
|
||||
const encoded: TlsConfig = { ...values };
|
||||
if (typeof encoded.certificate_chain === 'string') {
|
||||
encoded.certificate_chain = btoa(encoded.certificate_chain);
|
||||
}
|
||||
if (encoded.private_key) {
|
||||
if (typeof encoded.private_key === 'string') {
|
||||
encoded.private_key = btoa(encoded.private_key);
|
||||
}
|
||||
return encoded;
|
||||
|
|
@ -112,25 +100,27 @@ export const getTlsStatus = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const setTlsConfig = async (values: any, opts?: { silent?: boolean }) => {
|
||||
export const setTlsConfig = async (values: TlsConfigBody, opts?: { silent?: boolean }) => {
|
||||
setState('processingConfig', true);
|
||||
try {
|
||||
// Merge: start with all store values, then override with caller's
|
||||
// defined values (empty strings / false are intentional overrides).
|
||||
const fullValues = {
|
||||
const fullValues: TlsConfig = {
|
||||
enabled: state.enabled,
|
||||
serve_plain_dns: state.serve_plain_dns,
|
||||
server_name: state.server_name,
|
||||
force_https: state.force_https,
|
||||
port_https: state.port_https || 0,
|
||||
port_dns_over_tls: state.port_dns_over_tls || 0,
|
||||
port_dns_over_quic: state.port_dns_over_quic || 0,
|
||||
port_https: Number(state.port_https) || 0,
|
||||
port_dns_over_tls: Number(state.port_dns_over_tls) || 0,
|
||||
port_dns_over_quic: Number(state.port_dns_over_quic) || 0,
|
||||
certificate_chain: state.certificate_chain,
|
||||
private_key: state.private_key,
|
||||
certificate_path: state.certificate_path,
|
||||
private_key_path: state.private_key_path,
|
||||
private_key_saved: state.private_key_saved,
|
||||
...Object.fromEntries(Object.entries(values).filter(([, v]) => v !== undefined)),
|
||||
...Object.fromEntries(
|
||||
Object.entries(values).filter(([, v]) => v !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
const encoded = encodeRequest(fullValues);
|
||||
|
|
@ -153,7 +143,7 @@ export const setTlsConfig = async (values: any, opts?: { silent?: boolean }) =>
|
|||
}
|
||||
};
|
||||
|
||||
export const validateTlsConfig = async (values: any) => {
|
||||
export const validateTlsConfig = async (values: TlsConfigBody) => {
|
||||
setState('processingValidate', true);
|
||||
try {
|
||||
const encoded = encodeRequest(values);
|
||||
|
|
@ -180,7 +170,7 @@ export const resetValidationStatus = () => {
|
|||
valid_pair: false,
|
||||
subject: '',
|
||||
issuer: '',
|
||||
key_type: '',
|
||||
key_type: undefined,
|
||||
not_after: '',
|
||||
not_before: '',
|
||||
dns_names: null,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ import { addErrorToast, addSuccessToast, createUndoToast } from './toasts';
|
|||
import type { Filter } from 'panel/helpers/helpers';
|
||||
import { normalizeFilteringStatus, normalizeRulesTextarea } from 'panel/helpers/helpers';
|
||||
import intl from 'panel/common/intl';
|
||||
import type { FilterCheckHostResponse } from 'panel/api/model/filterCheckHostResponse';
|
||||
import type { FilterSetUrlData } from 'panel/api/model/filterSetUrlData';
|
||||
import type { FilterRefreshRequest } from 'panel/api/model/filterRefreshRequest';
|
||||
import type { FilterConfig } from 'panel/api/model/filterConfig';
|
||||
|
||||
type FilteringState = {
|
||||
isModalOpen: boolean;
|
||||
|
|
@ -29,13 +33,13 @@ type FilteringState = {
|
|||
isFilterRemoved: boolean;
|
||||
isFilterEdited: boolean;
|
||||
filters: Filter[];
|
||||
whitelistFilters: any[];
|
||||
whitelistFilters: Filter[];
|
||||
userRules: string;
|
||||
interval: number;
|
||||
enabled: boolean;
|
||||
modalType: string;
|
||||
modalFilterUrl: string;
|
||||
check: any;
|
||||
check: (FilterCheckHostResponse & { hostname?: string }) | Record<string, never>;
|
||||
};
|
||||
|
||||
const initialState: FilteringState = {
|
||||
|
|
@ -304,7 +308,11 @@ export const removeFilter = async (url: string, whitelist: boolean, name?: strin
|
|||
}
|
||||
};
|
||||
|
||||
export const toggleFilterStatus = async (url: string, data: any, whitelist: boolean) => {
|
||||
export const toggleFilterStatus = async (
|
||||
url: string,
|
||||
data: FilterSetUrlData,
|
||||
whitelist: boolean,
|
||||
) => {
|
||||
setState('processingConfigFilter', true);
|
||||
try {
|
||||
await filteringSetURL({ url, data, whitelist });
|
||||
|
|
@ -316,7 +324,7 @@ export const toggleFilterStatus = async (url: string, data: any, whitelist: bool
|
|||
}
|
||||
};
|
||||
|
||||
export const editFilter = async (url: string, data: any, whitelist: boolean) => {
|
||||
export const editFilter = async (url: string, data: FilterSetUrlData, whitelist: boolean) => {
|
||||
setState('processingConfigFilter', true);
|
||||
try {
|
||||
await filteringSetURL({ url, data, whitelist });
|
||||
|
|
@ -329,7 +337,7 @@ export const editFilter = async (url: string, data: any, whitelist: boolean) =>
|
|||
}
|
||||
};
|
||||
|
||||
export const refreshFilters = async (config: any) => {
|
||||
export const refreshFilters = async (config: FilterRefreshRequest) => {
|
||||
setState('processingRefreshFilters', true);
|
||||
try {
|
||||
const data = await filteringRefresh(config);
|
||||
|
|
@ -347,7 +355,7 @@ export const refreshFilters = async (config: any) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const setFiltersConfig = async (config: any) => {
|
||||
export const setFiltersConfig = async (config: FilterConfig) => {
|
||||
setState('processingSetConfig', true);
|
||||
try {
|
||||
await filteringConfig(config);
|
||||
|
|
@ -365,7 +373,10 @@ export const checkHost = async (
|
|||
try {
|
||||
const data = await filteringCheckHost(typeof host === 'string' ? { name: host } : host);
|
||||
const hostname = typeof host === 'string' ? host : host.name;
|
||||
setState({ check: { hostname, ...data }, processingCheck: false });
|
||||
setState({
|
||||
check: { hostname, ...data },
|
||||
processingCheck: false,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
addErrorToast({ error });
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import { untrack } from 'solid-js';
|
|||
import { installGetAddresses, installConfigure, installCheckConfig } from 'panel/api/generated';
|
||||
import { addErrorToast, addSuccessToast } from './toasts';
|
||||
import intl from 'panel/common/intl';
|
||||
import type { InstallInterface } from '../initialState';
|
||||
import type { NetInterface } from 'panel/api/model/netInterface';
|
||||
import type { InitialConfiguration } from 'panel/api/model/initialConfiguration';
|
||||
import type { CheckConfigRequest } from 'panel/api/model/checkConfigRequest';
|
||||
import {
|
||||
ALL_INTERFACES_IP,
|
||||
INSTALL_FIRST_STEP,
|
||||
|
|
@ -38,7 +42,7 @@ type InstallState = {
|
|||
ip: string;
|
||||
error: string;
|
||||
};
|
||||
interfaces: any[];
|
||||
interfaces: InstallInterface[];
|
||||
dnsVersion: string;
|
||||
};
|
||||
|
||||
|
|
@ -64,10 +68,15 @@ export const getDefaultAddresses = async () => {
|
|||
const data = await installGetAddresses();
|
||||
const normalizedInterfaces = Array.isArray(data.interfaces)
|
||||
? data.interfaces
|
||||
: Object.entries(data.interfaces || {}).map(([name, iface]: any) => ({
|
||||
...iface,
|
||||
name: iface?.name ?? name,
|
||||
}));
|
||||
: Object.entries(data.interfaces || {}).map(
|
||||
([name, iface]: [string, NetInterface]) => ({
|
||||
flags: iface.flags,
|
||||
hardware_address: iface.hardware_address,
|
||||
ip_addresses: [...iface.ipv4_addresses, ...iface.ipv6_addresses],
|
||||
mtu: 0,
|
||||
name: iface.name || name,
|
||||
}),
|
||||
);
|
||||
setState({
|
||||
web: { ...state.web, port: data.web_port },
|
||||
dns: { ...state.dns, port: data.dns_port },
|
||||
|
|
@ -93,7 +102,9 @@ export const setAuthData = (auth: Partial<InstallState['auth']>) => {
|
|||
setState('auth', (prev) => ({ ...prev, ...auth }));
|
||||
};
|
||||
|
||||
export const setAllSettings = async (config: any) => {
|
||||
export const setAllSettings = async (
|
||||
config: InitialConfiguration & { confirm_password: string },
|
||||
) => {
|
||||
setState({ processingSubmit: true, submitted: false });
|
||||
try {
|
||||
const { confirm_password, ...rest } = config;
|
||||
|
|
@ -107,13 +118,21 @@ export const setAllSettings = async (config: any) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const checkConfig = async (values: any) => {
|
||||
export const checkConfig = async (values: CheckConfigRequest) => {
|
||||
setState('processingCheck', true);
|
||||
try {
|
||||
const data = await installCheckConfig(values);
|
||||
setState({
|
||||
web: { ...values.web, ...data.web },
|
||||
dns: { ...values.dns, ...data.dns },
|
||||
web: {
|
||||
ip: values.web?.ip ?? '',
|
||||
port: values.web?.port ?? 0,
|
||||
...data.web,
|
||||
},
|
||||
dns: {
|
||||
ip: values.dns?.ip ?? '',
|
||||
port: values.dns?.port ?? 0,
|
||||
...data.dns,
|
||||
},
|
||||
staticIp: { ...untrack(() => state.staticIp), ...data.static_ip },
|
||||
processingCheck: false,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ import {
|
|||
QUERY_LOGS_PAGE_LIMIT,
|
||||
QUERY_LOG_INTERVALS_DAYS,
|
||||
QUERY_LOG_REASON_FILTER,
|
||||
type QueryLogFilter,
|
||||
} from 'panel/helpers/constants';
|
||||
import { normalizeLogs } from 'panel/helpers/helpers';
|
||||
import { normalizeLogs, type NormalizedQueryLogItem } from 'panel/helpers/helpers';
|
||||
import type { GetQueryLogConfigResponse } from 'panel/api/model/getQueryLogConfigResponse';
|
||||
|
||||
type QueryLogsState = {
|
||||
processingGetLogs: boolean;
|
||||
|
|
@ -20,10 +22,10 @@ type QueryLogsState = {
|
|||
processingSetConfig: boolean;
|
||||
processingAdditionalLogs: boolean;
|
||||
interval: number;
|
||||
logs: any[];
|
||||
logs: NormalizedQueryLogItem[];
|
||||
enabled: boolean;
|
||||
oldest: string;
|
||||
filter: any;
|
||||
filter: QueryLogFilter;
|
||||
isFiltered: boolean;
|
||||
anonymize_client_ip: boolean;
|
||||
isDetailed: boolean;
|
||||
|
|
@ -82,7 +84,7 @@ const REASON_FILTER_TO_REASONS: Record<string, string[]> = {
|
|||
[QUERY_LOG_REASON_FILTER.DNS_REWRITES.QUERY]: ['Rewrite', 'RewriteEtcHosts', 'RewriteRule'],
|
||||
};
|
||||
|
||||
const getReasons = (filter?: any): string[] => {
|
||||
const getReasons = (filter?: QueryLogFilter): string[] => {
|
||||
const reason = filter?.reason ?? DEFAULT_LOGS_FILTER.reason;
|
||||
const status = filter?.status ?? DEFAULT_LOGS_FILTER.status;
|
||||
if (reason !== 'all') {
|
||||
|
|
@ -91,10 +93,11 @@ const getReasons = (filter?: any): string[] => {
|
|||
return STATUS_TO_REASONS[status] ?? [];
|
||||
};
|
||||
|
||||
const fetchLogsWithParams = async (olderThan: string, filter?: any) => {
|
||||
const params: Record<string, any> = {
|
||||
const fetchLogsWithParams = async (olderThan: string, filter?: QueryLogFilter) => {
|
||||
const params: Record<string, string | string[] | number | undefined> = {
|
||||
search: filter?.search ?? DEFAULT_LOGS_FILTER.search,
|
||||
older_than: olderThan,
|
||||
limit: QUERY_LOGS_PAGE_LIMIT,
|
||||
};
|
||||
const reasons = getReasons(filter);
|
||||
if (reasons.length > 0) {
|
||||
|
|
@ -105,18 +108,21 @@ const fetchLogsWithParams = async (olderThan: string, filter?: any) => {
|
|||
};
|
||||
|
||||
/** Simple stateless filter: count entries matching the status */
|
||||
const filterLogsByStatus = (logs: any[], status: string): any[] => {
|
||||
const filterLogsByStatus = (
|
||||
logs: NormalizedQueryLogItem[],
|
||||
status: string,
|
||||
): NormalizedQueryLogItem[] => {
|
||||
if (!status || status === 'all') return logs;
|
||||
const reasons = STATUS_TO_REASONS[status];
|
||||
if (!reasons || reasons.length === 0) return logs;
|
||||
return logs.filter((log: any) => reasons.includes(log.reason));
|
||||
return logs.filter((log) => reasons.includes(log.reason ?? ''));
|
||||
};
|
||||
|
||||
const shortPollQueryLogs = async (
|
||||
data: { logs: any[]; oldest: string },
|
||||
filter: any,
|
||||
total?: { logs: any[]; oldest: string },
|
||||
): Promise<{ logs: any[]; oldest: string }> => {
|
||||
data: { logs: NormalizedQueryLogItem[]; oldest: string },
|
||||
filter: QueryLogFilter,
|
||||
total?: { logs: NormalizedQueryLogItem[]; oldest: string },
|
||||
): Promise<{ logs: NormalizedQueryLogItem[]; oldest: string }> => {
|
||||
const totalData = total
|
||||
? { logs: [...total.logs, ...data.logs], oldest: data.oldest }
|
||||
: { logs: data.logs, oldest: data.oldest };
|
||||
|
|
@ -214,7 +220,7 @@ export const getLogsConfig = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const setLogsConfig = async (values: any): Promise<boolean> => {
|
||||
export const setLogsConfig = async (values: GetQueryLogConfigResponse): Promise<boolean> => {
|
||||
setState('processingSetConfig', true);
|
||||
try {
|
||||
await putQueryLogConfig(values);
|
||||
|
|
@ -227,7 +233,7 @@ export const setLogsConfig = async (values: any): Promise<boolean> => {
|
|||
}
|
||||
};
|
||||
|
||||
export const setFilteredLogs = async (filter?: any): Promise<boolean> => {
|
||||
export const setFilteredLogs = async (filter?: QueryLogFilter): Promise<boolean> => {
|
||||
setState({
|
||||
filter: filter ?? DEFAULT_LOGS_FILTER,
|
||||
isFiltered: true,
|
||||
|
|
@ -251,7 +257,7 @@ export const setFilteredLogs = async (filter?: any): Promise<boolean> => {
|
|||
}
|
||||
};
|
||||
|
||||
export const setLogsFilter = (filter: any) => {
|
||||
export const setLogsFilter = (filter: QueryLogFilter): void => {
|
||||
setState({ filter });
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,8 @@ import {
|
|||
} from 'panel/api/generated';
|
||||
import { addErrorToast, addSuccessToast } from './toasts';
|
||||
import intl from 'panel/common/intl';
|
||||
|
||||
type RewriteConfig = {
|
||||
answer: string;
|
||||
domain: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
import type { RewriteEntry } from 'panel/api/model/rewriteEntry';
|
||||
import type { RewriteSettings } from 'panel/api/model/rewriteSettings';
|
||||
|
||||
type RewritesState = {
|
||||
processing: boolean;
|
||||
|
|
@ -25,8 +21,8 @@ type RewritesState = {
|
|||
processingSettings: boolean;
|
||||
isModalOpen: boolean;
|
||||
modalType: string;
|
||||
currentRewrite: RewriteConfig | Record<string, never>;
|
||||
list: RewriteConfig[];
|
||||
currentRewrite: RewriteEntry;
|
||||
list: RewriteEntry[];
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -45,7 +41,7 @@ const initialState: RewritesState = {
|
|||
|
||||
const [state, setState] = createStore<RewritesState>(initialState);
|
||||
|
||||
export const toggleRewritesModal = (modalType?: string, currentRewrite?: RewriteConfig) => {
|
||||
export const toggleRewritesModal = (modalType?: string, currentRewrite?: RewriteEntry) => {
|
||||
if (modalType !== undefined) {
|
||||
setState({
|
||||
isModalOpen: !state.isModalOpen,
|
||||
|
|
@ -62,14 +58,14 @@ export const getRewritesList = async () => {
|
|||
setState('processing', true);
|
||||
try {
|
||||
const data = await rewriteList();
|
||||
setState({ list: (data || []) as RewriteConfig[], processing: false });
|
||||
setState({ list: data || [], processing: false });
|
||||
} catch (error) {
|
||||
addErrorToast({ error });
|
||||
setState('processing', false);
|
||||
}
|
||||
};
|
||||
|
||||
export const addRewrite = async (config: RewriteConfig) => {
|
||||
export const addRewrite = async (config: RewriteEntry) => {
|
||||
setState('processingAdd', true);
|
||||
try {
|
||||
await rewriteAdd(config);
|
||||
|
|
@ -84,7 +80,7 @@ export const addRewrite = async (config: RewriteConfig) => {
|
|||
};
|
||||
|
||||
export const updateRewrite = async (
|
||||
config: { target: RewriteConfig; update: RewriteConfig },
|
||||
config: { target: RewriteEntry; update: RewriteEntry },
|
||||
options: { showToast?: boolean; closeModal?: boolean } = {},
|
||||
): Promise<boolean> => {
|
||||
setState('processingUpdate', true);
|
||||
|
|
@ -104,7 +100,7 @@ export const updateRewrite = async (
|
|||
}
|
||||
};
|
||||
|
||||
export const deleteRewrite = async (config: RewriteConfig): Promise<boolean> => {
|
||||
export const deleteRewrite = async (config: RewriteEntry): Promise<boolean> => {
|
||||
setState('processingDelete', true);
|
||||
try {
|
||||
await rewriteDelete(config);
|
||||
|
|
@ -130,7 +126,7 @@ export const getRewriteSettings = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const updateRewriteSettings = async (values: any) => {
|
||||
export const updateRewriteSettings = async (values: RewriteSettings) => {
|
||||
setState('processingSettings', true);
|
||||
try {
|
||||
await rewriteSettingsUpdate(values);
|
||||
|
|
|
|||
|
|
@ -6,14 +6,17 @@ import {
|
|||
blockedServicesScheduleUpdate,
|
||||
} from 'panel/api/generated';
|
||||
import { addErrorToast } from './toasts';
|
||||
import type { BlockedServicesSchedule } from 'panel/api/model/blockedServicesSchedule';
|
||||
import type { BlockedService } from 'panel/api/model/blockedService';
|
||||
import type { ServiceGroup } from 'panel/api/model/serviceGroup';
|
||||
|
||||
type ServicesState = {
|
||||
processing: boolean;
|
||||
processingAll: boolean;
|
||||
processingSet: boolean;
|
||||
list: any;
|
||||
allServices: any[];
|
||||
allGroups: any[];
|
||||
list: BlockedServicesSchedule;
|
||||
allServices: BlockedService[];
|
||||
allGroups: ServiceGroup[];
|
||||
};
|
||||
|
||||
const initialState: ServicesState = {
|
||||
|
|
@ -53,7 +56,7 @@ export const getAllBlockedServices = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const updateBlockedServices = async (values: { ids: string[]; schedule?: unknown }) => {
|
||||
export const updateBlockedServices = async (values: BlockedServicesSchedule) => {
|
||||
setState('processingSet', true);
|
||||
try {
|
||||
await blockedServicesScheduleUpdate(values);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import {
|
|||
import { addErrorToast, addSuccessToast } from './toasts';
|
||||
import { splitByNewLine } from 'panel/helpers/helpers';
|
||||
import intl from 'panel/common/intl';
|
||||
import type { SafeSearchConfig } from 'panel/api/model/safeSearchConfig';
|
||||
import type { UpstreamsConfig } from 'panel/api/model/upstreamsConfig';
|
||||
|
||||
type SettingsState = {
|
||||
processing: boolean;
|
||||
|
|
@ -22,7 +24,7 @@ type SettingsState = {
|
|||
settingsList: {
|
||||
parental: { enabled: boolean };
|
||||
safebrowsing: { enabled: boolean };
|
||||
safesearch: Record<string, boolean>;
|
||||
safesearch: SafeSearchConfig;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -47,7 +49,7 @@ export const initSettings = async () => {
|
|||
setState({
|
||||
settingsList: {
|
||||
safebrowsing: { enabled: safebrowsingStatusData.enabled },
|
||||
parental: { enabled: parentalStatusData.enable },
|
||||
parental: { enabled: parentalStatusData.enabled },
|
||||
safesearch: { ...safesearchStatusData },
|
||||
},
|
||||
processing: false,
|
||||
|
|
@ -58,7 +60,18 @@ export const initSettings = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const toggleSetting = async (settingKey: string, status: any) => {
|
||||
export async function toggleSetting(
|
||||
settingKey: 'safesearch',
|
||||
status: SafeSearchConfig,
|
||||
): Promise<boolean>;
|
||||
export async function toggleSetting(
|
||||
settingKey: 'safebrowsing' | 'parental',
|
||||
status: boolean,
|
||||
): Promise<boolean>;
|
||||
export async function toggleSetting(
|
||||
settingKey: string,
|
||||
status: boolean | SafeSearchConfig,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
switch (settingKey) {
|
||||
case 'safebrowsing':
|
||||
|
|
@ -78,8 +91,8 @@ export const toggleSetting = async (settingKey: string, status: any) => {
|
|||
setState('settingsList', 'parental', 'enabled', !status);
|
||||
return true;
|
||||
case 'safesearch':
|
||||
await safesearchSettings(status);
|
||||
setState('settingsList', 'safesearch', status);
|
||||
await safesearchSettings(status as SafeSearchConfig);
|
||||
setState('settingsList', 'safesearch', status as SafeSearchConfig);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
|
@ -88,7 +101,7 @@ export const toggleSetting = async (settingKey: string, status: any) => {
|
|||
addErrorToast({ error });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const settingsState = untrack(() => state);
|
||||
|
||||
|
|
@ -109,7 +122,7 @@ export const testUpstreamWithFormValues = async (
|
|||
lines.filter((line) => !line.startsWith('#') && !line.startsWith('!'));
|
||||
const removeComments = (text: string) => filterOutComments(splitByNewLine(text));
|
||||
|
||||
const config: any = {
|
||||
const config: UpstreamsConfig = {
|
||||
bootstrap_dns: splitByNewLine(bootstrap_dns),
|
||||
private_upstream: splitByNewLine(local_ptr_upstreams),
|
||||
fallback_dns: splitByNewLine(fallback_dns),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import {
|
|||
getParamsForClientsSearch,
|
||||
secondsToMilliseconds,
|
||||
} from 'panel/helpers/helpers';
|
||||
import type { GetStatsConfigResponse } from 'panel/api/model/getStatsConfigResponse';
|
||||
import type { ClientFindSubEntry } from 'panel/api/model/clientFindSubEntry';
|
||||
|
||||
type StatsState = {
|
||||
processingGetConfig: boolean;
|
||||
|
|
@ -30,7 +32,7 @@ type StatsState = {
|
|||
replacedParental: number[];
|
||||
replacedSafebrowsing: number[];
|
||||
topBlockedDomains: { name: string; count: number }[];
|
||||
topClients: { name: string; count: number; info: any }[];
|
||||
topClients: { name: string; count: number; info: ClientFindSubEntry }[];
|
||||
normalizedTopClients: {
|
||||
auto: Record<string, number>;
|
||||
configured: Record<string, number>;
|
||||
|
|
@ -142,7 +144,7 @@ export const getStatsConfig = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const setStatsConfig = async (values: any): Promise<boolean> => {
|
||||
export const setStatsConfig = async (values: GetStatsConfigResponse): Promise<boolean> => {
|
||||
setState('processingSetConfig', true);
|
||||
try {
|
||||
await putStatsConfig(values);
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ type ToastAction = {
|
|||
};
|
||||
|
||||
/** Payload accepted by addSuccessToast. */
|
||||
type SuccessToastPayload = string | {
|
||||
message: string;
|
||||
code?: string;
|
||||
actionLabel?: string;
|
||||
undoId?: string;
|
||||
};
|
||||
type SuccessToastPayload =
|
||||
| string
|
||||
| {
|
||||
message: string;
|
||||
code?: string;
|
||||
actionLabel?: string;
|
||||
undoId?: string;
|
||||
};
|
||||
|
||||
/** Payload accepted by addErrorToast / addWarningToast. */
|
||||
type ErrorToastPayload = {
|
||||
|
|
@ -24,7 +26,7 @@ type ErrorToastPayload = {
|
|||
action?: ToastAction;
|
||||
};
|
||||
|
||||
type ToastNotice = {
|
||||
export type ToastNotice = {
|
||||
id: string;
|
||||
message: string;
|
||||
type: 'error' | 'success' | 'notice' | 'warning';
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ const config = {
|
|||
use: 'yaml-loader',
|
||||
},
|
||||
{
|
||||
test: /\.(svg|png|jpe?g|gif|webp|ico)$/i,
|
||||
test: /\.(svg|png|jpe?g|gif|webp|ico|woff2?)$/i,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'assets/[name].[contenthash][ext]',
|
||||
|
|
|
|||
|
|
@ -865,7 +865,7 @@
|
|||
'schema':
|
||||
'type': 'object'
|
||||
'properties':
|
||||
'enable':
|
||||
'enabled':
|
||||
'type': 'boolean'
|
||||
'sensitivity':
|
||||
'type': 'integer'
|
||||
|
|
@ -1508,6 +1508,9 @@
|
|||
- 'dns_port'
|
||||
- 'http_port'
|
||||
- 'protection_enabled'
|
||||
# TODO: 'protection_disabled_until' in required does not match the property name
|
||||
# 'protection_disabled_duration' below. Check which name the
|
||||
# backend actually uses and align them.
|
||||
- 'protection_disabled_until'
|
||||
- 'running'
|
||||
- 'version'
|
||||
|
|
@ -1592,13 +1595,13 @@
|
|||
'type': 'boolean'
|
||||
'ratelimit':
|
||||
'type': 'integer'
|
||||
'ratelimit_subnet_subnet_len_ipv4':
|
||||
'ratelimit_subnet_len_ipv4':
|
||||
'description': 'Length of the subnet mask for IPv4 addresses.'
|
||||
'type': 'integer'
|
||||
'default': 24
|
||||
'minimum': 0
|
||||
'maximum': 32
|
||||
'ratelimit_subnet_subnet_len_ipv6':
|
||||
'ratelimit_subnet_len_ipv6':
|
||||
'description': 'Length of the subnet mask for IPv6 addresses.'
|
||||
'type': 'integer'
|
||||
'default': 56
|
||||
|
|
@ -2118,6 +2121,8 @@
|
|||
'DhcpStatus':
|
||||
'type': 'object'
|
||||
'description': 'Built-in DHCP server configuration and status'
|
||||
# TODO: 'config' in required does not exist as a property. The backend returns a flat object
|
||||
# (no 'config' wrapper). Remove 'config' from required.
|
||||
'required':
|
||||
- 'config'
|
||||
- 'leases'
|
||||
|
|
@ -2594,6 +2599,14 @@
|
|||
'example': true
|
||||
'description': >
|
||||
Set to true if plain DNS is allowed for incoming requests.
|
||||
'port_dnscrypt':
|
||||
'type': 'integer'
|
||||
'format': 'uint16'
|
||||
'example': 5443
|
||||
'description': 'DNS-over-HTTPS port. If 0, DNSCrypt will be disabled.'
|
||||
'dnscrypt_config_file':
|
||||
'type': 'string'
|
||||
'description': 'Path to the DNSCrypt configuration file.'
|
||||
'NetInterface':
|
||||
'type': 'object'
|
||||
'description': 'Network interface info'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue