AGDNS-4277 review fix

This commit is contained in:
Ildar Kamalov 2026-07-24 11:36:33 +03:00
parent 81a41ee4dc
commit 145a207442
24 changed files with 292 additions and 185 deletions

View file

@ -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(() => <DhcpToggle selectedInterface={() => '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(() => (
<DhcpToggle selectedInterface={() => '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(() => <DhcpToggle selectedInterface={() => ''} />);
const input = container.querySelector('#dhcp_enabled') as HTMLInputElement;

View file

@ -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) => {
<p class={s.description}>{intl.getMessage('blocked_services_desc')}</p>
</Show>
<Link to={getScheduleRoute()} props={scheduleRouteProps()} class={s.navItem} data-testid="blocked-services-schedule-link">
<Link
to={getScheduleRoute()}
props={scheduleRouteProps()}
class={s.navItem}
data-testid="blocked-services-schedule-link"
>
<div class={s.navItemContent}>
<div class={s.navItemTitle}>
{intl.getMessage('inactivity_schedule')}

View file

@ -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;

View file

@ -168,7 +168,7 @@ export const Dashboard = () => {
/>
<TopClients
topClients={statsState.topClients as any[]}
topClients={statsState.topClients}
numDnsQueries={statsState.numDnsQueries}
/>

View file

@ -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 = {

View file

@ -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}
/>

View file

@ -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<Rewrite>({
@ -123,7 +121,7 @@ export const DNSRewrites = () => {
<Show when={rewritesState.list.length > 0}>
<div class={cn(s.group, s.tableGroup)}>
<RewritesTable
list={rewritesState.list as Rewrite[]}
list={rewritesState.list}
processing={rewritesState.processing}
processingAdd={rewritesState.processingAdd}
processingUpdate={rewritesState.processingUpdate}

View file

@ -20,7 +20,7 @@ import {
import { DomainFaqTooltip } from './DomainFaqTooltip';
import { AnswerFaqTooltip } from './AnswerFaqTooltip';
type FormValues = {
export type FormValues = {
answer: string;
domain: string;
enabled: boolean;
@ -30,7 +30,7 @@ type ConfigureRewritesModalIdType = 'ADD_REWRITE' | 'EDIT_REWRITE';
type Props = {
modalId: ConfigureRewritesModalIdType;
rewriteToEdit?: FormValues;
rewriteToEdit?: Partial<FormValues>;
onSubmit?: (values: FormValues) => boolean | void | Promise<boolean | void>;
onClose?: () => void;
};

View file

@ -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<boolean | void>;

View file

@ -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<LogEntry | null>(null);
const [selectedEntry, setSelectedEntry] = createSignal<NormalizedQueryLogItem | null>(null);
const [disallowTarget, setDisallowTarget] = createSignal<string | null>(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 = () => {
<div class={s.desktopView}>
<LogTable
logs={visibleLogs() as LogEntry[]}
logs={visibleLogs()}
emptyStateMode={emptyStateMode()}
hasMore={hasMore()}
isLoadingMore={isLoadingMore()}
@ -275,7 +275,7 @@ export const QueryLog = () => {
<For each={visibleLogs()}>
{(entry) => (
<LogCard
entry={entry as LogEntry}
entry={entry}
onRowClick={handleRowClick}
onBlock={handleBlockDomain}
onUnblock={handleUnblockDomain}

View file

@ -25,12 +25,13 @@ import {
formatLogTimeDetailed,
formatLogDate,
} from '../../helpers';
import { LogEntry, ResponseEntry, Service } from '../../types';
import type { NormalizedQueryLogItem } from 'panel/helpers/helpers';
import { Service } from '../../types';
import s from './DetailModal.module.pcss';
type Props = {
entry: LogEntry;
entry: NormalizedQueryLogItem;
filters: Filter[];
services: Service[];
whitelistFilters: Filter[];
@ -40,7 +41,7 @@ type Props = {
onAllowService: (serviceId: string) => 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) => {
<h3 class={cn(s.sectionTitle, theme.title.h6)}>
{intl.getMessage('known_tracker')}
</h3>
<div
class={rowClassName()}
data-testid="query-log-detail-tracker-name"
data-field="tracker-name"
>
{intl.getMessage('query_log_detail_name', {
value: props.entry.tracker!.name,
span: renderValue,
})}
</div>
<div
class={rowClassName()}
data-testid="query-log-detail-tracker-category"
data-field="tracker-category"
>
{intl.getMessage('query_log_detail_category', {
value: props.entry.tracker!.category,
span: renderValue,
})}
</div>
<Show when={trackerSource()?.name}>
<div
class={rowClassName()}
data-testid="query-log-detail-tracker-source"
data-field="tracker-source"
>
{intl.getMessage('query_log_detail_source', {
value: trackerSource()!.name,
span: (content: any) =>
trackerSource()!.url ? (
<a
href={trackerSource()!.url}
target="_blank"
rel="noopener noreferrer"
class={cn(s.link, s.value)}
>
{content}
</a>
) : (
renderValue(content)
),
})}
</div>
<Show when={trackerName()}>
{(name) => (
<div
class={rowClassName()}
data-testid="query-log-detail-tracker-name"
data-field="tracker-name"
>
{intl.getMessage('query_log_detail_name', {
value: name(),
span: renderValue,
})}
</div>
)}
</Show>
<Show when={trackerCategory()}>
{(category) => (
<div
class={rowClassName()}
data-testid="query-log-detail-tracker-category"
data-field="tracker-category"
>
{intl.getMessage('query_log_detail_category', {
value: category(),
span: renderValue,
})}
</div>
)}
</Show>
<Show when={trackerSource()}>
{(source) => (
<Show when={source()?.name}>
{(name) => (
<div
class={rowClassName()}
data-testid="query-log-detail-tracker-source"
data-field="tracker-source"
>
{intl.getMessage('query_log_detail_source', {
value: name(),
span: (content: any) =>
source()?.url ? (
<a
href={source()?.url}
target="_blank"
rel="noopener noreferrer"
class={cn(s.link, s.value)}
>
{content}
</a>
) : (
renderValue(content)
),
})}
</div>
)}
</Show>
)}
</Show>
</div>
</Show>

View file

@ -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;

View file

@ -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<TableColumn<LogEntry>[]>(() => [
const columns = createMemo<TableColumn<NormalizedQueryLogItem>[]>(() => [
{
key: 'time',
header: { text: intl.getMessage('time_table_header') },
render: (_value: unknown, row: LogEntry) => <TimeCell row={row} />,
render: (_value: unknown, row: NormalizedQueryLogItem) => <TimeCell row={row} />,
width: 116,
sortable: false,
},
{
key: 'domain',
header: { text: intl.getMessage('request_table_header') },
render: (_value: unknown, row: LogEntry) => <RequestCell row={row} />,
render: (_value: unknown, row: NormalizedQueryLogItem) => <RequestCell row={row} />,
sortable: false,
},
{
key: 'status',
header: { text: intl.getMessage('status_table_header') },
render: (_value: unknown, row: LogEntry) => <StatusCell row={row} />,
render: (_value: unknown, row: NormalizedQueryLogItem) => <StatusCell row={row} />,
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 (
<ReasonCell
row={row}
@ -85,7 +86,7 @@ export const LogTable = (props: Props) => {
{
key: 'client',
header: { text: intl.getMessage('client_table_header') },
render: (_value: unknown, row: LogEntry) => (
render: (_value: unknown, row: NormalizedQueryLogItem) => (
<ClientCell onSearchSelect={handleSearchSelect} row={row} />
),
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) => (
<div
class={s.actionsCell}
data-testid="query-log-actions-cell"

View file

@ -4,13 +4,13 @@ import cn from 'clsx';
import { Icon } from 'panel/common/ui/Icon';
import theme from 'panel/lib/theme';
import { getClientLocation } from 'panel/components/QueryLog/helpers';
import { LogEntry } from 'panel/components/QueryLog/types';
import type { NormalizedQueryLogItem } from 'panel/helpers/helpers';
import s from '../LogTable.module.pcss';
type Props = {
onSearchSelect: (value: string) => (event: MouseEvent) => void;
row: LogEntry;
row: NormalizedQueryLogItem;
};
export const ClientCell = (props: Props) => {

View file

@ -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) => {
</div>
<div class={s.queryDetailsTooltipSection}>
<div class={s.queryDetailsTooltipItem}>
{intl.getMessage('query_log_detail_name', {
value: props.row.tracker!.name,
span: renderValue,
})}
</div>
<div class={s.queryDetailsTooltipItem}>
{intl.getMessage('query_log_detail_category', {
value: captitalizeWords(props.row.tracker!.category),
span: renderValue,
})}
</div>
<Show when={trackerSource()?.name}>
<div class={s.queryDetailsTooltipItem}>
{intl.getMessage('query_log_detail_source', {
value: trackerSource()!.name,
span: (content: any) =>
trackerSource()!.url ? (
<a
href={trackerSource()!.url}
target="_blank"
rel="noopener noreferrer"
class={cn(
s.queryDetailsTooltipLink,
theme.status.statusGreen,
)}
>
{content}
</a>
) : (
<span class={cn(s.queryDetailsTooltipValue, theme.text.t3)}>
{content}
</span>
),
})}
</div>
<Show when={trackerName()}>
{(name) => (
<div class={s.queryDetailsTooltipItem}>
{intl.getMessage('query_log_detail_name', {
value: name(),
span: renderValue,
})}
</div>
)}
</Show>
<Show when={trackerCategory()}>
{(category) => (
<div class={s.queryDetailsTooltipItem}>
{intl.getMessage('query_log_detail_category', {
value: captitalizeWords(category()),
span: renderValue,
})}
</div>
)}
</Show>
<Show when={trackerSource()}>
{(source) => (
<Show when={source()?.name}>
{(name) => (
<div class={s.queryDetailsTooltipItem}>
{intl.getMessage('query_log_detail_source', {
value: name(),
span: (content: any) =>
source()?.url ? (
<a
href={source()?.url}
target="_blank"
rel="noopener noreferrer"
class={cn(
s.queryDetailsTooltipLink,
theme.status.statusGreen,
)}
>
{content}
</a>
) : (
<span class={cn(s.queryDetailsTooltipValue, theme.text.t3)}>
{content}
</span>
),
})}
</div>
)}
</Show>
)}
</Show>
</div>
</Show>

View file

@ -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[];

View file

@ -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) => {

View file

@ -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) => {

View file

@ -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) => (

View file

@ -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<QueryStatusKey, 'all'> => {
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<QueryReasonKey, 'all'> => {
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<LogEntry, 'client' | 'client_id' | 'client_info'>,
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'),
);

View file

@ -1,7 +1,7 @@
import type { FilteringReason } from 'panel/api/model/filteringReason';
export type ResponseEntry = {
value: string;
value?: string;
type?: string;
ttl?: number;
};

View file

@ -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<string, unknown> | 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<Rule> => r.filter_list_id != null)
.map(({ filter_list_id }) =>
getFilterName(filters, whitelistFilters, filter_list_id),
);
/**
* @param {string[]} lines

View file

@ -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

View file

@ -22,7 +22,7 @@ type RewritesState = {
isModalOpen: boolean;
modalType: string;
currentRewrite: RewriteEntry;
list: RewriteEntry[];
list: (RewriteEntry & { enabled?: boolean })[];
enabled: boolean;
};