Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from "zod";
import { filterOperatorEnum } from "../../filters.schema";
import { ratelimitFilterOperatorEnum } from "../../filters.schema";

export const ratelimitQueryTimeseriesPayload = z.object({
startTime: z.number().int(),
Expand All @@ -10,7 +10,7 @@ export const ratelimitQueryTimeseriesPayload = z.object({
.object({
filters: z.array(
z.object({
operator: filterOperatorEnum,
operator: ratelimitFilterOperatorEnum,
value: z.string(),
}),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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 { RatelimitFilterValue } from "../../filters.schema";
import { useFilters } from "../../hooks/use-filters";
import { HISTORICAL_DATA_WINDOW } from "../table/hooks/use-logs-query";

Expand Down Expand Up @@ -44,7 +44,7 @@ const formatOperator = (operator: string, field: string): string => {
};

type ControlPillProps = {
filter: FilterValue;
filter: RatelimitFilterValue;
onRemove: (id: string) => void;
isFocused?: boolean;
onFocus?: () => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";
import { Button } from "@unkey/ui";
import { useCallback } from "react";
import type { FilterValue } from "../../../../../filters.type";
import type { RatelimitFilterValue } from "../../../../../filters.schema";
import { useFilters } from "../../../../../hooks/use-filters";
import { useCheckboxState } from "./hooks/use-checkbox-state";

Expand All @@ -21,7 +21,7 @@ interface BaseCheckboxFilterProps<TCheckbox extends BaseCheckboxOption> {
scrollContainerRef?: React.RefObject<HTMLDivElement>;
renderBottomGradient?: () => React.ReactNode;
renderOptionContent?: (option: TCheckbox) => React.ReactNode;
createFilterValue: (option: TCheckbox) => Pick<FilterValue, "value">;
createFilterValue: (option: TCheckbox) => Pick<RatelimitFilterValue, "value">;
}

export const FilterCheckbox = <TCheckbox extends BaseCheckboxOption>({
Expand All @@ -47,7 +47,7 @@ export const FilterCheckbox = <TCheckbox extends BaseCheckboxOption>({
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: RatelimitFilterValue[] = selectedValues.map((filterValue) => ({
id: crypto.randomUUID(),
field: filterField,
operator: "is",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { FilterValue } from "@/app/(app)/ratelimits/[namespaceId]/logs/filters.type";
import type { RatelimitFilterValue } from "@/app/(app)/ratelimits/[namespaceId]/logs/filters.schema";
import { useEffect, useState } from "react";

type UseCheckboxStateProps<TItem> = {
options: Array<{ id: number } & TItem>;
filters: FilterValue[];
filters: RatelimitFilterValue[];
filterField: string;
checkPath: keyof TItem; // Specify which field to get from checkbox item
shouldSyncWithOptions?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { trpc } from "@/lib/trpc/client";
import { Button } from "@unkey/ui";
import { useCallback, useEffect, useRef, useState } from "react";
import { useRatelimitLogsContext } from "../../../../../context/logs";
import type { FilterValue } from "../../../../../filters.type";
import type { RatelimitFilterValue } from "../../../../../filters.schema";
import { useFilters } from "../../../../../hooks/use-filters";
import { useCheckboxState } from "./hooks/use-checkbox-state";

Expand Down Expand Up @@ -75,7 +75,7 @@ export const IdentifiersFilter = () => {

// Keep all non-paths filters and add new path filters
const otherFilters = filters.filter((f) => f.field !== "identifiers");
const identifiersFilters: FilterValue[] = selectedPaths.map((path) => ({
const identifiersFilters: RatelimitFilterValue[] = selectedPaths.map((path) => ({
id: crypto.randomUUID(),
field: "identifiers",
operator: "is",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
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 } from "@unkey/icons";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "components/ui/tooltip";
import { useRef, useState } from "react";
import { transformStructuredOutputToFilters } from "../../../../filters.schema";
import { useFilters } from "../../../../hooks/use-filters";

export const LogsSearch = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from "zod";
import { filterOperatorEnum } from "../../filters.schema";
import { ratelimitFilterOperatorEnum } from "../../filters.schema";

export const ratelimitQueryLogsPayload = z.object({
limit: z.number().int(),
Expand All @@ -11,7 +11,7 @@ export const ratelimitQueryLogsPayload = z.object({
.object({
filters: z.array(
z.object({
operator: filterOperatorEnum,
operator: ratelimitFilterOperatorEnum,
value: z.string(),
}),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,115 +1,13 @@
import { z } from "zod";
import type {
FieldConfig,
FilterField,
FilterFieldConfigs,
FilterValue,
NumberConfig,
StringConfig,
} from "./filters.type";

export const filterOperatorEnum = z.enum(["is", "contains"]);

export const filterFieldEnum = z.enum([
"startTime",
"endTime",
"since",
"identifiers",
"requestIds",
"status",
]);

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<typeof filterOutputSchema>,
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;
}

uniqueFilters.push({
id: crypto.randomUUID(),
...baseFilter,
});

seenFilters.add(filterKey);
});
}

return uniqueFilters;
};

// Type guard for config types
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 (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 ratelimitFilterFieldConfig: FilterFieldConfigs = {
startTime: {
type: "number",
operators: ["is"],
Expand All @@ -134,11 +32,50 @@ export const filterFieldConfig: FilterFieldConfigs = {
type: "string",
operators: ["is"],
validValues: ["blocked", "passed"],
getColorClass: (value) => {
if (value === "blocked") {
return "bg-warning-9";
}
return "bg-success-9";
},
getColorClass: (value) => (value === "blocked" ? "bg-warning-9" : "bg-success-9"),
} as const,
};

// Schemas
export const ratelimitFilterOperatorEnum = z.enum(["is", "contains"]);
export const ratelimitFilterFieldEnum = z.enum([
"startTime",
"endTime",
"since",
"identifiers",
"requestIds",
"status",
]);
export const filterOutputSchema = createFilterOutputSchema(
ratelimitFilterFieldEnum,
ratelimitFilterOperatorEnum,
ratelimitFilterFieldConfig,
);

// Types
export type RatelimitFilterOperator = z.infer<typeof ratelimitFilterOperatorEnum>;
export type RatelimitFilterField = z.infer<typeof ratelimitFilterFieldEnum>;

export type FilterFieldConfigs = {
startTime: NumberConfig<RatelimitFilterOperator>;
endTime: NumberConfig<RatelimitFilterOperator>;
since: StringConfig<RatelimitFilterOperator>;
identifiers: StringConfig<RatelimitFilterOperator>;
requestIds: StringConfig<RatelimitFilterOperator>;
status: StringConfig<RatelimitFilterOperator>;
};

export type RatelimitFilterUrlValue = Pick<
FilterValue<RatelimitFilterField, RatelimitFilterOperator>,
"value" | "operator"
>;
export type RatelimitFilterValue = FilterValue<RatelimitFilterField, RatelimitFilterOperator>;

export type RatelimitQuerySearchParams = {
startTime?: number | null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need undefined and null?
I have no idea, just curious

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed for nuqs's default value. It's weird sometimes its complaining for null sometimes for undefined, so I just dumped both them there 😄

endTime?: number | null;
since?: string | null;
identifiers: RatelimitFilterUrlValue[] | null;
requestIds: RatelimitFilterUrlValue[] | null;
status: RatelimitFilterUrlValue[] | null;
};

This file was deleted.

Loading
Loading