mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2026-08-04 15:28:58 +00:00
AGDNS-4277 review fix
This commit is contained in:
parent
81a41ee4dc
commit
145a207442
24 changed files with 292 additions and 185 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ export const Dashboard = () => {
|
|||
/>
|
||||
|
||||
<TopClients
|
||||
topClients={statsState.topClients as any[]}
|
||||
topClients={statsState.topClients}
|
||||
numDnsQueries={statsState.numDnsQueries}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { FilteringReason } from 'panel/api/model/filteringReason';
|
||||
|
||||
export type ResponseEntry = {
|
||||
value: string;
|
||||
value?: string;
|
||||
type?: string;
|
||||
ttl?: number;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ type RewritesState = {
|
|||
isModalOpen: boolean;
|
||||
modalType: string;
|
||||
currentRewrite: RewriteEntry;
|
||||
list: RewriteEntry[];
|
||||
list: (RewriteEntry & { enabled?: boolean })[];
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue