From 999919b9230cd5257f41574eef42aa01386be147 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Thu, 6 Feb 2025 18:42:25 +0300 Subject: [PATCH 1/5] fix: usebility issues --- .../logs/components/charts/index.tsx | 2 +- .../components/datetime-popover.tsx | 2 +- .../logs-datetime/components/suggestions.tsx | 44 ++++++++----------- .../date-time/components/calendar.tsx | 4 ++ 4 files changed, 25 insertions(+), 27 deletions(-) diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/charts/index.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/charts/index.tsx index 9949cb0fd7..f07a159780 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/charts/index.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/charts/index.tsx @@ -56,7 +56,7 @@ export function RatelimitLogsChart({
{timeseries ? calculateTimePoints( - timeseries[0].originalTimestamp ?? Date.now(), + timeseries[0]?.originalTimestamp ?? Date.now(), timeseries.at(-1)?.originalTimestamp ?? Date.now(), ).map((time, i) => ( // biome-ignore lint/suspicious/noArrayIndexKey: use of index is acceptable here. diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/components/datetime-popover.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/components/datetime-popover.tsx index 9b34b59090..a1b4b78ed4 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/components/datetime-popover.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/components/datetime-popover.tsx @@ -1,6 +1,6 @@ -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 { processTimeFilters } from "@/lib/utils"; import { Button, DateTime, type Range, type TimeUnit } from "@unkey/ui"; import { type PropsWithChildren, useEffect, useState } from "react"; diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/components/suggestions.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/components/suggestions.tsx index 8109f18b2a..a50014302d 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/components/suggestions.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/components/suggestions.tsx @@ -1,5 +1,4 @@ "use client"; - import { cn } from "@/lib/utils"; import { Check } from "lucide-react"; import type { KeyboardEvent, PropsWithChildren } from "react"; @@ -18,59 +17,55 @@ export const DateTimeSuggestions = ({ className, options, onChange }: Suggestion ); const itemRefs = useRef<(HTMLButtonElement | null)[]>([]); - // Add effect to update focus when checked item changes useEffect(() => { const newCheckedIndex = options.findIndex((option) => option.checked); if (newCheckedIndex !== -1) { setFocusedIndex(newCheckedIndex); - // Optional: also update focus on the DOM element itemRefs.current[newCheckedIndex]?.focus(); } }, [options]); - const handleKeyDown = (e: KeyboardEvent) => { + const handleKeyDown = (e: KeyboardEvent, index: number) => { switch (e.key) { case "ArrowDown": - case "j": + case "j": { e.preventDefault(); - setFocusedIndex((prev) => { - const newIndex = (prev + 1) % options.length; - itemRefs.current[newIndex]?.focus(); - return newIndex; - }); + const nextIndex = (index + 1) % options.length; + itemRefs.current[nextIndex]?.focus(); + setFocusedIndex(nextIndex); break; + } case "ArrowUp": - case "k": + case "k": { e.preventDefault(); - setFocusedIndex((prev) => { - const newIndex = (prev - 1 + options.length) % options.length; - itemRefs.current[newIndex]?.focus(); - return newIndex; - }); + const prevIndex = (index - 1 + options.length) % options.length; + itemRefs.current[prevIndex]?.focus(); + setFocusedIndex(prevIndex); break; + } case "Enter": case " ": e.preventDefault(); - onChange(options[focusedIndex].id); + onChange(options[index].id); break; } }; return (
{options.map(({ id, display, checked }, index) => (
- - - - - ); -}; - -const PopoverHeader = () => { - return ( -
- Filter by time range - -
- ); -}; diff --git a/apps/dashboard/app/(app)/logs/components/controls/components/logs-datetime/components/suggestions.tsx b/apps/dashboard/app/(app)/logs/components/controls/components/logs-datetime/components/suggestions.tsx deleted file mode 100644 index 3464935e88..0000000000 --- a/apps/dashboard/app/(app)/logs/components/controls/components/logs-datetime/components/suggestions.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client"; - -import { cn } from "@/lib/utils"; -import { Check } from "lucide-react"; -import { useRef, useState } from "react"; -import type { KeyboardEvent, PropsWithChildren } from "react"; - -type SuggestionOption = { - id: number; - value: string | number | undefined; - display: string; - checked: boolean; -}; - -export type OptionsType = SuggestionOption[]; - -interface SuggestionsProps extends PropsWithChildren { - className?: string; - options: Array; - onChange: (id: number) => void; -} - -export const DateTimeSuggestions = ({ className, options, onChange }: SuggestionsProps) => { - // Set initial focused index to checked item or first item - const initialIndex = options.findIndex((option) => option.checked) ?? 0; - const [focusedIndex, setFocusedIndex] = useState(initialIndex); - const itemRefs = useRef<(HTMLButtonElement | null)[]>([]); - - const handleKeyDown = (e: KeyboardEvent) => { - switch (e.key) { - case "ArrowDown": - case "j": - e.preventDefault(); - setFocusedIndex((prev) => { - const newIndex = (prev + 1) % options.length; - itemRefs.current[newIndex]?.focus(); - return newIndex; - }); - break; - case "ArrowUp": - case "k": - e.preventDefault(); - setFocusedIndex((prev) => { - const newIndex = (prev - 1 + options.length) % options.length; - itemRefs.current[newIndex]?.focus(); - return newIndex; - }); - break; - case "Enter": - case " ": - e.preventDefault(); - onChange(options[focusedIndex].id); - break; - } - }; - - return ( -
- {options.map(({ id, display, checked }, index) => ( -
- -
- ))} -
- ); -}; diff --git a/apps/dashboard/app/(app)/logs/components/controls/components/logs-datetime/index.tsx b/apps/dashboard/app/(app)/logs/components/controls/components/logs-datetime/index.tsx index 75fb3e461e..4007c17f2d 100644 --- a/apps/dashboard/app/(app)/logs/components/controls/components/logs-datetime/index.tsx +++ b/apps/dashboard/app/(app)/logs/components/controls/components/logs-datetime/index.tsx @@ -1,32 +1,86 @@ +import { DatetimePopover } from "@/components/logs/datetime/datetime-popover"; import { cn } from "@/lib/utils"; import { Calendar } from "@unkey/icons"; import { Button } from "@unkey/ui"; -import { useState } from "react"; -import { DatetimePopover } from "./components/datetime-popover"; +import { useEffect, useState } from "react"; +import { useFilters } from "../../../../hooks/use-filters"; export const LogsDateTime = () => { - const [title, setTitle] = useState("Last 12 hours"); - const [isSelected, setIsSelected] = useState(false); + const [title, setTitle] = useState(null); + const { filters, updateFilters } = useFilters(); + + useEffect(() => { + if (!title) { + setTitle("Last 12 hours"); + } + }, [title]); + + const timeValues = filters + .filter((f) => ["startTime", "endTime", "since"].includes(f.field)) + .reduce( + (acc, f) => ({ + // biome-ignore lint/performance/noAccumulatingSpread: it's safe to spread + ...acc, + [f.field]: f.value, + }), + {}, + ); return ( { - setTitle(newTitle); - setIsSelected(newSelected); + initialTimeValues={timeValues} + onDateTimeChange={(startTime, endTime, since) => { + const activeFilters = filters.filter( + (f) => !["endTime", "startTime", "since"].includes(f.field), + ); + if (since !== undefined) { + updateFilters([ + ...activeFilters, + { + field: "since", + value: since, + id: crypto.randomUUID(), + operator: "is", + }, + ]); + return; + } + if (since === undefined && startTime) { + activeFilters.push({ + field: "startTime", + value: startTime, + id: crypto.randomUUID(), + operator: "is", + }); + if (endTime) { + activeFilters.push({ + field: "endTime", + value: endTime, + id: crypto.randomUUID(), + operator: "is", + }); + } + } + updateFilters(activeFilters); }} + initialTitle={title ?? ""} + onSuggestionChange={setTitle} >
diff --git a/apps/dashboard/app/(app)/logs/components/controls/components/logs-datetime/utils/process-time.ts b/apps/dashboard/app/(app)/logs/components/controls/components/logs-datetime/utils/process-time.ts deleted file mode 100644 index e7b4711efc..0000000000 --- a/apps/dashboard/app/(app)/logs/components/controls/components/logs-datetime/utils/process-time.ts +++ /dev/null @@ -1,18 +0,0 @@ -type TimeUnit = { - HH?: string; - mm?: string; - ss?: string; -}; - -//Process new Date and time filters to be added to the filters as time since epoch -export const processTimeFilters = (date?: Date, newTime?: TimeUnit) => { - if (date) { - const hours = newTime?.HH ? Number.parseInt(newTime.HH) : 0; - const minutes = newTime?.mm ? Number.parseInt(newTime.mm) : 0; - const seconds = newTime?.ss ? Number.parseInt(newTime.ss) : 0; - date.setHours(hours, minutes, seconds, 0); - return date; - } - const now = new Date(); - return now; -}; diff --git a/apps/dashboard/app/(app)/logs/components/table/logs-table.tsx b/apps/dashboard/app/(app)/logs/components/table/logs-table.tsx index 8fc307b108..59690ede54 100644 --- a/apps/dashboard/app/(app)/logs/components/table/logs-table.tsx +++ b/apps/dashboard/app/(app)/logs/components/table/logs-table.tsx @@ -113,7 +113,9 @@ const additionalColumns: Column[] = [ .join(" "), width: "1fr", render: (log: Log) => ( -
{log[key as keyof Log]}
+
+ {log[key as keyof Log]} +
), })); diff --git a/apps/dashboard/app/(app)/logs/hooks/use-filters.ts b/apps/dashboard/app/(app)/logs/hooks/use-filters.ts index c497071ce8..78e3130ccb 100644 --- a/apps/dashboard/app/(app)/logs/hooks/use-filters.ts +++ b/apps/dashboard/app/(app)/logs/hooks/use-filters.ts @@ -1,3 +1,4 @@ +import { getTimestampFromRelative } from "@/lib/utils"; import { type Parser, parseAsInteger, useQueryStates } from "nuqs"; import { useCallback, useMemo } from "react"; import { filterFieldConfig } from "../filters.schema"; @@ -18,11 +19,8 @@ export const parseAsRelativeTime: Parser = { } try { - // Validate the format matches one or more of: number + (h|d|m) - const isValid = /^(\d+[hdm])+$/.test(str); - if (!isValid) { - return null; - } + // If that function doesn't throw it means we are safe + getTimestampFromRelative(str); return str; } catch { return null; diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/components/suggestions.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/components/suggestions.tsx deleted file mode 100644 index a50014302d..0000000000 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/components/suggestions.tsx +++ /dev/null @@ -1,101 +0,0 @@ -"use client"; -import { cn } from "@/lib/utils"; -import { Check } from "lucide-react"; -import type { KeyboardEvent, PropsWithChildren } from "react"; -import { useEffect, useRef, useState } from "react"; -import type { SuggestionOption } from "../logs-datetime.type"; - -type SuggestionsProps = PropsWithChildren<{ - className?: string; - options: Array; - onChange: (id: number) => void; -}>; - -export const DateTimeSuggestions = ({ className, options, onChange }: SuggestionsProps) => { - const [focusedIndex, setFocusedIndex] = useState( - () => options.findIndex((option) => option.checked) ?? 0, - ); - const itemRefs = useRef<(HTMLButtonElement | null)[]>([]); - - useEffect(() => { - const newCheckedIndex = options.findIndex((option) => option.checked); - if (newCheckedIndex !== -1) { - setFocusedIndex(newCheckedIndex); - itemRefs.current[newCheckedIndex]?.focus(); - } - }, [options]); - - const handleKeyDown = (e: KeyboardEvent, index: number) => { - switch (e.key) { - case "ArrowDown": - case "j": { - e.preventDefault(); - const nextIndex = (index + 1) % options.length; - itemRefs.current[nextIndex]?.focus(); - setFocusedIndex(nextIndex); - break; - } - case "ArrowUp": - case "k": { - e.preventDefault(); - const prevIndex = (index - 1 + options.length) % options.length; - itemRefs.current[prevIndex]?.focus(); - setFocusedIndex(prevIndex); - break; - } - case "Enter": - case " ": - e.preventDefault(); - onChange(options[index].id); - break; - } - }; - - return ( -
- {options.map(({ id, display, checked }, index) => ( -
- -
- ))} -
- ); -}; diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/index.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/index.tsx index 4a112d5772..4007c17f2d 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/index.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/index.tsx @@ -1,18 +1,25 @@ +import { DatetimePopover } from "@/components/logs/datetime/datetime-popover"; import { cn } from "@/lib/utils"; import { Calendar } from "@unkey/icons"; import { Button } from "@unkey/ui"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useFilters } from "../../../../hooks/use-filters"; -import { DatetimePopover } from "./components/datetime-popover"; export const LogsDateTime = () => { - const [title, setTitle] = useState("Last 12 hours"); + const [title, setTitle] = useState(null); const { filters, updateFilters } = useFilters(); + + useEffect(() => { + if (!title) { + setTitle("Last 12 hours"); + } + }, [title]); + const timeValues = filters .filter((f) => ["startTime", "endTime", "since"].includes(f.field)) .reduce( (acc, f) => ({ - // biome-ignore lint/performance/noAccumulatingSpread: safe to spread + // biome-ignore lint/performance/noAccumulatingSpread: it's safe to spread ...acc, [f.field]: f.value, }), @@ -56,24 +63,24 @@ export const LogsDateTime = () => { } updateFilters(activeFilters); }} - initialTitle={title} - onSuggestionChange={(newTitle) => { - setTitle(newTitle); - }} + initialTitle={title ?? ""} + onSuggestionChange={setTitle} >
diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx index 5fc314ea0d..9eab3b1e50 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx @@ -120,7 +120,7 @@ export const RatelimitLogsTable = () => { key: "identifier", header: "Identifier", width: "15%", - render: (log) =>
{log.identifier}
, + render: (log) =>
{log.identifier}
, }, { key: "rejected", diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/hooks/use-filters.ts b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/hooks/use-filters.ts index a1d8c8bcb9..42720e7f38 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/hooks/use-filters.ts +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/hooks/use-filters.ts @@ -1,3 +1,4 @@ +import { getTimestampFromRelative } from "@/lib/utils"; import { type Parser, parseAsInteger, useQueryStates } from "nuqs"; import { useCallback, useMemo } from "react"; import { filterFieldConfig } from "../filters.schema"; @@ -16,11 +17,8 @@ export const parseAsRelativeTime: Parser = { } try { - // Validate the format matches one or more of: number + (h|d|m) - const isValid = /^(\d+[hdm])+$/.test(str); - if (!isValid) { - return null; - } + // If that function doesn't throw it means we are safe + getTimestampFromRelative(str); return str; } catch { return null; diff --git a/apps/dashboard/components/logs/datetime/constants.ts b/apps/dashboard/components/logs/datetime/constants.ts new file mode 100644 index 0000000000..9322b14085 --- /dev/null +++ b/apps/dashboard/components/logs/datetime/constants.ts @@ -0,0 +1,102 @@ +import type { OptionsType } from "./types"; + +export const OPTIONS: OptionsType = [ + { + id: 1, + value: "1m", + display: "Last minute", + checked: false, + }, + { + id: 2, + value: "5m", + display: "Last 5 minutes", + checked: false, + }, + { + id: 3, + value: "15m", + display: "Last 15 minutes", + checked: false, + }, + { + id: 4, + value: "30m", + display: "Last 30 minutes", + checked: false, + }, + { + id: 5, + value: "1h", + display: "Last 1 hour", + checked: false, + }, + { + id: 6, + value: "3h", + display: "Last 3 hours", + checked: false, + }, + { + id: 7, + value: "6h", + display: "Last 6 hours", + checked: false, + }, + { + id: 8, + value: "12h", + display: "Last 12 hours", + checked: false, + }, + { + id: 9, + value: "24h", + display: "Last 24 hours", + checked: false, + }, + { + id: 10, + value: "2d", + display: "Last 2 days", + checked: false, + }, + { + id: 11, + value: "3d", + display: "Last 3 days", + checked: false, + }, + { + id: 12, + value: "1w", + display: "Last week", + checked: false, + }, + { + id: 13, + value: "2w", + display: "Last 2 weeks", + checked: false, + }, + { + id: 14, + value: "3w", + display: "Last 3 weeks", + checked: false, + }, + { + id: 15, + value: "4w", + display: "Last 4 weeks", + checked: false, + }, + { + id: 16, + value: undefined, + display: "Custom", + checked: false, + }, +]; + +export const CUSTOM_OPTION_ID = OPTIONS.find((o) => o.value === undefined)?.id; diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/components/datetime-popover.tsx b/apps/dashboard/components/logs/datetime/datetime-popover.tsx similarity index 78% rename from apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/components/datetime-popover.tsx rename to apps/dashboard/components/logs/datetime/datetime-popover.tsx index a1b4b78ed4..78ca1f8e84 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/components/datetime-popover.tsx +++ b/apps/dashboard/components/logs/datetime/datetime-popover.tsx @@ -4,72 +4,9 @@ import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut"; import { processTimeFilters } from "@/lib/utils"; import { Button, DateTime, type Range, type TimeUnit } from "@unkey/ui"; import { type PropsWithChildren, useEffect, useState } from "react"; -import type { OptionsType } from "../logs-datetime.type"; +import { CUSTOM_OPTION_ID, OPTIONS } from "./constants"; import { DateTimeSuggestions } from "./suggestions"; - -const CUSTOM_OPTION_ID = 10; -const options: OptionsType = [ - { - id: 1, - value: "5m", - display: "Last 5 minutes", - checked: false, - }, - { - id: 2, - value: "15m", - display: "Last 15 minutes", - checked: false, - }, - { - id: 3, - value: "30m", - display: "Last 30 minutes", - checked: false, - }, - { - id: 4, - value: "1h", - display: "Last 1 hour", - checked: false, - }, - { - id: 5, - value: "3h", - display: "Last 3 hours", - checked: false, - }, - { - id: 6, - value: "6h", - display: "Last 6 hours", - checked: false, - }, - { - id: 7, - value: "12h", - display: "Last 12 hours", - checked: false, - }, - { - id: 8, - value: "24h", - display: "Last 24 hours", - checked: false, - }, - { - id: 9, - value: "48h", - display: "Last 2 days", - checked: false, - }, - { - id: 10, - value: undefined, - display: "Custom", - checked: false, - }, -]; +import type { OptionsType } from "./types"; interface DatetimePopoverProps extends PropsWithChildren { initialTitle: string; @@ -97,12 +34,12 @@ export const DatetimePopover = ({ const [time, setTime] = useState({ startTime, endTime }); const [suggestions, setSuggestions] = useState(() => { const matchingSuggestion = since - ? options.find((s) => s.value === since) + ? OPTIONS.find((s) => s.value === since) : startTime - ? options.find((s) => s.id === CUSTOM_OPTION_ID) + ? OPTIONS.find((s) => s.id === CUSTOM_OPTION_ID) : null; - return options.map((s) => ({ + return OPTIONS.map((s) => ({ ...s, checked: s.id === matchingSuggestion?.id, })); @@ -110,7 +47,7 @@ export const DatetimePopover = ({ useEffect(() => { const newTitle = since - ? options.find((s) => s.value === since)?.display ?? initialTitle + ? OPTIONS.find((s) => s.value === since)?.display ?? initialTitle : startTime ? "Custom" : initialTitle; diff --git a/apps/dashboard/components/logs/datetime/suggestions.tsx b/apps/dashboard/components/logs/datetime/suggestions.tsx new file mode 100644 index 0000000000..093789799e --- /dev/null +++ b/apps/dashboard/components/logs/datetime/suggestions.tsx @@ -0,0 +1,123 @@ +import { ScrollArea } from "@/components/ui/scroll-area"; +import { cn } from "@/lib/utils"; +import { Check } from "lucide-react"; +import type { KeyboardEvent, PropsWithChildren } from "react"; +import { useEffect, useRef, useState } from "react"; +import type { SuggestionOption } from "./types"; + +type SuggestionsProps = PropsWithChildren<{ + className?: string; + options: Array; + onChange: (id: number) => void; +}>; + +export const DateTimeSuggestions = ({ className, options, onChange }: SuggestionsProps) => { + const [focusedIndex, setFocusedIndex] = useState( + () => options.findIndex((option) => option.checked) ?? 0, + ); + const itemRefs = useRef<(HTMLButtonElement | null)[]>([]); + const scrollAreaRef = useRef(null); + + useEffect(() => { + const newCheckedIndex = options.findIndex((option) => option.checked); + if (newCheckedIndex !== -1) { + setFocusedIndex(newCheckedIndex); + itemRefs.current[newCheckedIndex]?.focus(); + } + }, [options]); + + const scrollIntoView = (index: number) => { + const element = itemRefs.current[index]; + if (element && scrollAreaRef.current) { + const container = scrollAreaRef.current; + const elementRect = element.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + + if (elementRect.bottom > containerRect.bottom) { + container.scrollTop += elementRect.bottom - containerRect.bottom; + } else if (elementRect.top < containerRect.top) { + container.scrollTop += elementRect.top - containerRect.top; + } + } + }; + + const handleKeyDown = (e: KeyboardEvent, index: number) => { + switch (e.key) { + case "ArrowDown": + case "j": { + e.preventDefault(); + const nextIndex = (index + 1) % options.length; + itemRefs.current[nextIndex]?.focus(); + setFocusedIndex(nextIndex); + scrollIntoView(nextIndex); + break; + } + case "ArrowUp": + case "k": { + e.preventDefault(); + const prevIndex = (index - 1 + options.length) % options.length; + itemRefs.current[prevIndex]?.focus(); + setFocusedIndex(prevIndex); + scrollIntoView(prevIndex); + break; + } + case "Enter": + case " ": + e.preventDefault(); + onChange(options[index].id); + break; + } + }; + + return ( +
+ +
+ {options.map(({ id, display, checked }, index) => ( +
+ +
+ ))} +
+
+
+ ); +}; diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/logs-datetime.type.ts b/apps/dashboard/components/logs/datetime/types.ts similarity index 100% rename from apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-datetime/logs-datetime.type.ts rename to apps/dashboard/components/logs/datetime/types.ts diff --git a/apps/dashboard/lib/utils.ts b/apps/dashboard/lib/utils.ts index e3bec819a1..3ab78de23b 100644 --- a/apps/dashboard/lib/utils.ts +++ b/apps/dashboard/lib/utils.ts @@ -131,18 +131,18 @@ export function throttle any>( } export const getTimestampFromRelative = (relativeTime: string): number => { - if (!relativeTime.match(/^(\d+[hdm])+$/)) { + if (!relativeTime.match(/^(\d+[whdm])+$/)) { throw new Error( - 'Invalid relative time format. Expected format: combination of numbers followed by h, d, or m (e.g., "1h", "2d", "30m", "1h30m")', + 'Invalid relative time format. Expected format: combination of numbers followed by w, h, d, or m (e.g., "1h", "2d", "30m", "1w", "1w2d")', ); } - let totalMilliseconds = 0; - - for (const [, amount, unit] of relativeTime.matchAll(/(\d+)([hdm])/g)) { + for (const [, amount, unit] of relativeTime.matchAll(/(\d+)([whdm])/g)) { const value = Number.parseInt(amount, 10); - switch (unit) { + case "w": + totalMilliseconds += value * 7 * 24 * 60 * 60 * 1000; + break; case "h": totalMilliseconds += value * 60 * 60 * 1000; break; @@ -154,7 +154,6 @@ export const getTimestampFromRelative = (relativeTime: string): number => { break; } } - return Date.now() - totalMilliseconds; }; From d439e11549d5dafb6d64dccb579c58b71088e625 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Thu, 6 Feb 2025 20:14:42 +0300 Subject: [PATCH 3/5] fix: llm search prompt --- .../controls/components/logs-search/index.tsx | 2 +- .../routers/ratelimit/llm-search/utils.ts | 36 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-search/index.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-search/index.tsx index 7ff85b4620..bb8c0120b0 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-search/index.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-search/index.tsx @@ -138,7 +138,7 @@ export const LogsSearch = () => { className="hover:text-accent-11 transition-colors cursor-pointer hover:underline" onClick={() => handlePresetQuery("Show rejected requests from today")} > - "Show rejected requests from today" + "Show blocked requests from today"
  • diff --git a/apps/dashboard/lib/trpc/routers/ratelimit/llm-search/utils.ts b/apps/dashboard/lib/trpc/routers/ratelimit/llm-search/utils.ts index b1f9abf2be..18293b2736 100644 --- a/apps/dashboard/lib/trpc/routers/ratelimit/llm-search/utils.ts +++ b/apps/dashboard/lib/trpc/routers/ratelimit/llm-search/utils.ts @@ -41,11 +41,11 @@ export async function getStructuredSearchFromLLM( code: "UNPROCESSABLE_CONTENT", message: "Try using phrases like:\n" + - "• 'show rejected requests'\n" + + "• 'show blocked requests'\n" + "• 'find requests from last 30 minutes'\n" + "• 'show requests containing test'\n" + "• 'find request req_abc123'\n" + - "• 'show succeeded requests since 1h'\n" + + "• 'show passed requests since 1h'\n" + "For additional help, contact support@unkey.dev", }); } @@ -81,14 +81,14 @@ export const getSystemPrompt = (usersReferenceMS: number) => { const operators = config.operators.join(", "); let constraints = ""; if (field === "status") { - constraints = ` and must be one of: "rejected", "succeeded"`; + constraints = ` and must be one of: "blocked", "passed"`; } return `- ${field} accepts ${operators} operator${ config.operators.length > 1 ? "s" : "" }${constraints}`; }) .join("\n"); - return `You are an expert at converting natural language queries into filters, understanding context and inferring filter types from natural expressions. Handle complex, ambiguous queries by breaking them down into clear filters. For status, use "rejected" or "succeeded". Use ${usersReferenceMS} timestamp for time-related queries. + return `You are an expert at converting natural language queries into filters, understanding context and inferring filter types from natural expressions. Handle complex, ambiguous queries by breaking them down into clear filters. For status, use "blocked" or "passed". Use ${usersReferenceMS} timestamp for time-related queries. Examples: @@ -123,21 +123,21 @@ Result: [ ] # Status Examples -Query: "show rejected requests" +Query: "show blocked requests" Result: [ { field: "status", - filters: [{ operator: "is", value: "rejected" }] + filters: [{ operator: "is", value: "blocked" }] } ] -Query: "find all succeeded and rejected requests" +Query: "find all passed and blocked requests" Result: [ { field: "status", filters: [ - { operator: "is", value: "succeeded" }, - { operator: "is", value: "rejected" } + { operator: "is", value: "passed" }, + { operator: "is", value: "blocked" } ] } ] @@ -169,11 +169,11 @@ Result: [ ] # Complex Combinations -Query: "show rejected requests from last 2h with identifier containing test" +Query: "show blocked requests from last 2h with identifier containing test" Result: [ { field: "status", - filters: [{ operator: "is", value: "rejected" }] + filters: [{ operator: "is", value: "blocked" }] }, { field: "since", @@ -194,18 +194,18 @@ ${operatorsByField} • Nx[m] for minutes (e.g., 30m, 45m) • Nx[h] for hours (e.g., 1h, 24h) • Nx[d] for days (e.g., 1d, 7d) + • Nx[d] for weeks (e.g., 1w, 2w) Multiple units can be combined (e.g., "1d 6h") Special handling rules: 1. For multiple time ranges, use the longest duration -2. Status must be exactly "rejected" or "succeeded" +2. Status must be exactly "blocked" or "passed" 3. Identifiers support both exact matches and contains operations 4. Request IDs must be exact matches Error Handling Rules: -1. Invalid time formats: Convert to nearest supported range (e.g., "1w" → "7d") -2. Invalid status values: Default to "rejected" for negative terms (failed, error), "succeeded" for positive terms -3. For ambiguous identifiers, prefer "contains" over exact match +1. Invalid status values: Default to "blocked" for negative terms (failed, error), "passed" for positive terms +2. For ambiguous identifiers, prefer "contains" over exact match Ambiguity Resolution Priority: 1. Explicit over implicit (e.g., exact identifier over partial match) @@ -216,7 +216,7 @@ Output Validation: 1. Required fields must be present: field, filters 2. Filters must have: operator, value 3. Values must match field constraints: - - status: must be "rejected" or "succeeded" + - status: must be "blocked" or "passed" - time: must be valid timestamp or duration - identifiers and requestIds: must be strings @@ -229,7 +229,7 @@ Result: [ field: "since", filters: [{ operator: "is", - value: "7d" // Converts unsupported "week" to "7d" + value: "1w" }] } ] @@ -240,7 +240,7 @@ Result: [ field: "status", filters: [{ operator: "is", - value: "rejected" // Maps "failed" to rejected status + value: "blocked" // Maps "failed" to blocked status }] } ]`; From 14bc9dfd95880cc80d014533611c8d83ecdc9f68 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Thu, 6 Feb 2025 20:21:13 +0300 Subject: [PATCH 4/5] fix: update suggestions --- apps/dashboard/components/logs/datetime/constants.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/dashboard/components/logs/datetime/constants.ts b/apps/dashboard/components/logs/datetime/constants.ts index 9322b14085..98cb0be6e1 100644 --- a/apps/dashboard/components/logs/datetime/constants.ts +++ b/apps/dashboard/components/logs/datetime/constants.ts @@ -81,18 +81,12 @@ export const OPTIONS: OptionsType = [ }, { id: 14, - value: "3w", - display: "Last 3 weeks", - checked: false, - }, - { - id: 15, value: "4w", display: "Last 4 weeks", checked: false, }, { - id: 16, + id: 15, value: undefined, display: "Custom", checked: false, From eafbe5735e666316c471cc61b8e55119705107e8 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Thu, 6 Feb 2025 21:11:10 +0300 Subject: [PATCH 5/5] fix: dont let apply without changing datetime --- .../logs/datetime/datetime-popover.tsx | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/dashboard/components/logs/datetime/datetime-popover.tsx b/apps/dashboard/components/logs/datetime/datetime-popover.tsx index 78ca1f8e84..eaf245696e 100644 --- a/apps/dashboard/components/logs/datetime/datetime-popover.tsx +++ b/apps/dashboard/components/logs/datetime/datetime-popover.tsx @@ -8,6 +8,8 @@ import { CUSTOM_OPTION_ID, OPTIONS } from "./constants"; import { DateTimeSuggestions } from "./suggestions"; import type { OptionsType } from "./types"; +const CUSTOM_PLACEHOLDER = "Custom"; + interface DatetimePopoverProps extends PropsWithChildren { initialTitle: string; initialTimeValues: { startTime?: number; endTime?: number; since?: string }; @@ -32,6 +34,11 @@ export const DatetimePopover = ({ const { startTime, since, endTime } = initialTimeValues; const [time, setTime] = useState({ startTime, endTime }); + const [lastAppliedTime, setLastAppliedTime] = useState<{ + startTime?: number; + endTime?: number; + }>({ startTime, endTime }); + const [suggestions, setSuggestions] = useState(() => { const matchingSuggestion = since ? OPTIONS.find((s) => s.value === since) @@ -47,9 +54,9 @@ export const DatetimePopover = ({ useEffect(() => { const newTitle = since - ? OPTIONS.find((s) => s.value === since)?.display ?? initialTitle + ? OPTIONS.find((s) => s.value === since)?.display ?? CUSTOM_PLACEHOLDER : startTime - ? "Custom" + ? CUSTOM_PLACEHOLDER : initialTitle; onSuggestionChange(newTitle); @@ -84,8 +91,17 @@ export const DatetimePopover = ({ }); }; + const isTimeChanged = + time.startTime !== lastAppliedTime.startTime || time.endTime !== lastAppliedTime.endTime; + const handleApplyFilter = () => { - onDateTimeChange(time.startTime, time.endTime, undefined); + if (!isTimeChanged) { + setOpen(false); + return; + } + + onDateTimeChange(time.startTime, time.endTime); + setLastAppliedTime({ startTime: time.startTime, endTime: time.endTime }); setOpen(false); }; @@ -120,6 +136,7 @@ export const DatetimePopover = ({ variant="primary" className="font-sans w-full h-9 rounded-md" onClick={handleApplyFilter} + disabled={!isTimeChanged} > Apply Filter