fix query log filters

This commit is contained in:
Ildar Kamalov
2026-06-25 18:53:17 +03:00
parent 3a890b66ac
commit 2b6ae03d21
4 changed files with 122 additions and 97 deletions
@@ -1,54 +0,0 @@
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { getAdditionalLogs, setFilteredLogs, queryLogsState } from 'panel/stores/queryLogs';
import { apiClient } from 'panel/api/Api';
vi.mock('panel/api/Api', () => ({
apiClient: {
getQueryLog: vi.fn(),
},
}));
vi.mock('panel/stores/toasts', () => ({
addErrorToast: vi.fn(),
addSuccessToast: vi.fn(),
}));
describe('queryLogs store', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('does not mark the log as complete when additional loading stops', async () => {
// Mock the API to return a non-complete log
(apiClient.getQueryLog as any).mockResolvedValue({
data: [],
oldest: '',
is_entire_log: false,
});
// Simulate the state before additional loading
// The store starts with processingAdditionalLogs: false
await getAdditionalLogs();
expect(queryLogsState.processingAdditionalLogs).toBe(false);
expect(queryLogsState.isEntireLog).toBe(false);
});
it('accumulates pages until oldest is empty (short-polling)', async () => {
(apiClient.getQueryLog as any)
.mockResolvedValueOnce({
data: [{ reason: 'Rewrite' }],
oldest: 'cur',
is_entire_log: false,
})
.mockResolvedValueOnce({
data: [{ reason: 'Rewrite' }],
oldest: '',
is_entire_log: true,
});
await setFilteredLogs({ search: '', status: 'rewritten', reason: 'all' });
expect(apiClient.getQueryLog).toHaveBeenCalledTimes(2);
expect(queryLogsState.processingGetLogs).toBe(false);
});
});
+69 -5
View File
@@ -18,16 +18,80 @@ describe('queryLogs store', () => {
vi.clearAllMocks();
});
it("getAdditionalLogs appends filtered rows using the oldest cursor and filter", async () => {
(apiClient.getQueryLog as any).mockReset();
// Seed state: a full first page (20 rewritten) with more behind it.
const fullPage = Array.from({ length: 20 }, () => ({
reason: "Rewrite",
question: {},
}));
(apiClient.getQueryLog as any).mockResolvedValueOnce({
data: fullPage,
oldest: "cur",
});
await setFilteredLogs({ search: "", status: "rewritten", reason: "all" });
expect(queryLogsState.isEntireLog).toBe(false);
expect(queryLogsState.logs).toHaveLength(20);
// Load more: one extra rewritten entry, then end of log.
(apiClient.getQueryLog as any).mockResolvedValueOnce({
data: [{ reason: "Rewrite", question: {} }],
oldest: "",
});
await getAdditionalLogs();
// The load-more request must carry the cursor and the reason filter.
expect(apiClient.getQueryLog).toHaveBeenLastCalledWith(
expect.objectContaining({
older_than: "cur",
reason: expect.arrayContaining([
"Rewrite",
"RewriteEtcHosts",
"RewriteRule",
"FilteredSafeSearch",
]),
}),
);
// Must NOT send the deprecated response_status
const lastCall = (apiClient.getQueryLog as any).mock.calls.at(-1)[0];
expect(lastCall).not.toHaveProperty("response_status");
expect(queryLogsState.logs).toHaveLength(21);
expect(queryLogsState.isEntireLog).toBe(true);
expect(queryLogsState.processingAdditionalLogs).toBe(false);
});
it("setFilteredLogs sends reason strings for blocked status", async () => {
(apiClient.getQueryLog as any).mockReset();
(apiClient.getQueryLog as any).mockResolvedValue({
data: [{ reason: "FilteredBlackList", question: {} }],
oldest: "",
});
await setFilteredLogs({ search: "", status: "blocked", reason: "all" });
const lastCall = (apiClient.getQueryLog as any).mock.calls.at(-1)[0];
expect(apiClient.getQueryLog).toHaveBeenLastCalledWith(
expect.objectContaining({
reason: expect.arrayContaining([
"FilteredBlackList",
"FilteredSafeBrowsing",
"FilteredParental",
"FilteredBlockedService",
]),
}),
);
expect(lastCall).not.toHaveProperty("response_status");
});
it('does not mark the log as complete when additional loading stops', async () => {
// Mock the API to return a non-complete log
(apiClient.getQueryLog as any).mockResolvedValue({
data: [],
oldest: '',
is_entire_log: false,
oldest: 'next-cursor',
});
// Simulate the state before additional loading
// The store starts with processingAdditionalLogs: false
await getAdditionalLogs();
expect(queryLogsState.processingAdditionalLogs).toBe(false);
@@ -199,7 +199,7 @@ export const QueryLog = () => {
return;
}
setIsIncrementalLoad(true);
getAdditionalLogs(currentSearch());
getAdditionalLogs();
};
const allowedClients = (): string[] => {
+52 -37
View File
@@ -55,55 +55,69 @@ const [state, setState] = createStore<QueryLogsState>(initialState);
// ---------- Short-poll helpers (v2 parity) ----------
const REASON_TO_RESPONSE_STATUS: Record<string, string> = {
[QUERY_LOG_REASON_FILTER.BLOCKED_BY_FILTER.QUERY]: 'blocked',
[QUERY_LOG_REASON_FILTER.BLOCKED_SERVICES.QUERY]: 'blocked_services',
[QUERY_LOG_REASON_FILTER.BLOCKED_BY_THREATS.QUERY]: 'blocked_safebrowsing',
[QUERY_LOG_REASON_FILTER.BLOCKED_BY_PARENTAL_CONTROL.QUERY]: 'blocked_parental',
[QUERY_LOG_REASON_FILTER.SAFE_SEARCH.QUERY]: 'safe_search',
[QUERY_LOG_REASON_FILTER.DNS_REWRITES.QUERY]: 'rewritten',
/** Maps frontend status filter → exact backend reason strings */
const STATUS_TO_REASONS: Record<string, string[]> = {
blocked: [
'FilteredBlackList',
'FilteredSafeBrowsing',
'FilteredParental',
'FilteredBlockedService',
],
rewritten: [
'Rewrite',
'RewriteEtcHosts',
'RewriteRule',
'FilteredSafeSearch',
],
processed: ['NotFilteredNotFound'],
allowed: ['NotFilteredWhiteList'],
error: ['NotFilteredError', 'FilteredInvalid'],
all: [],
};
const STATUS_TO_RESPONSE_STATUS: Record<string, string> = {
allowed: 'whitelisted',
processed: 'processed',
blocked: 'blocked',
rewritten: 'rewritten',
/** Maps frontend reason filter query → exact backend reason strings */
const REASON_FILTER_TO_REASONS: Record<string, string[]> = {
[QUERY_LOG_REASON_FILTER.BLOCKED_BY_FILTER.QUERY]: ['FilteredBlackList'],
[QUERY_LOG_REASON_FILTER.BLOCKED_SERVICES.QUERY]: ['FilteredBlockedService'],
[QUERY_LOG_REASON_FILTER.BLOCKED_BY_THREATS.QUERY]: ['FilteredSafeBrowsing'],
[QUERY_LOG_REASON_FILTER.BLOCKED_BY_PARENTAL_CONTROL.QUERY]: [
'FilteredParental',
],
[QUERY_LOG_REASON_FILTER.SAFE_SEARCH.QUERY]: ['FilteredSafeSearch'],
[QUERY_LOG_REASON_FILTER.DNS_REWRITES.QUERY]: [
'Rewrite',
'RewriteEtcHosts',
'RewriteRule',
],
};
const getEffectiveResponseStatus = (filter?: any) => {
const getReasons = (filter?: any): string[] => {
const reason = filter?.reason ?? DEFAULT_LOGS_FILTER.reason;
const status = filter?.status ?? DEFAULT_LOGS_FILTER.status;
if (reason !== 'all') return REASON_TO_RESPONSE_STATUS[reason] ?? 'all';
return STATUS_TO_RESPONSE_STATUS[status] ?? 'all';
if (reason !== 'all') {
return REASON_FILTER_TO_REASONS[reason] ?? [];
}
return STATUS_TO_REASONS[status] ?? [];
};
const fetchLogsWithParams = async (olderThan: string, filter?: any) => {
const raw = await apiClient.getQueryLog({
const params: Record<string, any> = {
search: filter?.search ?? DEFAULT_LOGS_FILTER.search,
response_status: getEffectiveResponseStatus(filter),
older_than: olderThan,
});
};
const reasons = getReasons(filter);
if (reasons.length > 0) {
params.reason = reasons;
}
const raw = await apiClient.getQueryLog(params);
return { logs: normalizeLogs(raw.data || []), oldest: raw.oldest || '' };
};
/** Simple stateless filter: count entries matching the status */
const filterLogsByStatus = (logs: any[], status: string): any[] => {
if (!status || status === 'all') return logs;
const statusToReason: Record<string, string[]> = {
processed: ['Filtered', 'NotFiltered'],
allowed: ['NotFilteredNotFound', 'NotFilteredWhiteList'],
blocked: [
'FilteredBlackList',
'FilteredBlockedService',
'FilteredSafeBrowsing',
'FilteredParental',
'FilteredInvalid',
],
rewritten: ['Rewrite', 'RewriteEtcHosts', 'RewriteRule'],
};
const reasons = statusToReason[status];
if (!reasons) return logs;
const reasons = STATUS_TO_REASONS[status];
if (!reasons || reasons.length === 0) return logs;
return logs.filter((log: any) => reasons.includes(log.reason));
};
@@ -154,14 +168,15 @@ export const getLogs = async (currentQuery?: string) => {
}
};
export const getAdditionalLogs = async (params?: any) => {
export const getAdditionalLogs = async () => {
setState('processingAdditionalLogs', true);
try {
const data = await apiClient.getQueryLog(params);
const { filter, oldest } = untrack(() => state);
const data = await fetchLogsWithParams(oldest, filter);
setState({
logs: [...state.logs, ...normalizeLogs(data.data || [])],
oldest: data.oldest || '',
isEntireLog: data.is_entire_log || false,
logs: [...state.logs, ...data.logs],
oldest: data.oldest,
isEntireLog: data.oldest === '',
processingAdditionalLogs: false,
});
} catch (error) {