diff --git a/client_v2/src/__tests__/dhcp-toggle.test.tsx b/client_v2/src/__tests__/dhcp-toggle.test.tsx index 1dfaba18d..fb1e20ed0 100644 --- a/client_v2/src/__tests__/dhcp-toggle.test.tsx +++ b/client_v2/src/__tests__/dhcp-toggle.test.tsx @@ -9,6 +9,8 @@ const mocks = vi.hoisted(() => ({ interface_name: '', processingDhcp: false, processingConfig: false, + v4: { gateway_ip: '', subnet_mask: '', range_start: '', range_end: '', lease_duration: 0 }, + v6: { range_start: '', lease_duration: 0 }, }, })); @@ -28,18 +30,51 @@ describe('DhcpToggle', () => { interface_name: 'eth0', processingDhcp: false, processingConfig: false, + v4: { gateway_ip: '', subnet_mask: '', range_start: '', range_end: '', lease_duration: 0 }, + v6: { range_start: '', lease_duration: 0 }, }; const { container } = render(() => 'eth0'} />); const input = container.querySelector('#dhcp_enabled') as HTMLInputElement; expect(input.checked).toBe(true); }); - it('calls toggleDhcp with enabled:false + interface_name when toggled ON + calls onToggleOn', () => { + it('when toggled ON without v4 config, reverts UI and calls onToggleOn without toggleDhcp', () => { mocks.dhcpState = { enabled: false, interface_name: 'eth0', processingDhcp: false, processingConfig: false, + v4: { gateway_ip: '', subnet_mask: '', range_start: '', range_end: '', lease_duration: 0 }, + v6: { range_start: '', lease_duration: 0 }, + }; + const onToggleOn = vi.fn(); + const { container } = render(() => ( + 'eth0'} onToggleOn={onToggleOn} /> + )); + const input = container.querySelector('#dhcp_enabled') as HTMLInputElement; + fireEvent.change(input, { target: { checked: true } }); + // Shadow signal reverts — the switch should appear unchecked. + expect(input.checked).toBe(false); + // Backend must NOT be called — config is not yet filled. + expect(mocks.toggleDhcp).not.toHaveBeenCalled(); + // Config modal should open so the user can fill v4 settings. + expect(onToggleOn).toHaveBeenCalledOnce(); + }); + + it('when toggled ON with existing v4 config, calls toggleDhcp without onToggleOn', () => { + mocks.dhcpState = { + enabled: false, + interface_name: 'eth0', + processingDhcp: false, + processingConfig: false, + v4: { + gateway_ip: '192.168.1.1', + subnet_mask: '255.255.255.0', + range_start: '192.168.1.50', + range_end: '192.168.1.100', + lease_duration: 86400, + }, + v6: { range_start: '', lease_duration: 0 }, }; const onToggleOn = vi.fn(); const { container } = render(() => ( @@ -51,9 +86,11 @@ describe('DhcpToggle', () => { expect.objectContaining({ enabled: false, interface_name: 'eth0', + v4: expect.objectContaining({ gateway_ip: '192.168.1.1' }), }), ); - expect(onToggleOn).toHaveBeenCalledOnce(); + // v4 is configured — no need to open the config modal. + expect(onToggleOn).not.toHaveBeenCalled(); }); it('calls toggleDhcp with enabled:true when toggled OFF, does NOT call onToggleOn', () => { @@ -62,6 +99,8 @@ describe('DhcpToggle', () => { interface_name: 'eth0', processingDhcp: false, processingConfig: false, + v4: { gateway_ip: '', subnet_mask: '', range_start: '', range_end: '', lease_duration: 0 }, + v6: { range_start: '', lease_duration: 0 }, }; const onToggleOn = vi.fn(); const { container } = render(() => ( @@ -79,6 +118,8 @@ describe('DhcpToggle', () => { interface_name: '', processingConfig: true, processingDhcp: false, + v4: { gateway_ip: '', subnet_mask: '', range_start: '', range_end: '', lease_duration: 0 }, + v6: { range_start: '', lease_duration: 0 }, }; const { container } = render(() => ''} />); const input = container.querySelector('#dhcp_enabled') as HTMLInputElement; diff --git a/client_v2/src/components/BlockedServices/BlockedServices.tsx b/client_v2/src/components/BlockedServices/BlockedServices.tsx index 491258609..74759b226 100644 --- a/client_v2/src/components/BlockedServices/BlockedServices.tsx +++ b/client_v2/src/components/BlockedServices/BlockedServices.tsx @@ -147,7 +147,7 @@ export const BlockedServices = (props: Props) => { (servicesState.allServices == null || servicesState.allServices.length === 0) && (servicesState.processingAll || servicesState.processing); const isGloballyDisabled = () => - props.clientScope ? clientFormState.use_global_blocked_services : false; + props.clientScope ? clientFormState.use_global_settings : false; const getScheduleRoute = () => { if (!props.clientScope) { @@ -203,7 +203,12 @@ export const BlockedServices = (props: Props) => {

{intl.getMessage('blocked_services_desc')}

- +
{intl.getMessage('inactivity_schedule')} diff --git a/client_v2/src/components/Clients/blocks/RuntimeClientsTable/RuntimeClientsTable.module.pcss b/client_v2/src/components/Clients/blocks/RuntimeClientsTable/RuntimeClientsTable.module.pcss index b84040403..4a62cacf7 100644 --- a/client_v2/src/components/Clients/blocks/RuntimeClientsTable/RuntimeClientsTable.module.pcss +++ b/client_v2/src/components/Clients/blocks/RuntimeClientsTable/RuntimeClientsTable.module.pcss @@ -19,6 +19,8 @@ justify-content: space-between; gap: 8px; padding: 4px 0; + font-size: var(--fs-text-t3); + line-height: var(--lh-t3-normal); @media (min-width: 768px) { padding: 0; diff --git a/client_v2/src/components/Dashboard/Dashboard.tsx b/client_v2/src/components/Dashboard/Dashboard.tsx index 8f633ea73..dd302689d 100644 --- a/client_v2/src/components/Dashboard/Dashboard.tsx +++ b/client_v2/src/components/Dashboard/Dashboard.tsx @@ -168,7 +168,7 @@ export const Dashboard = () => { /> diff --git a/client_v2/src/components/Dashboard/blocks/TopClients/TopClients.tsx b/client_v2/src/components/Dashboard/blocks/TopClients/TopClients.tsx index 30c4253e2..cf4e35a12 100644 --- a/client_v2/src/components/Dashboard/blocks/TopClients/TopClients.tsx +++ b/client_v2/src/components/Dashboard/blocks/TopClients/TopClients.tsx @@ -18,17 +18,12 @@ import { EmptyState } from '../EmptyState'; import s from './TopClients.module.pcss'; +import type { ClientFindSubEntry } from 'panel/api/model/clientFindSubEntry'; + type ClientInfo = { name: string; count: number; - info?: { - name?: string; - whois_info?: { - orgname?: string; - country?: string; - }; - disallowed?: boolean; - }; + info?: ClientFindSubEntry; }; type Props = { diff --git a/client_v2/src/components/Dhcp/blocks/DhcpToggle.tsx b/client_v2/src/components/Dhcp/blocks/DhcpToggle.tsx index fd969606d..50ecdefff 100644 --- a/client_v2/src/components/Dhcp/blocks/DhcpToggle.tsx +++ b/client_v2/src/components/Dhcp/blocks/DhcpToggle.tsx @@ -1,4 +1,5 @@ import type { Accessor } from 'solid-js'; +import { createSignal, createEffect } from 'solid-js'; import { SettingRow } from 'panel/common/ui/SettingRow'; import intl from 'panel/common/intl'; import { dhcpState, toggleDhcp } from 'panel/stores/dhcp'; @@ -16,32 +17,58 @@ type DhcpToggleConfig = { }; export const DhcpToggle = (props: Props) => { + /** + * Shadows dhcpState.enabled with {@code equals: false} so we can + * force a DOM re-sync even when the value is unchanged (e.g. reverting + * after an error or when opening the config modal without saving). + * Synced from store only when {@code processingConfig} is false + * to avoid flashing during async save. + */ + const [dhcpEnabled, setDhcpEnabled] = createSignal(false, { + equals: false, + }); + + createEffect(() => { + if (!dhcpState.processingConfig) { + setDhcpEnabled(!!dhcpState.enabled); + } + }); + const disabled = () => dhcpState.processingDhcp || dhcpState.processingConfig || (!dhcpState.enabled && !props.selectedInterface()); const onChange = (checked: boolean) => { - if (checked) { - const v4 = dhcpState.v4; - const v6 = dhcpState.v6; - const hasV4Config = !!(v4 && Object.values(v4).some(Boolean)); - const hasV6Config = !!(v6 && Object.values(v6).some(Boolean)); + if (!checked) { + // Turning OFF — save immediately (no prerequisites needed). + setDhcpEnabled(false); + toggleDhcp({ enabled: true }); + return; + } + // Turning ON — check prerequisites before saving to backend. + const v4 = dhcpState.v4; + const v6 = dhcpState.v6; + const hasV4Config = !!(v4 && Object.values(v4).some(Boolean)); + const hasV6Config = !!(v6 && Object.values(v6).some(Boolean)); + + // v4 config is already set up — save the change. + if (hasV4Config) { const values: DhcpToggleConfig = { enabled: false, interface_name: props.selectedInterface() || dhcpState.interface_name, }; if (hasV4Config) values.v4 = v4; if (hasV6Config) values.v6 = v6; - toggleDhcp(values); - if (!hasV4Config) { - props.onToggleOn?.(); - } - } else { - toggleDhcp({ enabled: true }); + return; } + + // v4 config is missing — revert UI and open the config modal + // WITHOUT saving to backend (avoids showing an error toast). + setDhcpEnabled(false); + props.onToggleOn?.(); }; return ( @@ -49,7 +76,7 @@ export const DhcpToggle = (props: Props) => { id="dhcp_enabled" variant="switch" title={intl.getMessage('dhcp_enable')} - checked={!!dhcpState.enabled} + checked={dhcpEnabled()} disabled={disabled()} onChange={onChange} /> diff --git a/client_v2/src/components/FilterLists/DNSRewrites.tsx b/client_v2/src/components/FilterLists/DNSRewrites.tsx index 895028414..e861bc3cf 100644 --- a/client_v2/src/components/FilterLists/DNSRewrites.tsx +++ b/client_v2/src/components/FilterLists/DNSRewrites.tsx @@ -21,11 +21,9 @@ import { RewritesTable } from './blocks/RewritesTable/RewritesTable'; import s from './FilterLists.module.pcss'; -export type Rewrite = { - answer: string; - domain: string; - enabled: boolean; -}; +import type { RewriteEntry } from 'panel/api/model/rewriteEntry'; + +export type Rewrite = RewriteEntry & { enabled?: boolean }; export const DNSRewrites = () => { const [currentRewrite, setCurrentRewrite] = createSignal({ @@ -123,7 +121,7 @@ export const DNSRewrites = () => { 0}>
; onSubmit?: (values: FormValues) => boolean | void | Promise; onClose?: () => void; }; diff --git a/client_v2/src/components/FilterLists/blocks/DeleteRewriteModal/DeleteRewriteModal.tsx b/client_v2/src/components/FilterLists/blocks/DeleteRewriteModal/DeleteRewriteModal.tsx index 89131b880..25406cf9f 100644 --- a/client_v2/src/components/FilterLists/blocks/DeleteRewriteModal/DeleteRewriteModal.tsx +++ b/client_v2/src/components/FilterLists/blocks/DeleteRewriteModal/DeleteRewriteModal.tsx @@ -9,9 +9,9 @@ import { deleteRewrite, rewritesState } from 'panel/stores/rewrites'; type Props = { rewriteToDelete: { - answer: string; - domain: string; - enabled: boolean; + answer?: string; + domain?: string; + enabled?: boolean; }; setRewriteToDelete: (value: { answer: string; domain: string; enabled: boolean }) => void; onConfirm?: () => boolean | void | Promise; diff --git a/client_v2/src/components/QueryLog/QueryLog.tsx b/client_v2/src/components/QueryLog/QueryLog.tsx index ddd602ceb..1b67e4d99 100644 --- a/client_v2/src/components/QueryLog/QueryLog.tsx +++ b/client_v2/src/components/QueryLog/QueryLog.tsx @@ -31,7 +31,7 @@ import { getLogsUrlParams } from 'panel/helpers/helpers'; import { RoutePath, linkPathBuilder } from 'panel/components/Routes/Paths'; import { filterLogsByStatus } from './helpers'; -import { LogEntry, ResponseEntry } from './types'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; import { Header } from './blocks/Header'; import { EmptyState, type EmptyStateMode } from './blocks/EmptyState/EmptyState'; import { LogTable } from './blocks/LogTable'; @@ -56,7 +56,7 @@ export const QueryLog = () => { const navigate = useNavigate(); const location = useLocation(); - const [selectedEntry, setSelectedEntry] = createSignal(null); + const [selectedEntry, setSelectedEntry] = createSignal(null); const [disallowTarget, setDisallowTarget] = createSignal(null); const [isIncrementalLoad, setIsIncrementalLoad] = createSignal(false); @@ -111,7 +111,7 @@ export const QueryLog = () => { ); const visibleLogs = () => filterLogsByStatus( - (queryLogsState.logs || []) as { reason: string; originalResponse?: ResponseEntry[] }[], + queryLogsState.logs || [], currentStatus(), ); const emptyStateMode = () => getEmptyStateMode(queryLogsState.enabled, queryLogsState.interval); @@ -190,7 +190,7 @@ export const QueryLog = () => { setDisallowTarget(null); }; - const handleRowClick = (entry: LogEntry) => { + const handleRowClick = (entry: NormalizedQueryLogItem) => { setSelectedEntry(entry); }; @@ -236,7 +236,7 @@ export const QueryLog = () => {
{ {(entry) => ( void; }; -const formatResponses = (responses: ResponseEntry[] = []) => +const formatResponses = (responses: { value?: string; type?: string; ttl?: number }[] = []) => responses .map(({ type, value, ttl }) => { if (!value) { @@ -89,6 +90,8 @@ export const DetailModal = (props: Props) => { const responseList = () => formatResponses(props.entry.response); const originalResponseList = () => formatResponses(props.entry.originalResponse); const trackerSource = () => props.entry.tracker?.sourceData; + const trackerName = () => props.entry.tracker?.name; + const trackerCategory = () => props.entry.tracker?.category; const country = () => props.entry.client_info?.whois?.country; const network = () => props.entry.client_info?.whois?.orgname; const serviceId = () => props.entry.serviceName || props.entry.service_name; @@ -121,10 +124,11 @@ export const DetailModal = (props: Props) => { }; const handleAllowService = () => { - if (!serviceId()) { + const sid = serviceId(); + if (!sid) { return; } - props.onAllowService(serviceId()!); + props.onAllowService(sid); props.onClose(); }; @@ -215,49 +219,63 @@ export const DetailModal = (props: Props) => {

{intl.getMessage('known_tracker')}

-
- {intl.getMessage('query_log_detail_name', { - value: props.entry.tracker!.name, - span: renderValue, - })} -
-
- {intl.getMessage('query_log_detail_category', { - value: props.entry.tracker!.category, - span: renderValue, - })} -
- -
- {intl.getMessage('query_log_detail_source', { - value: trackerSource()!.name, - span: (content: any) => - trackerSource()!.url ? ( - - {content} - - ) : ( - renderValue(content) - ), - })} -
+ + {(name) => ( +
+ {intl.getMessage('query_log_detail_name', { + value: name(), + span: renderValue, + })} +
+ )} +
+ + {(category) => ( +
+ {intl.getMessage('query_log_detail_category', { + value: category(), + span: renderValue, + })} +
+ )} +
+ + {(source) => ( + + {(name) => ( +
+ {intl.getMessage('query_log_detail_source', { + value: name(), + span: (content: any) => + source()?.url ? ( + + {content} + + ) : ( + renderValue(content) + ), + })} +
+ )} +
+ )}
diff --git a/client_v2/src/components/QueryLog/blocks/LogCard/LogCard.tsx b/client_v2/src/components/QueryLog/blocks/LogCard/LogCard.tsx index 7e987e56c..e70c43309 100644 --- a/client_v2/src/components/QueryLog/blocks/LogCard/LogCard.tsx +++ b/client_v2/src/components/QueryLog/blocks/LogCard/LogCard.tsx @@ -20,17 +20,18 @@ import { hasPersistentClient, isBlockedReason, } from '../../helpers'; -import { LogEntry, Service } from '../../types'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; +import { Service } from '../../types'; import { ActionsMenu } from '../ActionsMenu'; import s from './LogCard.module.pcss'; type Props = { - entry: LogEntry; + entry: NormalizedQueryLogItem; filters: Filter[]; services: Service[]; whitelistFilters: Filter[]; - onRowClick: (entry: LogEntry) => void; + onRowClick: (entry: NormalizedQueryLogItem) => void; onBlock: (domain: string) => void; onUnblock: (domain: string) => void; onBlockClient: (domain: string, client: string) => void; diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/LogTable.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/LogTable.tsx index 9ffaba907..7c83889fc 100644 --- a/client_v2/src/components/QueryLog/blocks/LogTable/LogTable.tsx +++ b/client_v2/src/components/QueryLog/blocks/LogTable/LogTable.tsx @@ -4,7 +4,8 @@ import intl from 'panel/common/intl'; import { Loader } from 'panel/common/ui/Loader'; import { Table, TableColumn } from 'panel/common/ui/Table/Table'; -import { LogEntry, Service } from 'panel/components/QueryLog/types'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; +import { Service } from 'panel/components/QueryLog/types'; import { hasPersistentClient, isBlockedReason } from 'panel/components/QueryLog/helpers'; import { Filter } from 'panel/helpers/helpers'; @@ -16,7 +17,7 @@ import s from './LogTable.module.pcss'; import { ActionsMenu } from '../ActionsMenu'; type Props = { - logs: LogEntry[]; + logs: NormalizedQueryLogItem[]; emptyStateMode: EmptyStateMode; hasMore: boolean; isLoadingMore: boolean; @@ -25,7 +26,7 @@ type Props = { isFilterReloading: boolean; infiniteScrollResetToken: string; onLoadMore: () => void; - onRowClick: (entry: LogEntry) => void; + onRowClick: (entry: NormalizedQueryLogItem) => void; onBlock: (domain: string) => void; onUnblock: (domain: string) => void; onBlockClient: (domain: string, client: string) => void; @@ -45,31 +46,31 @@ export const LogTable = (props: Props) => { untrack(() => props.onSearchSelect(value)); }; - const columns = createMemo[]>(() => [ + const columns = createMemo[]>(() => [ { key: 'time', header: { text: intl.getMessage('time_table_header') }, - render: (_value: unknown, row: LogEntry) => , + render: (_value: unknown, row: NormalizedQueryLogItem) => , width: 116, sortable: false, }, { key: 'domain', header: { text: intl.getMessage('request_table_header') }, - render: (_value: unknown, row: LogEntry) => , + render: (_value: unknown, row: NormalizedQueryLogItem) => , sortable: false, }, { key: 'status', header: { text: intl.getMessage('status_table_header') }, - render: (_value: unknown, row: LogEntry) => , + render: (_value: unknown, row: NormalizedQueryLogItem) => , width: 'minmax(108px, 0.7fr)', sortable: false, }, { key: 'reason', header: { text: intl.getMessage('reason_table_header') }, - render: (_value: unknown, row: LogEntry) => { + render: (_value: unknown, row: NormalizedQueryLogItem) => { return ( { { key: 'client', header: { text: intl.getMessage('client_table_header') }, - render: (_value: unknown, row: LogEntry) => ( + render: (_value: unknown, row: NormalizedQueryLogItem) => ( ), sortable: false, @@ -93,7 +94,7 @@ export const LogTable = (props: Props) => { { key: 'actions', header: { text: '', render: () => null }, - render: (_value: unknown, row: LogEntry) => ( + render: (_value: unknown, row: NormalizedQueryLogItem) => (
(event: MouseEvent) => void; - row: LogEntry; + row: NormalizedQueryLogItem; }; export const ClientCell = (props: Props) => { diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/QueryDetailsTooltipContent.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/QueryDetailsTooltipContent.tsx index 53e0ba848..5cc090f26 100644 --- a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/QueryDetailsTooltipContent.tsx +++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/QueryDetailsTooltipContent.tsx @@ -4,17 +4,17 @@ import cn from 'clsx'; import intl from 'panel/common/intl'; import { captitalizeWords } from 'panel/helpers/helpers'; import theme from 'panel/lib/theme'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; import { formatLogDate, formatLogTimeDetailed, getProtocolName, } from 'panel/components/QueryLog/helpers'; -import { LogEntry } from 'panel/components/QueryLog/types'; import s from '../LogTable.module.pcss'; type Props = { - row: LogEntry; + row: NormalizedQueryLogItem; }; const renderValue = (value: any) => ( @@ -23,6 +23,8 @@ const renderValue = (value: any) => ( export const QueryDetailsTooltipContent = (props: Props) => { const trackerSource = () => props.row.tracker?.sourceData; + const trackerName = () => props.row.tracker?.name; + const trackerCategory = () => props.row.tracker?.category; const displayDomain = () => props.row.unicodeName || props.row.domain; return ( @@ -77,42 +79,56 @@ export const QueryDetailsTooltipContent = (props: Props) => {
-
- {intl.getMessage('query_log_detail_name', { - value: props.row.tracker!.name, - span: renderValue, - })} -
-
- {intl.getMessage('query_log_detail_category', { - value: captitalizeWords(props.row.tracker!.category), - span: renderValue, - })} -
- -
- {intl.getMessage('query_log_detail_source', { - value: trackerSource()!.name, - span: (content: any) => - trackerSource()!.url ? ( - - {content} - - ) : ( - - {content} - - ), - })} -
+ + {(name) => ( +
+ {intl.getMessage('query_log_detail_name', { + value: name(), + span: renderValue, + })} +
+ )} +
+ + {(category) => ( +
+ {intl.getMessage('query_log_detail_category', { + value: captitalizeWords(category()), + span: renderValue, + })} +
+ )} +
+ + {(source) => ( + + {(name) => ( +
+ {intl.getMessage('query_log_detail_source', { + value: name(), + span: (content: any) => + source()?.url ? ( + + {content} + + ) : ( + + {content} + + ), + })} +
+ )} +
+ )}
diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ReasonCell.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ReasonCell.tsx index 00e91686f..9c0443e93 100644 --- a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ReasonCell.tsx +++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ReasonCell.tsx @@ -2,7 +2,8 @@ import cn from 'clsx'; import theme from 'panel/lib/theme'; import { Filter } from 'panel/helpers/helpers'; -import { LogEntry, Service } from 'panel/components/QueryLog/types'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; +import { Service } from 'panel/components/QueryLog/types'; import { getQueryReasonLabel, getQueryReasonDetails, @@ -12,7 +13,7 @@ import { import s from '../LogTable.module.pcss'; type Props = { - row: LogEntry; + row: NormalizedQueryLogItem; filters: Filter[]; services: Service[]; whitelistFilters: Filter[]; diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/RequestCell.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/RequestCell.tsx index 7bf0801b4..991d2f430 100644 --- a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/RequestCell.tsx +++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/RequestCell.tsx @@ -7,12 +7,12 @@ import theme from 'panel/lib/theme'; import { Icon } from 'panel/common/ui/Icon'; import { getProtocolName } from 'panel/components/QueryLog/helpers'; import { QueryDetailsTooltipContent } from 'panel/components/QueryLog/blocks/LogTable/blocks/QueryDetailsTooltipContent'; -import { LogEntry } from 'panel/components/QueryLog/types'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; import s from '../LogTable.module.pcss'; type Props = { - row: LogEntry; + row: NormalizedQueryLogItem; }; export const RequestCell = (props: Props) => { diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/StatusCell.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/StatusCell.tsx index 27f0d07f9..bf4c70201 100644 --- a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/StatusCell.tsx +++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/StatusCell.tsx @@ -2,7 +2,7 @@ import { createMemo } from 'solid-js'; import cn from 'clsx'; import theme from 'panel/lib/theme'; -import { LogEntry } from 'panel/components/QueryLog/types'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; import { getQueryStatusLabel, getQueryStatusDetails, @@ -13,7 +13,7 @@ import { import s from '../LogTable.module.pcss'; type Props = { - row: LogEntry; + row: NormalizedQueryLogItem; }; export const StatusCell = (props: Props) => { diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/TimeCell.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/TimeCell.tsx index a57296731..d6563cde4 100644 --- a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/TimeCell.tsx +++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/TimeCell.tsx @@ -1,13 +1,13 @@ import cn from 'clsx'; import theme from 'panel/lib/theme'; -import { LogEntry } from 'panel/components/QueryLog/types'; +import type { NormalizedQueryLogItem } from 'panel/helpers/helpers'; import { formatLogDate, formatLogTime } from 'panel/components/QueryLog/helpers'; import s from '../LogTable.module.pcss'; type Props = { - row: LogEntry; + row: NormalizedQueryLogItem; }; export const TimeCell = (props: Props) => ( diff --git a/client_v2/src/components/QueryLog/helpers.ts b/client_v2/src/components/QueryLog/helpers.ts index 927ba8579..3f0f3a2a9 100644 --- a/client_v2/src/components/QueryLog/helpers.ts +++ b/client_v2/src/components/QueryLog/helpers.ts @@ -16,9 +16,8 @@ import { getFilterNames, getServiceName, type Filter, - type Rule, } from 'panel/helpers/helpers'; -import { LogEntry, ResponseEntry, WhoisInfo } from './types'; +import { ResponseEntry, WhoisInfo } from './types'; const parseLogDate = (time: string): Date | null => { const parsedTime = new Date(time); @@ -140,13 +139,13 @@ const PROTOCOL_LABEL_GETTERS = { plain_dns: () => intl.getMessage('plain_dns'), } as const; -export const getStatusClassName = (reason: string): string => +export const getStatusClassName = (reason?: string): string => STATUS_COLOR_TO_CLASS[ FILTERED_STATUS_TO_COLOR_MAP[reason as keyof typeof FILTERED_STATUS_TO_COLOR_MAP] ] || ''; -export const isBlockedReason = (reason: string): boolean => - reason.startsWith('Filtered') && reason !== 'FilteredSafeSearch'; +export const isBlockedReason = (reason?: string): boolean => + !!reason && reason.startsWith('Filtered') && reason !== 'FilteredSafeSearch'; export const getProtocolName = (clientProto: string): string => { const key = SCHEME_TO_PROTOCOL_MAP[clientProto as keyof typeof SCHEME_TO_PROTOCOL_MAP]; @@ -185,18 +184,18 @@ export const getClientLocation = (whois?: WhoisInfo | null): string => [whois?.city, whois?.country].filter(Boolean).join(', '); type ResponseDetailsParams = { - elapsedMs: string; + elapsedMs?: string; filters: Filter[]; - reason: string; - rules: Rule[]; + reason?: string; + rules: { filter_list_id?: number; text?: string }[]; serviceName?: string; services?: { id: string; name: string }[]; whitelistFilters: Filter[]; }; export const getQueryStatusKey = ( - reason: string, - originalResponse: ResponseEntry[] = [], + reason?: string, + originalResponse: { value?: string; type?: string; ttl?: number }[] = [], ): Exclude => { switch (reason) { case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST: @@ -216,7 +215,7 @@ export const getQueryStatusKey = ( return 'rewritten'; } - if (reason.startsWith('Filtered')) { + if (reason && reason.startsWith('Filtered')) { return 'blocked'; } @@ -225,8 +224,8 @@ export const getQueryStatusKey = ( }; export const getQueryReasonKey = ( - reason: string, - rules: Rule[] = [], + reason?: string, + rules: { filter_list_id?: number; text?: string }[] = [], ): Exclude => { switch (reason) { case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST: @@ -286,7 +285,7 @@ export const getQueryReasonDetails = ({ }; export const filterLogsByStatus = < - T extends { reason: string; originalResponse?: ResponseEntry[] }, + T extends { reason?: string; originalResponse?: { type?: string; value?: string }[] }, >( logs: T[], status: QueryStatusKey | string, @@ -296,12 +295,12 @@ export const filterLogsByStatus = < } return logs.filter( - (log) => getQueryStatusKey(log.reason, log.originalResponse ?? []) === status, + (log) => getQueryStatusKey(log.reason ?? '', log.originalResponse ?? []) === status, ); }; export const hasPersistentClient = ( - entry: Pick, + entry: { client: string; client_id?: string; client_info?: { name?: string; ids?: string[] } | null }, persistentClientIds: string[], ): boolean => { const entryIds = [entry.client, entry.client_id, ...(entry.client_info?.ids ?? [])].filter( @@ -321,7 +320,7 @@ export const getResponseDetails = ({ whitelistFilters, }: ResponseDetailsParams): string => { const formattedElapsedMs = formatElapsedMs( - elapsedMs, + elapsedMs || '', intl.getMessage('milliseconds_abbreviation'), ); diff --git a/client_v2/src/components/QueryLog/types.ts b/client_v2/src/components/QueryLog/types.ts index 5df88c742..dd37533c0 100644 --- a/client_v2/src/components/QueryLog/types.ts +++ b/client_v2/src/components/QueryLog/types.ts @@ -1,7 +1,7 @@ import type { FilteringReason } from 'panel/api/model/filteringReason'; export type ResponseEntry = { - value: string; + value?: string; type?: string; ttl?: number; }; diff --git a/client_v2/src/helpers/helpers.tsx b/client_v2/src/helpers/helpers.tsx index 7a0fe68c6..996690bcb 100644 --- a/client_v2/src/helpers/helpers.tsx +++ b/client_v2/src/helpers/helpers.tsx @@ -2,6 +2,7 @@ import ipaddr, { IPv4, IPv6 } from 'ipaddr.js'; import queryString from 'qs'; import intl from 'panel/common/intl'; import { getTrackerData } from './trackers/trackers'; +import type { TrackerData } from './trackers/trackers'; import { ADDRESS_TYPES, @@ -57,7 +58,7 @@ export type NormalizedQueryLogItem = { serviceName?: string; originalAnswer?: DnsAnswer[]; originalResponse: NormalizedDnsResponse[]; - tracker: Record | null; + tracker: TrackerData | null; answer_dnssec?: boolean; elapsedMs?: string; upstream?: string; @@ -893,8 +894,8 @@ export type Filter = { }; export type Rule = { - filter_list_id: number; - text: string; + filter_list_id?: number; + text?: string; }; export const getFilterName = ( @@ -915,9 +916,11 @@ export const getFilterName = ( }; export const getFilterNames = (rules: Rule[], filters: Filter[], whitelistFilters: Filter[]) => - rules.map(({ filter_list_id }: Rule) => - getFilterName(filters, whitelistFilters, filter_list_id), - ); + rules + .filter((r): r is Required => r.filter_list_id != null) + .map(({ filter_list_id }) => + getFilterName(filters, whitelistFilters, filter_list_id), + ); /** * @param {string[]} lines diff --git a/client_v2/src/helpers/trackers/trackers.ts b/client_v2/src/helpers/trackers/trackers.ts index aa11c80d1..1e1bea046 100644 --- a/client_v2/src/helpers/trackers/trackers.ts +++ b/client_v2/src/helpers/trackers/trackers.ts @@ -3,15 +3,15 @@ import whotracksmeWebsites from './whotracksme_web.json'; import trackersDb from './trackers.json'; import { REPOSITORY } from '../constants'; -/** - @typedef TrackerData - @type {object} - @property {string} id - tracker ID. - @property {string} name - tracker name. - @property {string} url - tracker website url. - @property {number} category - tracker category. - @property {source} source - tracker data source. - */ +/** Return type of {@link getTrackerData}. */ +export type TrackerData = { + id: string; + name: string; + url: string; + category: string; + source: number; + sourceData: { name: string; url: string } | null; +}; /** * Tracker data sources diff --git a/client_v2/src/stores/rewrites.ts b/client_v2/src/stores/rewrites.ts index 9943af3f1..7b596a5d6 100644 --- a/client_v2/src/stores/rewrites.ts +++ b/client_v2/src/stores/rewrites.ts @@ -22,7 +22,7 @@ type RewritesState = { isModalOpen: boolean; modalType: string; currentRewrite: RewriteEntry; - list: RewriteEntry[]; + list: (RewriteEntry & { enabled?: boolean })[]; enabled: boolean; };