diff --git a/apps/dashboard/app/(app)/logs/components/charts/query-timeseries.schema.ts b/apps/dashboard/app/(app)/logs/components/charts/query-timeseries.schema.ts index f0598efa8a..2a833ea81a 100644 --- a/apps/dashboard/app/(app)/logs/components/charts/query-timeseries.schema.ts +++ b/apps/dashboard/app/(app)/logs/components/charts/query-timeseries.schema.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { filterOperatorEnum } from "../../filters.schema"; +import { logsFilterOperatorEnum } from "../../filters.schema"; export const queryTimeseriesPayload = z.object({ startTime: z.number().int(), @@ -9,7 +9,7 @@ export const queryTimeseriesPayload = z.object({ .object({ filters: z.array( z.object({ - operator: filterOperatorEnum, + operator: logsFilterOperatorEnum, value: z.string(), }), ), diff --git a/apps/dashboard/app/(app)/logs/components/control-cloud/index.tsx b/apps/dashboard/app/(app)/logs/components/control-cloud/index.tsx index 98bf8af07b..411ef5521f 100644 --- a/apps/dashboard/app/(app)/logs/components/control-cloud/index.tsx +++ b/apps/dashboard/app/(app)/logs/components/control-cloud/index.tsx @@ -1,13 +1,13 @@ import { KeyboardButton } from "@/components/keyboard-button"; import { TimestampInfo } from "@/components/timestamp-info"; +import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut"; import { cn } from "@/lib/utils"; import { XMark } from "@unkey/icons"; import { Button } from "@unkey/ui"; import { format } from "date-fns"; import { type KeyboardEvent, useCallback, useEffect, useRef, useState } from "react"; -import type { FilterValue } from "../../filters.type"; +import type { LogsFilterValue } from "../../filters.schema"; import { useFilters } from "../../hooks/use-filters"; -import { useKeyboardShortcut } from "../../hooks/use-keyboard-shortcut"; const formatFieldName = (field: string): string => { switch (field) { @@ -57,7 +57,7 @@ const formatValue = (value: string | number, field: string): string => { }; type ControlPillProps = { - filter: FilterValue; + filter: LogsFilterValue; onRemove: (id: string) => void; isFocused?: boolean; onFocus?: () => void; diff --git a/apps/dashboard/app/(app)/logs/components/controls/components/logs-display/components/display-popover.tsx b/apps/dashboard/app/(app)/logs/components/controls/components/logs-display/components/display-popover.tsx index f7b802ef01..91bfb2e391 100644 --- a/apps/dashboard/app/(app)/logs/components/controls/components/logs-display/components/display-popover.tsx +++ b/apps/dashboard/app/(app)/logs/components/controls/components/logs-display/components/display-popover.tsx @@ -1,7 +1,7 @@ import { isDisplayProperty, useLogsContext } from "@/app/(app)/logs/context/logs"; -import { useKeyboardShortcut } from "@/app/(app)/logs/hooks/use-keyboard-shortcut"; import { KeyboardButton } from "@/components/keyboard-button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut"; import { type KeyboardEvent, type PropsWithChildren, diff --git a/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/filter-checkbox.tsx b/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/filter-checkbox.tsx index 85eeb1b80c..7ca9faaada 100644 --- a/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/filter-checkbox.tsx +++ b/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/filter-checkbox.tsx @@ -1,4 +1,4 @@ -import type { FilterValue } from "@/app/(app)/logs/filters.type"; +import type { LogsFilterValue } from "@/app/(app)/logs/filters.schema"; import { useFilters } from "@/app/(app)/logs/hooks/use-filters"; import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; @@ -21,7 +21,7 @@ interface BaseCheckboxFilterProps { scrollContainerRef?: React.RefObject; renderBottomGradient?: () => React.ReactNode; renderOptionContent?: (option: TCheckbox) => React.ReactNode; - createFilterValue: (option: TCheckbox) => Pick; + createFilterValue: (option: TCheckbox) => Pick; } export const FilterCheckbox = ({ @@ -47,7 +47,7 @@ export const FilterCheckbox = ({ const selectedValues = checkboxes.filter((c) => c.checked).map((c) => createFilterValue(c)); const otherFilters = filters.filter((f) => f.field !== filterField); - const newFilters: FilterValue[] = selectedValues.map((filterValue) => ({ + const newFilters: LogsFilterValue[] = selectedValues.map((filterValue) => ({ id: crypto.randomUUID(), field: filterField, operator: "is", diff --git a/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/filters-popover.tsx b/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/filters-popover.tsx index 88b6783872..3675c21df4 100644 --- a/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/filters-popover.tsx +++ b/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/filters-popover.tsx @@ -1,7 +1,7 @@ import { useFilters } from "@/app/(app)/logs/hooks/use-filters"; -import { useKeyboardShortcut } from "@/app/(app)/logs/hooks/use-keyboard-shortcut"; import { KeyboardButton } from "@/components/keyboard-button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut"; import { CaretRight } from "@unkey/icons"; import { Button } from "@unkey/ui"; import { type KeyboardEvent, type PropsWithChildren, useEffect, useRef, useState } from "react"; diff --git a/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/hooks/use-checkbox-state.ts b/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/hooks/use-checkbox-state.ts index 334cfface7..27c715690c 100644 --- a/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/hooks/use-checkbox-state.ts +++ b/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/hooks/use-checkbox-state.ts @@ -1,9 +1,9 @@ -import type { FilterValue } from "@/app/(app)/logs/filters.type"; +import type { LogsFilterValue } from "@/app/(app)/logs/filters.schema"; import { useEffect, useState } from "react"; type UseCheckboxStateProps = { options: Array<{ id: number } & TItem>; - filters: FilterValue[]; + filters: LogsFilterValue[]; filterField: string; checkPath: keyof TItem; // Specify which field to get from checkbox item shouldSyncWithOptions?: boolean; diff --git a/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/methods-filter.tsx b/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/methods-filter.tsx index 103bcc7212..7475fb9be8 100644 --- a/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/methods-filter.tsx +++ b/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/methods-filter.tsx @@ -1,9 +1,8 @@ -import type { HttpMethod } from "@/app/(app)/logs/filters.type"; import { FilterCheckbox } from "./filter-checkbox"; type MethodOption = { id: number; - method: HttpMethod; + method: string; checked: boolean; }; diff --git a/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/paths-filter.tsx b/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/paths-filter.tsx index f56da9b9ed..39c429e66b 100644 --- a/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/paths-filter.tsx +++ b/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/paths-filter.tsx @@ -1,4 +1,4 @@ -import type { FilterValue } from "@/app/(app)/logs/filters.type"; +import type { LogsFilterValue } from "@/app/(app)/logs/filters.schema"; import { useFilters } from "@/app/(app)/logs/hooks/use-filters"; import { Checkbox } from "@/components/ui/checkbox"; import { trpc } from "@/lib/trpc/client"; @@ -33,7 +33,7 @@ export const PathsFilter = () => { // Keep all non-paths filters and add new path filters const otherFilters = filters.filter((f) => f.field !== "paths"); - const pathFilters: FilterValue[] = selectedPaths.map((path) => ({ + const pathFilters: LogsFilterValue[] = selectedPaths.map((path) => ({ id: crypto.randomUUID(), field: "paths", operator: "is", diff --git a/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/status-filter.tsx b/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/status-filter.tsx index c3371f8637..4837f04b06 100644 --- a/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/status-filter.tsx +++ b/apps/dashboard/app/(app)/logs/components/controls/components/logs-filters/components/status-filter.tsx @@ -1,4 +1,4 @@ -import type { ResponseStatus } from "@/app/(app)/logs/filters.type"; +import type { ResponseStatus } from "@/app/(app)/logs/types"; import { FilterCheckbox } from "./filter-checkbox"; type StatusOption = { diff --git a/apps/dashboard/app/(app)/logs/components/controls/components/logs-live-switch.tsx b/apps/dashboard/app/(app)/logs/components/controls/components/logs-live-switch.tsx index cb2558f137..2c026bfbeb 100644 --- a/apps/dashboard/app/(app)/logs/components/controls/components/logs-live-switch.tsx +++ b/apps/dashboard/app/(app)/logs/components/controls/components/logs-live-switch.tsx @@ -1,9 +1,9 @@ +import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut"; import { CircleCarretRight } from "@unkey/icons"; import { Button } from "@unkey/ui"; import { cn } from "@unkey/ui/src/lib/utils"; import { useLogsContext } from "../../../context/logs"; import { useFilters } from "../../../hooks/use-filters"; -import { useKeyboardShortcut } from "../../../hooks/use-keyboard-shortcut"; import { HISTORICAL_DATA_WINDOW } from "../../table/hooks/use-logs-query"; export const LogsLiveSwitch = () => { diff --git a/apps/dashboard/app/(app)/logs/components/controls/components/logs-refresh.tsx b/apps/dashboard/app/(app)/logs/components/controls/components/logs-refresh.tsx index a1fb932c81..6dca128418 100644 --- a/apps/dashboard/app/(app)/logs/components/controls/components/logs-refresh.tsx +++ b/apps/dashboard/app/(app)/logs/components/controls/components/logs-refresh.tsx @@ -1,3 +1,4 @@ +import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut"; import { trpc } from "@/lib/trpc/client"; import { Refresh3 } from "@unkey/icons"; import { Button } from "@unkey/ui"; @@ -5,7 +6,6 @@ import { cn } from "@unkey/ui/src/lib/utils"; import { useState } from "react"; import { useLogsContext } from "../../../context/logs"; import { useFilters } from "../../../hooks/use-filters"; -import { useKeyboardShortcut } from "../../../hooks/use-keyboard-shortcut"; export const LogsRefresh = () => { const { toggleLive, isLive } = useLogsContext(); diff --git a/apps/dashboard/app/(app)/logs/components/controls/components/logs-search/index.tsx b/apps/dashboard/app/(app)/logs/components/controls/components/logs-search/index.tsx index 686b3885ab..d063fd7d9d 100644 --- a/apps/dashboard/app/(app)/logs/components/controls/components/logs-search/index.tsx +++ b/apps/dashboard/app/(app)/logs/components/controls/components/logs-search/index.tsx @@ -1,7 +1,7 @@ -import { transformStructuredOutputToFilters } from "@/app/(app)/logs/filters.schema"; import { useFilters } from "@/app/(app)/logs/hooks/use-filters"; -import { useKeyboardShortcut } from "@/app/(app)/logs/hooks/use-keyboard-shortcut"; +import { transformStructuredOutputToFilters } from "@/components/logs/validation/utils/transform-structured-output-filter-format"; import { toast } from "@/components/ui/toaster"; +import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut"; import { trpc } from "@/lib/trpc/client"; import { cn } from "@/lib/utils"; import { CaretRightOutline, CircleInfoSparkle, Magnifier, Refresh3, XMark } from "@unkey/icons"; diff --git a/apps/dashboard/app/(app)/logs/components/table/query-logs.schema.ts b/apps/dashboard/app/(app)/logs/components/table/query-logs.schema.ts index de8b5416b8..198fa3ccf7 100644 --- a/apps/dashboard/app/(app)/logs/components/table/query-logs.schema.ts +++ b/apps/dashboard/app/(app)/logs/components/table/query-logs.schema.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { filterOperatorEnum } from "../../filters.schema"; +import { logsFilterOperatorEnum } from "../../filters.schema"; export const queryLogsPayload = z.object({ limit: z.number().int(), @@ -10,7 +10,7 @@ export const queryLogsPayload = z.object({ .object({ filters: z.array( z.object({ - operator: filterOperatorEnum, + operator: logsFilterOperatorEnum, value: z.string(), }), ), diff --git a/apps/dashboard/app/(app)/logs/constants.ts b/apps/dashboard/app/(app)/logs/constants.ts index 81a1b65d7b..be02968cef 100644 --- a/apps/dashboard/app/(app)/logs/constants.ts +++ b/apps/dashboard/app/(app)/logs/constants.ts @@ -2,9 +2,6 @@ export const DEFAULT_DRAGGABLE_WIDTH = 500; export const MAX_DRAGGABLE_WIDTH = 800; export const MIN_DRAGGABLE_WIDTH = 300; -export const ONE_DAY_MS = 24 * 60 * 60 * 1000; -export const DEFAULT_LOGS_FETCH_COUNT = 100; - export const YELLOW_STATES = ["RATE_LIMITED", "EXPIRED", "USAGE_EXCEEDED"]; export const RED_STATES = ["DISABLED", "FORBIDDEN", "INSUFFICIENT_PERMISSIONS"]; diff --git a/apps/dashboard/app/(app)/logs/filters-schema.test.ts b/apps/dashboard/app/(app)/logs/filters-schema.test.ts deleted file mode 100644 index 619224a5ac..0000000000 --- a/apps/dashboard/app/(app)/logs/filters-schema.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { - filterFieldConfig, - transformStructuredOutputToFilters, - validateFieldValue, -} from "./filters.schema"; -import type { FilterValue } from "./filters.type"; - -vi.stubGlobal("crypto", { - randomUUID: vi.fn(() => "test-uuid"), -}); - -describe("transformStructuredOutputToFilters", () => { - it("should transform structured output to filters correctly", () => { - const input = { - filters: [ - { - field: "status", - filters: [{ operator: "is", value: 404 }], - }, - { - field: "paths", - filters: [ - { operator: "contains", value: "api" }, - { operator: "startsWith", value: "/v1" }, - ], - }, - ], - }; - - //@ts-ignore - const result = transformStructuredOutputToFilters(input, undefined); - - expect(result).toHaveLength(3); - expect(result[0]).toMatchObject({ - field: "status", - operator: "is", - value: 404, - metadata: { - colorClass: "bg-warning-8", - }, - }); - expect(result[1]).toMatchObject({ - field: "paths", - operator: "contains", - value: "api", - }); - expect(result[2]).toMatchObject({ - field: "paths", - operator: "startsWith", - value: "/v1", - }); - }); - - it("should deduplicate filters with existing filters", () => { - const existingFilters: FilterValue[] = [ - { - id: "123", - field: "status", - operator: "is", - value: 404, - metadata: { colorClass: "bg-warning-8" }, - }, - ]; - - const input = { - filters: [ - { - field: "status", - filters: [{ operator: "is", value: 404 }], - }, - { - field: "paths", - filters: [{ operator: "contains", value: "api" }], - }, - ], - }; - - //@ts-ignore - const result = transformStructuredOutputToFilters(input, existingFilters); - - expect(result).toHaveLength(2); - expect(result[1]).toMatchObject({ - field: "paths", - operator: "contains", - value: "api", - }); - }); -}); - -describe("validateFieldValue", () => { - it("should validate status codes correctly", () => { - expect(validateFieldValue("status", 200)).toBe(true); - expect(validateFieldValue("status", 404)).toBe(true); - expect(validateFieldValue("status", 500)).toBe(true); - expect(validateFieldValue("status", 600)).toBe(false); - }); - - it("should validate HTTP methods correctly", () => { - expect(validateFieldValue("methods", "GET")).toBe(true); - expect(validateFieldValue("methods", "POST")).toBe(true); - expect(validateFieldValue("methods", "INVALID")).toBe(false); - }); - - it("should validate string fields correctly", () => { - expect(validateFieldValue("paths", "/api/v1")).toBe(true); - expect(validateFieldValue("host", "example.com")).toBe(true); - expect(validateFieldValue("requestId", "req-123")).toBe(true); - }); - - it("should validate number fields correctly", () => { - expect(validateFieldValue("startTime", 1234567890)).toBe(true); - expect(validateFieldValue("endTime", 1234567890)).toBe(true); - }); -}); - -describe("filterFieldConfig", () => { - it("should have correct status color classes", () => { - expect(filterFieldConfig.status.getColorClass!(200)).toBe("bg-success-9"); - expect(filterFieldConfig.status.getColorClass!(404)).toBe("bg-warning-8"); - expect(filterFieldConfig.status.getColorClass!(500)).toBe("bg-error-9"); - }); - - it("should have correct operators for each field", () => { - expect(filterFieldConfig.status.operators).toEqual(["is"]); - expect(filterFieldConfig.paths.operators).toEqual(["is", "contains", "startsWith", "endsWith"]); - expect(filterFieldConfig.host.operators).toEqual(["is"]); - expect(filterFieldConfig.requestId.operators).toEqual(["is"]); - }); - - it("should have correct field types", () => { - expect(filterFieldConfig.status.type).toBe("number"); - expect(filterFieldConfig.methods.type).toBe("string"); - expect(filterFieldConfig.paths.type).toBe("string"); - expect(filterFieldConfig.host.type).toBe("string"); - }); -}); diff --git a/apps/dashboard/app/(app)/logs/filters.schema.ts b/apps/dashboard/app/(app)/logs/filters.schema.ts index d3e2d85cb2..2d766fded9 100644 --- a/apps/dashboard/app/(app)/logs/filters.schema.ts +++ b/apps/dashboard/app/(app)/logs/filters.schema.ts @@ -1,146 +1,15 @@ -import { z } from "zod"; import { METHODS } from "./constants"; + import type { - FieldConfig, - FilterField, - FilterFieldConfigs, FilterValue, - HttpMethod, NumberConfig, - StatusConfig, StringConfig, -} from "./filters.type"; - -export const filterOperatorEnum = z.enum(["is", "contains", "startsWith", "endsWith"]); - -export const filterFieldEnum = z.enum([ - "host", - "requestId", - "methods", - "paths", - "status", - "startTime", - "endTime", - "since", -]); - -export const filterOutputSchema = z.object({ - filters: z.array( - z - .object({ - field: filterFieldEnum, - filters: z.array( - z.object({ - operator: filterOperatorEnum, - value: z.union([z.string(), z.number()]), - }), - ), - }) - .refine( - (data) => { - const config = filterFieldConfig[data.field]; - return data.filters.every((filter) => { - const isOperatorValid = config.operators.includes(filter.operator as any); - if (!isOperatorValid) { - return false; - } - return validateFieldValue(data.field, filter.value); - }); - }, - { - message: "Invalid field/operator/value combination", - }, - ), - ), -}); - -// Required for transforming OpenAI structured outputs into our own Filter types -export const transformStructuredOutputToFilters = ( - data: z.infer, - existingFilters: FilterValue[] = [], -): FilterValue[] => { - const uniqueFilters = [...existingFilters]; - const seenFilters = new Set(existingFilters.map((f) => `${f.field}-${f.operator}-${f.value}`)); - - for (const filterGroup of data.filters) { - filterGroup.filters.forEach((filter) => { - const baseFilter = { - field: filterGroup.field, - operator: filter.operator, - value: filter.value, - }; - - const filterKey = `${baseFilter.field}-${baseFilter.operator}-${baseFilter.value}`; - - if (seenFilters.has(filterKey)) { - return; - } - - if (filterGroup.field === "status") { - const numericValue = - typeof filter.value === "string" ? Number.parseInt(filter.value) : filter.value; - - uniqueFilters.push({ - id: crypto.randomUUID(), - ...baseFilter, - value: numericValue, - metadata: { - colorClass: filterFieldConfig.status.getColorClass?.(numericValue), - }, - }); - } else { - uniqueFilters.push({ - id: crypto.randomUUID(), - ...baseFilter, - }); - } - - seenFilters.add(filterKey); - }); - } - - return uniqueFilters; -}; - -// Type guard for config types -function isStatusConfig(config: FieldConfig): config is StatusConfig { - return "validate" in config && config.type === "number"; -} - -function isNumberConfig(config: FieldConfig): config is NumberConfig { - return config.type === "number"; -} - -function isStringConfig(config: FieldConfig): config is StringConfig { - return config.type === "string"; -} - -export function validateFieldValue(field: FilterField, value: string | number): boolean { - const config = filterFieldConfig[field]; - - if (isStatusConfig(config) && typeof value === "number") { - return config.validate(value); - } - - if (field === "methods" && typeof value === "string") { - return METHODS.includes(value as HttpMethod); - } - - if (isStringConfig(config) && typeof value === "string") { - if (config.validValues) { - return config.validValues.includes(value); - } - return config.validate ? config.validate(value) : true; - } - - if (isNumberConfig(config) && typeof value === "number") { - return config.validate ? config.validate(value) : true; - } - - return true; -} +} from "@/components/logs/validation/filter.types"; +import { createFilterOutputSchema } from "@/components/logs/validation/utils/structured-output-schema-generator"; +import { z } from "zod"; -export const filterFieldConfig: FilterFieldConfigs = { +// Configuration +export const logsFilterFieldConfig: FilterFieldConfigs = { status: { type: "number", operators: ["is"], @@ -185,3 +54,61 @@ export const filterFieldConfig: FilterFieldConfigs = { operators: ["is"], }, } as const; + +export interface StatusConfig extends NumberConfig { + type: "number"; + operators: ["is"]; + validate: (value: number) => boolean; +} + +// Schemas +export const logsFilterOperatorEnum = z.enum(["is", "contains", "startsWith", "endsWith"]); + +export const logsFilterFieldEnum = z.enum([ + "host", + "requestId", + "methods", + "paths", + "status", + "startTime", + "endTime", + "since", +]); + +export const filterOutputSchema = createFilterOutputSchema( + logsFilterFieldEnum, + logsFilterOperatorEnum, + logsFilterFieldConfig, +); + +// Types +export type LogsFilterOperator = z.infer; +export type LogsFilterField = z.infer; + +export type FilterFieldConfigs = { + status: StatusConfig; + methods: StringConfig; + paths: StringConfig; + host: StringConfig; + requestId: StringConfig; + startTime: NumberConfig; + endTime: NumberConfig; + since: StringConfig; +}; + +export type LogsFilterUrlValue = Pick< + FilterValue, + "value" | "operator" +>; +export type LogsFilterValue = FilterValue; + +export type QuerySearchParams = { + methods: LogsFilterUrlValue[] | null; + paths: LogsFilterUrlValue[] | null; + status: LogsFilterUrlValue[] | null; + startTime?: number | null; + endTime?: number | null; + since?: string | null; + host: LogsFilterUrlValue[] | null; + requestId: LogsFilterUrlValue[] | null; +}; diff --git a/apps/dashboard/app/(app)/logs/filters.type.ts b/apps/dashboard/app/(app)/logs/filters.type.ts deleted file mode 100644 index 67ca013973..0000000000 --- a/apps/dashboard/app/(app)/logs/filters.type.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { z } from "zod"; -import type { METHODS, STATUSES } from "./constants"; -import type { filterFieldEnum, filterOperatorEnum } from "./filters.schema"; - -export type FilterOperator = z.infer; -export type FilterField = z.infer; - -export type HttpMethod = (typeof METHODS)[number]; -export type ResponseStatus = (typeof STATUSES)[number]; - -export type FieldConfig = StringConfig | NumberConfig | StatusConfig; - -export interface BaseFieldConfig { - type: T extends string ? "string" : "number"; - operators: FilterOperator[]; -} - -export interface NumberConfig extends BaseFieldConfig { - type: "number"; - validate?: (value: number) => boolean; - getColorClass?: (value: number) => string; -} - -export interface StringConfig extends BaseFieldConfig { - type: "string"; - validValues?: readonly string[]; - validate?: (value: string) => boolean; - getColorClass?: (value: string) => string; -} - -export interface StatusConfig extends NumberConfig { - type: "number"; - operators: ["is"]; - validate: (value: number) => boolean; -} - -export type FilterFieldConfigs = { - status: StatusConfig; - methods: StringConfig; - paths: StringConfig; - host: StringConfig; - requestId: StringConfig; - startTime: NumberConfig; - endTime: NumberConfig; - since: StringConfig; -}; - -export type AllowedOperators = FilterFieldConfigs[F]["operators"][number]; - -export type QuerySearchParams = { - methods: FilterUrlValue[] | null; - paths: FilterUrlValue[] | null; - status: FilterUrlValue[] | null; - startTime?: number | null; - endTime?: number | null; - since?: string | null; - host: FilterUrlValue[] | null; - requestId: FilterUrlValue[] | null; -}; - -export interface FilterUrlValue { - value: string | number; - operator: FilterOperator; -} - -export interface FilterValue { - id: string; - field: FilterField; - operator: FilterOperator; - value: string | number | ResponseStatus | HttpMethod; - metadata?: { - colorClass?: string; - icon?: React.ReactNode; - }; -} diff --git a/apps/dashboard/app/(app)/logs/hooks/use-bookmarked-filters.ts b/apps/dashboard/app/(app)/logs/hooks/use-bookmarked-filters.ts index 150e645660..43d5b31e54 100644 --- a/apps/dashboard/app/(app)/logs/hooks/use-bookmarked-filters.ts +++ b/apps/dashboard/app/(app)/logs/hooks/use-bookmarked-filters.ts @@ -1,21 +1,17 @@ +import type { FilterUrlValue } from "@/components/logs/validation/filter.types"; import { useCallback, useEffect } from "react"; -import { filterFieldConfig } from "../filters.schema"; -import type { FilterField, FilterUrlValue, FilterValue } from "../filters.type"; +import { + type LogsFilterField, + type LogsFilterValue, + type QuerySearchParams, + logsFilterFieldConfig, +} from "../filters.schema"; import { useFilters } from "./use-filters"; export type SavedFiltersGroup = { id: string; createdAt: number; - filters: { - status?: FilterUrlValue[]; - methods?: FilterUrlValue[]; - paths?: FilterUrlValue[]; - host?: FilterUrlValue[]; - requestId?: FilterUrlValue[]; - startTime?: number; - endTime?: number; - since?: string; - }; + filters: QuerySearchParams; }; export const useBookmarkedFilters = () => { @@ -74,13 +70,13 @@ export const useBookmarkedFilters = () => { const applyFilterGroup = useCallback( (savedGroup: SavedFiltersGroup) => { - const reconstructedFilters: FilterValue[] = []; + const reconstructedFilters: LogsFilterValue[] = []; Object.entries(savedGroup.filters).forEach(([field, value]) => { if (["startTime", "endTime", "since"].includes(field)) { reconstructedFilters.push({ id: crypto.randomUUID(), - field: field as FilterField, + field: field as LogsFilterField, operator: "is", value: value as number | string, }); @@ -88,13 +84,15 @@ export const useBookmarkedFilters = () => { (value as FilterUrlValue[]).forEach((filter) => { reconstructedFilters.push({ id: crypto.randomUUID(), - field: field as FilterField, + field: field as LogsFilterField, operator: filter.operator, value: filter.value, metadata: field === "status" ? { - colorClass: filterFieldConfig.status.getColorClass?.(filter.value as number), + colorClass: logsFilterFieldConfig.status.getColorClass?.( + filter.value as number, + ), } : undefined, }); diff --git a/apps/dashboard/app/(app)/logs/hooks/use-filters.test.ts b/apps/dashboard/app/(app)/logs/hooks/use-filters.test.ts deleted file mode 100644 index f463c11daf..0000000000 --- a/apps/dashboard/app/(app)/logs/hooks/use-filters.test.ts +++ /dev/null @@ -1,340 +0,0 @@ -import { act, renderHook } from "@testing-library/react"; -import { useQueryStates } from "nuqs"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { parseAsFilterValueArray, parseAsRelativeTime, useFilters } from "./use-filters"; - -vi.mock("nuqs", () => { - const mockSetSearchParams = vi.fn(); - - return { - useQueryStates: vi.fn(() => [ - { - status: null, - methods: null, - paths: null, - host: null, - requestId: null, - startTime: null, - endTime: null, - }, - mockSetSearchParams, - ]), - parseAsInteger: { - parse: (str: string | null) => (str ? Number.parseInt(str) : null), - serialize: (value: number | null) => value?.toString() ?? "", - }, - }; -}); - -vi.stubGlobal("crypto", { - randomUUID: vi.fn(() => "test-uuid"), -}); - -const mockUseQueryStates = vi.mocked(useQueryStates); -const mockSetSearchParams = vi.fn(); - -describe("parseAsFilterValueArray", () => { - it("should return empty array for null input", () => { - //@ts-expect-error ts yells for no reason - expect(parseAsFilterValueArray.parse(null)).toEqual([]); - }); - - it("should return empty array for empty string", () => { - expect(parseAsFilterValueArray.parse("")).toEqual([]); - }); - - it("should parse single filter correctly", () => { - const result = parseAsFilterValueArray.parse("is:200"); - expect(result).toEqual([ - { - operator: "is", - value: "200", - }, - ]); - }); - - it("should parse multiple filters correctly", () => { - const result = parseAsFilterValueArray.parse("is:200,contains:error"); - expect(result).toEqual([ - { operator: "is", value: "200" }, - { operator: "contains", value: "error" }, - ]); - }); - - it("should return empty array for invalid operator", () => { - expect(parseAsFilterValueArray.parse("invalid:200")).toEqual([]); - }); - - it("should serialize empty array to empty string", () => { - //@ts-expect-error ts yells for no reason - expect(parseAsFilterValueArray.serialize([])).toBe(""); - }); - - it("should serialize array of filters correctly", () => { - const filters = [ - { operator: "is", value: "200" }, - { operator: "contains", value: "error" }, - ]; - //@ts-expect-error ts yells for no reason - expect(parseAsFilterValueArray?.serialize(filters)).toBe("is:200,contains:error"); - }); -}); - -describe("parseAsRelativeTime", () => { - it("should return null for null input", () => { - //@ts-expect-error ts yells for no reason - expect(parseAsRelativeTime.parse(null)).toBeNull(); - }); - - it("should return null for empty string", () => { - expect(parseAsRelativeTime.parse("")).toBeNull(); - }); - - it("should parse valid single unit formats", () => { - expect(parseAsRelativeTime.parse("1h")).toBe("1h"); - expect(parseAsRelativeTime.parse("24h")).toBe("24h"); - expect(parseAsRelativeTime.parse("7d")).toBe("7d"); - expect(parseAsRelativeTime.parse("30m")).toBe("30m"); - }); - - it("should parse valid multiple unit formats", () => { - expect(parseAsRelativeTime.parse("1h30m")).toBe("1h30m"); - expect(parseAsRelativeTime.parse("2d5h")).toBe("2d5h"); - expect(parseAsRelativeTime.parse("1d6h30m")).toBe("1d6h30m"); - }); - - it("should return null for invalid formats", () => { - expect(parseAsRelativeTime.parse("1x")).toBeNull(); - expect(parseAsRelativeTime.parse("h")).toBeNull(); - expect(parseAsRelativeTime.parse("24")).toBeNull(); - expect(parseAsRelativeTime.parse("-1h")).toBeNull(); - expect(parseAsRelativeTime.parse("1h2")).toBeNull(); - expect(parseAsRelativeTime.parse("1h 2d")).toBeNull(); - }); - - it("should serialize null to empty string", () => { - //@ts-expect-error ts yells for no reason - expect(parseAsRelativeTime.serialize(null)).toBe(""); - }); - - it("should serialize valid time strings correctly", () => { - //@ts-expect-error ts yells for no reason - expect(parseAsRelativeTime.serialize("1h")).toBe("1h"); - //@ts-expect-error ts yells for no reason - expect(parseAsRelativeTime.serialize("2d5h30m")).toBe("2d5h30m"); - }); -}); - -describe("useFilters hook", () => { - beforeEach(() => { - vi.clearAllMocks(); - mockUseQueryStates.mockImplementation(() => [ - { - status: null, - methods: null, - paths: null, - host: null, - requestId: null, - since: null, - startTime: null, - endTime: null, - }, - mockSetSearchParams, - ]); - }); - - it("should initialize with empty filters", () => { - const { result } = renderHook(() => useFilters()); - expect(result.current.filters).toEqual([]); - }); - - it("should initialize with existing filters", () => { - mockUseQueryStates.mockImplementation(() => [ - { - status: [{ operator: "is", value: "200" }], - methods: null, - paths: null, - host: null, - requestId: null, - startTime: null, - since: null, - endTime: null, - }, - mockSetSearchParams, - ]); - - const { result } = renderHook(() => useFilters()); - expect(result.current.filters).toEqual([ - { - id: "test-uuid", - field: "status", - operator: "is", - value: "200", - metadata: expect.any(Object), - }, - ]); - }); - - it("should remove filter correctly", () => { - mockUseQueryStates.mockImplementation(() => [ - { - status: [{ operator: "is", value: "200" }], - methods: null, - paths: null, - host: null, - requestId: null, - startTime: null, - since: null, - endTime: null, - }, - mockSetSearchParams, - ]); - - const { result } = renderHook(() => useFilters()); - - act(() => { - result.current.removeFilter("test-uuid"); - }); - - expect(mockSetSearchParams).toHaveBeenCalledWith({ - status: null, - methods: null, - paths: null, - host: null, - requestId: null, - startTime: null, - since: null, - endTime: null, - }); - }); - - it("should handle multiple filters", () => { - const { result } = renderHook(() => useFilters()); - - act(() => { - result.current.updateFilters([ - { - id: "test-uuid-1", - field: "status", - operator: "is", - value: 200, - }, - { - id: "test-uuid-2", - field: "methods", - operator: "is", - value: "GET", - }, - ]); - }); - - expect(mockSetSearchParams).toHaveBeenCalledWith({ - status: [{ operator: "is", value: 200 }], - methods: [{ operator: "is", value: "GET" }], - paths: null, - host: null, - requestId: null, - startTime: null, - endTime: null, - since: null, - }); - }); - - it("should handle time range filters", () => { - const { result } = renderHook(() => useFilters()); - const startTime = 1609459200000; - - act(() => { - result.current.updateFilters([ - { - id: "test-uuid", - field: "startTime", - operator: "is", - value: startTime, - }, - ]); - }); - - expect(mockSetSearchParams).toHaveBeenCalledWith({ - status: null, - methods: null, - paths: null, - host: null, - requestId: null, - startTime, - endTime: null, - since: null, - }); - }); - - it("should handle complex filter operators", () => { - const { result } = renderHook(() => useFilters()); - - act(() => { - result.current.updateFilters([ - { - id: "test-uuid-1", - field: "paths", - operator: "contains", - value: "/api", - }, - { - id: "test-uuid-2", - field: "host", - operator: "startsWith", - value: "test", - }, - { - id: "test-uuid-3", - field: "since", - operator: "is", - value: "3h", - }, - ]); - }); - - expect(mockSetSearchParams).toHaveBeenCalledWith({ - status: null, - methods: null, - paths: [{ operator: "contains", value: "/api" }], - host: [{ operator: "startsWith", value: "test" }], - requestId: null, - startTime: null, - endTime: null, - since: "3h", - }); - }); - - it("should handle clearing all filters", () => { - mockUseQueryStates.mockImplementation(() => [ - { - status: [{ operator: "is", value: "200" }], - methods: [{ operator: "is", value: "GET" }], - paths: null, - host: null, - requestId: null, - startTime: null, - endTime: null, - since: null, - }, - mockSetSearchParams, - ]); - - const { result } = renderHook(() => useFilters()); - - act(() => { - result.current.updateFilters([]); - }); - - expect(mockSetSearchParams).toHaveBeenCalledWith({ - status: null, - methods: null, - paths: null, - host: null, - requestId: null, - startTime: null, - endTime: null, - since: null, - }); - }); -}); diff --git a/apps/dashboard/app/(app)/logs/hooks/use-filters.ts b/apps/dashboard/app/(app)/logs/hooks/use-filters.ts index 78e3130ccb..07d86bc0f6 100644 --- a/apps/dashboard/app/(app)/logs/hooks/use-filters.ts +++ b/apps/dashboard/app/(app)/logs/hooks/use-filters.ts @@ -1,74 +1,30 @@ -import { getTimestampFromRelative } from "@/lib/utils"; -import { type Parser, parseAsInteger, useQueryStates } from "nuqs"; +import { + parseAsFilterValueArray, + parseAsRelativeTime, +} from "@/components/logs/validation/utils/nuqs-parsers"; +import { parseAsInteger, useQueryStates } from "nuqs"; import { useCallback, useMemo } from "react"; -import { filterFieldConfig } from "../filters.schema"; -import type { - FilterField, - FilterOperator, - FilterUrlValue, - FilterValue, - HttpMethod, - QuerySearchParams, - ResponseStatus, -} from "../filters.type"; - -export const parseAsRelativeTime: Parser = { - parse: (str: string | null) => { - if (!str) { - return null; - } - - try { - // If that function doesn't throw it means we are safe - getTimestampFromRelative(str); - return str; - } catch { - return null; - } - }, - serialize: (value: string | null) => { - if (!value) { - return ""; - } - return value; - }, -}; - -export const parseAsFilterValueArray: Parser = { - parse: (str: string | null) => { - if (!str) { - return []; - } - try { - // Format: operator:value,operator:value (e.g., "is:200,is:404") - return str.split(",").map((item) => { - const [operator, val] = item.split(/:(.+)/); - if (!["is", "contains", "startsWith", "endsWith"].includes(operator)) { - throw new Error("Invalid operator"); - } - return { - operator: operator as FilterOperator, - value: val, - }; - }); - } catch { - return []; - } - }, - serialize: (value: FilterUrlValue[]) => { - if (!value?.length) { - return ""; - } - return value.map((v) => `${v.operator}:${v.value}`).join(","); - }, -}; - +import { + type LogsFilterField, + type LogsFilterOperator, + type LogsFilterUrlValue, + type LogsFilterValue, + type QuerySearchParams, + logsFilterFieldConfig, +} from "../filters.schema"; + +const parseAsFilterValArray = parseAsFilterValueArray([ + "is", + "contains", + "startsWith", + "endsWith", +]); export const queryParamsPayload = { - requestId: parseAsFilterValueArray, - host: parseAsFilterValueArray, - methods: parseAsFilterValueArray, - paths: parseAsFilterValueArray, - status: parseAsFilterValueArray, + requestId: parseAsFilterValArray, + host: parseAsFilterValArray, + methods: parseAsFilterValArray, + paths: parseAsFilterValArray, + status: parseAsFilterValArray, startTime: parseAsInteger, endTime: parseAsInteger, since: parseAsRelativeTime, @@ -78,16 +34,16 @@ export const useFilters = () => { const [searchParams, setSearchParams] = useQueryStates(queryParamsPayload); const filters = useMemo(() => { - const activeFilters: FilterValue[] = []; + const activeFilters: LogsFilterValue[] = []; searchParams.status?.forEach((status) => { activeFilters.push({ id: crypto.randomUUID(), field: "status", operator: status.operator, - value: status.value as ResponseStatus, + value: status.value, metadata: { - colorClass: filterFieldConfig.status.getColorClass?.(status.value as number), + colorClass: logsFilterFieldConfig.status.getColorClass?.(status.value as number), }, }); }); @@ -97,7 +53,7 @@ export const useFilters = () => { id: crypto.randomUUID(), field: "methods", operator: method.operator, - value: method.value as HttpMethod, + value: method.value, }); }); @@ -133,7 +89,7 @@ export const useFilters = () => { if (value !== null && value !== undefined) { activeFilters.push({ id: crypto.randomUUID(), - field: field as FilterField, + field: field as LogsFilterField, operator: "is", value: value as string | number, }); @@ -144,7 +100,7 @@ export const useFilters = () => { }, [searchParams]); const updateFilters = useCallback( - (newFilters: FilterValue[]) => { + (newFilters: LogsFilterValue[]) => { const newParams: Partial = { paths: null, host: null, @@ -157,11 +113,11 @@ export const useFilters = () => { }; // Group filters by field - const responseStatusFilters: FilterUrlValue[] = []; - const methodFilters: FilterUrlValue[] = []; - const pathFilters: FilterUrlValue[] = []; - const hostFilters: FilterUrlValue[] = []; - const requestIdFilters: FilterUrlValue[] = []; + const responseStatusFilters: LogsFilterUrlValue[] = []; + const methodFilters: LogsFilterUrlValue[] = []; + const pathFilters: LogsFilterUrlValue[] = []; + const hostFilters: LogsFilterUrlValue[] = []; + const requestIdFilters: LogsFilterUrlValue[] = []; newFilters.forEach((filter) => { switch (filter.field) { diff --git a/apps/dashboard/app/(app)/logs/hooks/use-keyboard-shortcut.tsx b/apps/dashboard/app/(app)/logs/hooks/use-keyboard-shortcut.tsx deleted file mode 100644 index dc83d452e9..0000000000 --- a/apps/dashboard/app/(app)/logs/hooks/use-keyboard-shortcut.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useEffect } from "react"; - -type KeyCombo = { - key: string; - ctrl?: boolean; - meta?: boolean; - shift?: boolean; - alt?: boolean; -}; - -type KeyboardShortcutOptions = { - preventDefault?: boolean; - ignoreInputs?: boolean; - ignoreContentEditable?: boolean; -}; - -const defaultOptions: KeyboardShortcutOptions = { - preventDefault: true, - ignoreInputs: true, - ignoreContentEditable: true, -}; - -export function useKeyboardShortcut( - shortcut: string | KeyCombo, - callback: () => void, - options: KeyboardShortcutOptions = defaultOptions, -) { - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - // Convert simple string shortcut to KeyCombo - const combo = typeof shortcut === "string" ? { key: shortcut } : shortcut; - - // Normalize the key to lowercase for comparison - const keyMatch = e.key.toLowerCase() === combo.key.toLowerCase(); - - // Check if any modifier keys are pressed when they're not part of the shortcut - const hasUnwantedModifiers = - (combo.ctrl === undefined && e.ctrlKey) || - (combo.meta === undefined && e.metaKey) || - (combo.shift === undefined && e.shiftKey) || - (combo.alt === undefined && e.altKey); - - // If unwanted modifiers are pressed, don't trigger the shortcut - if (hasUnwantedModifiers) { - return; - } - - // Check modifier keys if specified - const ctrlMatch = combo.ctrl === undefined || e.ctrlKey === combo.ctrl; - const metaMatch = combo.meta === undefined || e.metaKey === combo.meta; - const shiftMatch = combo.shift === undefined || e.shiftKey === combo.shift; - const altMatch = combo.alt === undefined || e.altKey === combo.alt; - - // Check if we should ignore based on target - if ( - options.ignoreInputs && - (e.target instanceof HTMLInputElement || - e.target instanceof HTMLTextAreaElement || - e.target instanceof HTMLSelectElement) - ) { - return; - } - - // Check for contentEditable if option is set - if (options.ignoreContentEditable && (e.target as HTMLElement).isContentEditable) { - return; - } - - // If all conditions match, execute callback - if (keyMatch && ctrlMatch && metaMatch && shiftMatch && altMatch) { - if (options.preventDefault) { - e.preventDefault(); - } - callback(); - } - }; - - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); - }, [shortcut, callback, options]); -} diff --git a/apps/dashboard/app/(app)/logs/types.ts b/apps/dashboard/app/(app)/logs/types.ts new file mode 100644 index 0000000000..be5879b57c --- /dev/null +++ b/apps/dashboard/app/(app)/logs/types.ts @@ -0,0 +1 @@ +export type ResponseStatus = 200 | 400 | 500; diff --git a/apps/dashboard/components/logs/validation/filter.types.ts b/apps/dashboard/components/logs/validation/filter.types.ts index c9c3e1938d..3959f12a2a 100644 --- a/apps/dashboard/components/logs/validation/filter.types.ts +++ b/apps/dashboard/components/logs/validation/filter.types.ts @@ -14,6 +14,7 @@ export type BaseFieldConfig = BaseFieldConfig & { type: "number"; validate?: (value: number) => boolean; + getColorClass?: (value: number) => string; }; export type StringConfig = BaseFieldConfig & { diff --git a/apps/dashboard/components/logs/validation/utils/structured-output-schema-generator.ts b/apps/dashboard/components/logs/validation/utils/structured-output-schema-generator.ts index 4519810018..0a243a55bf 100644 --- a/apps/dashboard/components/logs/validation/utils/structured-output-schema-generator.ts +++ b/apps/dashboard/components/logs/validation/utils/structured-output-schema-generator.ts @@ -38,7 +38,7 @@ export function createFilterOutputSchema< }); } -function validateFieldValue>( +export function validateFieldValue>( field: keyof TConfig, value: string | number, filterFieldConfig: TConfig, diff --git a/apps/dashboard/lib/trpc/routers/logs/llm-search.ts b/apps/dashboard/lib/trpc/routers/logs/llm-search.ts index f6e111e6cb..ad16864baa 100644 --- a/apps/dashboard/lib/trpc/routers/logs/llm-search.ts +++ b/apps/dashboard/lib/trpc/routers/logs/llm-search.ts @@ -1,5 +1,5 @@ import { METHODS } from "@/app/(app)/logs/constants"; -import { filterFieldConfig, filterOutputSchema } from "@/app/(app)/logs/filters.schema"; +import { filterOutputSchema, logsFilterFieldConfig } from "@/app/(app)/logs/filters.schema"; import { env } from "@/lib/env"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; @@ -88,7 +88,7 @@ export const llmSearch = rateLimitedProcedure(ratelimit.update) // HELPERS const getSystemPrompt = () => { - const operatorsByField = Object.entries(filterFieldConfig) + const operatorsByField = Object.entries(logsFilterFieldConfig) .map(([field, config]) => { const operators = config.operators.join(", "); let constraints = ""; diff --git a/apps/dashboard/lib/trpc/routers/logs/llm-search/utils.test.ts b/apps/dashboard/lib/trpc/routers/logs/llm-search/utils.test.ts index e224f10214..49104c58d2 100644 --- a/apps/dashboard/lib/trpc/routers/logs/llm-search/utils.test.ts +++ b/apps/dashboard/lib/trpc/routers/logs/llm-search/utils.test.ts @@ -9,22 +9,11 @@ describe("getSystemPrompt", () => { it("should include all necessary examples and constraints", () => { const prompt = getSystemPrompt(referenceTime); - expect(prompt).toContain("show me failed requests"); - expect(prompt).toContain("show logs between 2024-01-19 and 2024-01-20"); expect(prompt).toContain('field: "methods"'); expect(prompt).toContain('field: "status"'); expect(prompt).toContain('operator: "startsWith"'); expect(prompt).toContain("GET, POST, PUT, DELETE"); }); - - it("should include correct timestamp calculations", () => { - const prompt = getSystemPrompt(referenceTime); - const dayStart = new Date(referenceTime); - dayStart.setHours(0, 0, 0, 0); - - expect(prompt).toContain(`value: ${referenceTime - 60 * 60 * 1000}`); // 1h - expect(prompt).toContain(`value: ${dayStart.getTime()}`); // day start - }); }); describe("getStructuredSearchFromLLM", () => { diff --git a/apps/dashboard/lib/trpc/routers/logs/llm-search/utils.ts b/apps/dashboard/lib/trpc/routers/logs/llm-search/utils.ts index 34a6c1fdc9..3228d3c279 100644 --- a/apps/dashboard/lib/trpc/routers/logs/llm-search/utils.ts +++ b/apps/dashboard/lib/trpc/routers/logs/llm-search/utils.ts @@ -1,5 +1,5 @@ import { METHODS } from "@/app/(app)/logs/constants"; -import { filterFieldConfig, filterOutputSchema } from "@/app/(app)/logs/filters.schema"; +import { filterOutputSchema, logsFilterFieldConfig } from "@/app/(app)/logs/filters.schema"; import { TRPCError } from "@trpc/server"; import type OpenAI from "openai"; import { zodResponseFormat } from "openai/helpers/zod.mjs"; @@ -74,7 +74,7 @@ export async function getStructuredSearchFromLLM( } } export const getSystemPrompt = (usersReferenceMS: number) => { - const operatorsByField = Object.entries(filterFieldConfig) + const operatorsByField = Object.entries(logsFilterFieldConfig) .map(([field, config]) => { const operators = config.operators.join(", "); let constraints = ""; diff --git a/apps/dashboard/lib/trpc/routers/ratelimit/llm-search/utils.test.ts b/apps/dashboard/lib/trpc/routers/ratelimit/llm-search/utils.test.ts index cfeac2b534..5ae0ef763b 100644 --- a/apps/dashboard/lib/trpc/routers/ratelimit/llm-search/utils.test.ts +++ b/apps/dashboard/lib/trpc/routers/ratelimit/llm-search/utils.test.ts @@ -9,8 +9,8 @@ describe("getSystemPrompt", () => { const prompt = getSystemPrompt(referenceTime); // Check for status examples - expect(prompt).toContain("show rejected requests"); - expect(prompt).toContain("succeeded"); + expect(prompt).toContain("blocked"); + expect(prompt).toContain("passed"); // Check for time-based examples expect(prompt).toContain("show requests from last 30m"); @@ -30,14 +30,6 @@ describe("getSystemPrompt", () => { expect(prompt).toContain('operator: "contains"'); expect(prompt).toContain('operator: "is"'); }); - - it("should include correct timestamp calculations", () => { - const prompt = getSystemPrompt(referenceTime); - - // Check for various time calculations - expect(prompt).toContain(`value: ${referenceTime}`); // Current time - expect(prompt).toContain(`value: ${referenceTime - 24 * 60 * 60 * 1000}`); // Yesterday - }); }); describe("getStructuredSearchFromLLM", () => {