improve usage analytics filters

This commit is contained in:
miloschwartz
2026-08-14 11:11:43 -04:00
parent 574ae8f5f9
commit db2de4af89
2 changed files with 133 additions and 62 deletions
+6 -4
View File
@@ -1977,6 +1977,8 @@
"aiUsageFilterAllResources": "All Resources", "aiUsageFilterAllResources": "All Resources",
"aiUsageFilterAllRoles": "All Roles", "aiUsageFilterAllRoles": "All Roles",
"aiUsageFilterAllUsers": "All Users", "aiUsageFilterAllUsers": "All Users",
"aiUsageFilterSearch": "Search...",
"aiUsageFilterNotFound": "No options found",
"aiUsageResetFilters": "Reset Filters", "aiUsageResetFilters": "Reset Filters",
"aiUsageRefresh": "Refresh", "aiUsageRefresh": "Refresh",
"aiUsageTokenTypePrompt": "Prompt", "aiUsageTokenTypePrompt": "Prompt",
@@ -2003,8 +2005,8 @@
"aiUsageTopResources": "Top Resources", "aiUsageTopResources": "Top Resources",
"aiUsageResourceCost": "Resource Cost", "aiUsageResourceCost": "Resource Cost",
"aiUsageResourceTokenUsage": "Resource Token Usage", "aiUsageResourceTokenUsage": "Resource Token Usage",
"aiUsageResourceTypePublic": "Resource", "aiUsageResourceTypePublic": "Public Resource",
"aiUsageResourceTypeSite": "Site resource", "aiUsageResourceTypeSite": "Private Resource",
"aiUsageNoResource": "No resource", "aiUsageNoResource": "No resource",
"aiUsageRolesTab": "Roles", "aiUsageRolesTab": "Roles",
"aiUsageUsersTab": "Users", "aiUsageUsersTab": "Users",
@@ -3543,8 +3545,8 @@
"sourceAddress": "Source Address", "sourceAddress": "Source Address",
"destinationAddress": "Destination Address", "destinationAddress": "Destination Address",
"duration": "Duration", "duration": "Duration",
"licenseRequiredToUse": "<enterpriseEditionLink>Enterprise Edition license</enterpriseEditionLink> is required. <bookADemoLink>Book a demo or POC</bookADemoLink>", "licenseRequiredToUse": "<enterpriseEditionLink>Enterprise Edition license</enterpriseEditionLink> is required. <bookADemoLink>Book a demo or trial</bookADemoLink>",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition license</enterpriseEditionLink> is required. <bookADemoLink>Book a demo or POC</bookADemoLink>", "ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition license</enterpriseEditionLink> is required. <bookADemoLink>Book a demo or trial</bookADemoLink>",
"certResolver": "Certificate Resolver", "certResolver": "Certificate Resolver",
"certResolverDescription": "Select the certificate resolver to use for this resource.", "certResolverDescription": "Select the certificate resolver to use for this resource.",
"selectCertResolver": "Select Certificate Resolver", "selectCertResolver": "Select Certificate Resolver",
+127 -58
View File
@@ -7,20 +7,23 @@ import {
type AiUsageAnalyticsFilters type AiUsageAnalyticsFilters
} from "@app/lib/queries"; } from "@app/lib/queries";
import { useIsFetching, useQuery, useQueryClient } from "@tanstack/react-query"; import { useIsFetching, useQuery, useQueryClient } from "@tanstack/react-query";
import { RefreshCw, XIcon } from "lucide-react"; import { CheckIcon, ChevronsUpDown, RefreshCw, XIcon } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import { DateRangePicker, type DateTimeValue } from "./DateTimePicker"; import { DateRangePicker, type DateTimeValue } from "./DateTimePicker";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Card, CardHeader } from "./ui/card"; import { Card, CardHeader } from "./ui/card";
import { Label } from "./ui/label";
import { import {
Select, Command,
SelectContent, CommandEmpty,
SelectItem, CommandGroup,
SelectTrigger, CommandInput,
SelectValue CommandItem,
} from "./ui/select"; CommandList
} from "./ui/command";
import { Label } from "./ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { Separator } from "./ui/separator"; import { Separator } from "./ui/separator";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo"; import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
import { HorizontalTabs, type TabItem } from "./HorizontalTabs"; import { HorizontalTabs, type TabItem } from "./HorizontalTabs";
@@ -161,7 +164,7 @@ export function AiUsageAnalyticsData(props: AiUsageAnalyticsDataProps) {
return ( return (
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
<Card> <Card>
<CardHeader className="flex flex-row flex-wrap items-end gap-x-3 gap-y-3"> <CardHeader className="flex flex-row flex-wrap items-end gap-x-3 gap-y-3 space-y-0">
<DateRangePicker <DateRangePicker
startValue={{ startValue={{
date: dateRange.startDate, date: dateRange.startDate,
@@ -230,38 +233,40 @@ export function AiUsageAnalyticsData(props: AiUsageAnalyticsDataProps) {
onValueChange={(v) => setFilter("virtualApiKeyId", v)} onValueChange={(v) => setFilter("virtualApiKeyId", v)}
/> />
{!isEmptySearchParams && ( <div className="flex items-center gap-2 ml-auto">
{!isEmptySearchParams && (
<Button
variant="ghost"
onClick={() => router.replace(path)}
className="gap-2"
>
<XIcon className="size-4" />
{t("aiUsageResetFilters")}
</Button>
)}
<Button <Button
variant="ghost" variant="outline"
onClick={() => router.replace(path)} onClick={() =>
queryClient.invalidateQueries({
queryKey: [
...AI_USAGE_ANALYTICS_QUERY_PREFIX,
props.orgId
]
})
}
disabled={isFetching}
className="gap-2" className="gap-2"
> >
<XIcon className="size-4" /> <RefreshCw
{t("aiUsageResetFilters")} className={cn(
"size-4",
isFetching && "animate-spin"
)}
/>
{t("aiUsageRefresh")}
</Button> </Button>
)} </div>
<Button
variant="outline"
onClick={() =>
queryClient.invalidateQueries({
queryKey: [
...AI_USAGE_ANALYTICS_QUERY_PREFIX,
props.orgId
]
})
}
disabled={isFetching}
className="gap-2 ml-auto"
>
<RefreshCw
className={cn(
"size-4",
isFetching && "animate-spin"
)}
/>
{t("aiUsageRefresh")}
</Button>
</CardHeader> </CardHeader>
</Card> </Card>
@@ -287,29 +292,93 @@ type FilterSelectProps = {
}; };
function FilterSelect(props: FilterSelectProps) { function FilterSelect(props: FilterSelectProps) {
const t = useTranslations();
const [open, setOpen] = useState(false);
const selected = props.options.find(
(option) => option.value === props.value
);
return ( return (
<div className="flex flex-col items-start gap-2 w-36"> <div className="flex flex-col items-start gap-2 w-44">
<Label htmlFor={props.id}>{props.label}</Label> <Label htmlFor={props.id}>{props.label}</Label>
<Select <Popover open={open} onOpenChange={setOpen}>
onValueChange={(newValue) => <PopoverTrigger asChild>
props.onValueChange( <Button
newValue === "all" ? undefined : newValue id={props.id}
) type="button"
} variant="outline"
value={props.value ?? "all"} role="combobox"
> aria-expanded={open}
<SelectTrigger id={props.id} className="w-full"> className={cn(
<SelectValue placeholder={props.placeholder} /> "w-full justify-between font-normal",
</SelectTrigger> !selected && "text-muted-foreground"
<SelectContent className="w-full"> )}
<SelectItem value="all">{props.placeholder}</SelectItem> >
{props.options.map((option) => ( <span className="truncate text-left">
<SelectItem key={option.value} value={option.value}> {selected?.label ?? props.placeholder}
{option.label} </span>
</SelectItem> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
))} </Button>
</SelectContent> </PopoverTrigger>
</Select> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] min-w-56 p-0"
align="start"
>
<Command>
<CommandInput placeholder={t("aiUsageFilterSearch")} />
<CommandList>
<CommandEmpty>
{t("aiUsageFilterNotFound")}
</CommandEmpty>
<CommandGroup>
<CommandItem
value={props.placeholder}
onSelect={() => {
props.onValueChange(undefined);
setOpen(false);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4 shrink-0",
!props.value
? "opacity-100"
: "opacity-0"
)}
/>
{props.placeholder}
</CommandItem>
{props.options.map((option) => (
<CommandItem
key={option.value}
value={`${option.value} ${option.label}`}
onSelect={() => {
props.onValueChange(
option.value === props.value
? undefined
: option.value
);
setOpen(false);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4 shrink-0",
option.value === props.value
? "opacity-100"
: "opacity-0"
)}
/>
<span className="truncate">
{option.label}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div> </div>
); );
} }