feat: new overview filters (#9013)

Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
This commit is contained in:
Alejandro Bailo
2025-10-28 08:44:46 +01:00
committed by GitHub
parent efba5d2a8d
commit be7680786a
14 changed files with 814 additions and 95 deletions
@@ -0,0 +1,156 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { ReactNode } from "react";
import {
AWSProviderBadge,
AzureProviderBadge,
GCPProviderBadge,
GitHubProviderBadge,
KS8ProviderBadge,
M365ProviderBadge,
} from "@/components/icons/providers-badge";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/shadcn";
import type { ProviderProps, ProviderType } from "@/types/providers";
const PROVIDER_ICON: Record<ProviderType, ReactNode> = {
aws: <AWSProviderBadge width={18} height={18} />,
azure: <AzureProviderBadge width={18} height={18} />,
gcp: <GCPProviderBadge width={18} height={18} />,
kubernetes: <KS8ProviderBadge width={18} height={18} />,
m365: <M365ProviderBadge width={18} height={18} />,
github: <GitHubProviderBadge width={18} height={18} />,
};
interface AccountsSelectorProps {
providers: ProviderProps[];
}
export function AccountsSelector({ providers }: AccountsSelectorProps) {
const router = useRouter();
const searchParams = useSearchParams();
const current = searchParams.get("filter[provider_id__in]") || "";
const selectedTypes = searchParams.get("filter[provider_type__in]") || "";
const selectedTypesList = selectedTypes
? selectedTypes.split(",").filter(Boolean)
: [];
const selectedIds = current ? current.split(",").filter(Boolean) : [];
const visibleProviders = providers
.filter((p) => p.attributes.connection?.connected)
.filter((p) =>
selectedTypesList.length > 0
? selectedTypesList.includes(p.attributes.provider)
: true,
);
const handleMultiValueChange = (ids: string[]) => {
const params = new URLSearchParams(searchParams.toString());
if (ids.length > 0) {
params.set("filter[provider_id__in]", ids.join(","));
} else {
params.delete("filter[provider_id__in]");
}
// Auto-deselect provider types that no longer have any selected accounts
if (selectedTypesList.length > 0) {
// Get provider types of currently selected accounts
const selectedProviders = providers.filter((p) => ids.includes(p.id));
const selectedProviderTypes = new Set(
selectedProviders.map((p) => p.attributes.provider),
);
// Keep only provider types that still have selected accounts
const remainingProviderTypes = selectedTypesList.filter((type) =>
selectedProviderTypes.has(type as ProviderType),
);
// Update provider_type__in filter
if (remainingProviderTypes.length > 0) {
params.set(
"filter[provider_type__in]",
remainingProviderTypes.join(","),
);
} else {
params.delete("filter[provider_type__in]");
}
}
router.push(`?${params.toString()}`, { scroll: false });
};
const selectedLabel = () => {
if (selectedIds.length === 0) return null; // placeholder visible
if (selectedIds.length === 1) {
const p = providers.find((pr) => pr.id === selectedIds[0]);
const name = p ? p.attributes.alias || p.attributes.uid : selectedIds[0];
return <span className="truncate">{name}</span>;
}
return (
<span className="truncate">{selectedIds.length} accounts selected</span>
);
};
const filterDescription =
selectedTypesList.length > 0
? `Showing accounts for ${selectedTypesList.join(", ")} providers`
: "All connected cloud provider accounts";
return (
<div className="relative">
<label
htmlFor="accounts-selector"
className="sr-only"
id="accounts-label"
>
Filter by cloud provider account. {filterDescription}. Select one or
more accounts to view findings.
</label>
<Select
multiple
selectedValues={selectedIds}
onMultiValueChange={handleMultiValueChange}
ariaLabel="Cloud provider accounts filter"
>
<SelectTrigger id="accounts-selector" aria-labelledby="accounts-label">
<SelectValue placeholder="All accounts">
{selectedLabel()}
</SelectValue>
</SelectTrigger>
<SelectContent align="start">
{visibleProviders.length > 0 ? (
visibleProviders.map((p) => {
const id = p.id;
const displayName = p.attributes.alias || p.attributes.uid;
const providerType = p.attributes.provider as ProviderType;
const icon = PROVIDER_ICON[providerType];
return (
<SelectItem
key={id}
value={id}
aria-label={`${displayName} account (${providerType.toUpperCase()})`}
>
<span aria-hidden="true">{icon}</span>
<span className="truncate">{displayName}</span>
</SelectItem>
);
})
) : (
<div className="px-3 py-2 text-sm text-slate-500 dark:text-slate-400">
{selectedTypesList.length > 0
? "No accounts available for selected providers"
: "No connected accounts available"}
</div>
)}
</SelectContent>
</Select>
</div>
);
}
@@ -9,10 +9,10 @@ import {
CardContent,
CardHeader,
CardTitle,
CardVariant,
ResourceStatsCard,
StatsContainer,
ResourceStatsCardContainer,
} from "@/components/shadcn";
import { CardVariant } from "@/components/shadcn/card/resource-stats-card/resource-stats-card-content";
interface CheckFindingsProps {
failFindingsData: {
@@ -93,7 +93,7 @@ export const CheckFindings = ({
</div>
{/* Footer with ResourceStatsCards */}
<StatsContainer>
<ResourceStatsCardContainer className="flex w-full flex-col items-start justify-center gap-4 sm:flex-row md:w-[480px] md:justify-between">
<ResourceStatsCard
containerless
badge={{
@@ -106,11 +106,16 @@ export const CheckFindings = ({
{ icon: Bell, label: `${failFindingsData.new} New` },
{ icon: BellOff, label: `${failFindingsData.muted} Muted` },
]}
emptyState={
failFindingsData.total === 0
? { message: "No failed findings to display" }
: undefined
}
className="flex-1"
/>
<div className="flex items-center justify-center px-[46px]">
<div className="h-full w-px bg-slate-300 dark:bg-[rgba(39,39,42,1)]" />
<div className="flex w-full items-center justify-center sm:w-auto sm:self-stretch sm:px-[46px]">
<div className="h-px w-full bg-slate-300 sm:h-full sm:w-px dark:bg-[rgba(39,39,42,1)]" />
</div>
<ResourceStatsCard
@@ -125,9 +130,14 @@ export const CheckFindings = ({
{ icon: Bell, label: `${passFindingsData.new} New` },
{ icon: BellOff, label: `${passFindingsData.muted} Muted` },
]}
emptyState={
passFindingsData.total === 0
? { message: "No passed findings to display" }
: undefined
}
className="flex-1"
/>
</StatsContainer>
</ResourceStatsCardContainer>
</CardContent>
</BaseCard>
);
@@ -0,0 +1,194 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { lazy, Suspense } from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/shadcn";
import { type ProviderProps, ProviderType } from "@/types/providers";
const AWSProviderBadge = lazy(() =>
import("@/components/icons/providers-badge").then((m) => ({
default: m.AWSProviderBadge,
})),
);
const AzureProviderBadge = lazy(() =>
import("@/components/icons/providers-badge").then((m) => ({
default: m.AzureProviderBadge,
})),
);
const GCPProviderBadge = lazy(() =>
import("@/components/icons/providers-badge").then((m) => ({
default: m.GCPProviderBadge,
})),
);
const KS8ProviderBadge = lazy(() =>
import("@/components/icons/providers-badge").then((m) => ({
default: m.KS8ProviderBadge,
})),
);
const M365ProviderBadge = lazy(() =>
import("@/components/icons/providers-badge").then((m) => ({
default: m.M365ProviderBadge,
})),
);
const GitHubProviderBadge = lazy(() =>
import("@/components/icons/providers-badge").then((m) => ({
default: m.GitHubProviderBadge,
})),
);
type IconProps = { width: number; height: number };
const IconPlaceholder = ({ width, height }: IconProps) => (
<div style={{ width, height }} />
);
const PROVIDER_DATA: Record<
ProviderType,
{ label: string; icon: React.ComponentType<IconProps> }
> = {
aws: {
label: "Amazon Web Services",
icon: AWSProviderBadge,
},
azure: {
label: "Microsoft Azure",
icon: AzureProviderBadge,
},
gcp: {
label: "Google Cloud Platform",
icon: GCPProviderBadge,
},
kubernetes: {
label: "Kubernetes",
icon: KS8ProviderBadge,
},
m365: {
label: "Microsoft 365",
icon: M365ProviderBadge,
},
github: {
label: "GitHub",
icon: GitHubProviderBadge,
},
};
type ProviderTypeSelectorProps = {
providers: ProviderProps[];
};
export const ProviderTypeSelector = ({
providers,
}: ProviderTypeSelectorProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const currentProviders = searchParams.get("filter[provider_type__in]") || "";
const selectedTypes = currentProviders
? currentProviders.split(",").filter(Boolean)
: [];
const handleMultiValueChange = (values: string[]) => {
const params = new URLSearchParams(searchParams.toString());
// Update provider_type__in
if (values.length > 0) {
params.set("filter[provider_type__in]", values.join(","));
} else {
params.delete("filter[provider_type__in]");
}
// Clear account selection when changing provider types
// User should manually select accounts if they want to filter by specific accounts
params.delete("filter[provider_id__in]");
router.push(`?${params.toString()}`, { scroll: false });
};
const availableTypes = Array.from(
new Set(
providers
.filter((p) => p.attributes.connection?.connected)
.map((p) => p.attributes.provider),
),
) as ProviderType[];
const renderIcon = (providerType: ProviderType) => {
const IconComponent = PROVIDER_DATA[providerType].icon;
return (
<Suspense fallback={<IconPlaceholder width={24} height={24} />}>
<IconComponent width={24} height={24} />
</Suspense>
);
};
const selectedLabel = () => {
if (selectedTypes.length === 0) return null; // placeholder visible
if (selectedTypes.length === 1) {
const providerType = selectedTypes[0] as ProviderType;
return (
<span className="flex items-center gap-2">
{renderIcon(providerType)}
<span>{PROVIDER_DATA[providerType].label}</span>
</span>
);
}
return (
<span className="truncate">
{selectedTypes.length} providers selected
</span>
);
};
return (
<div className="relative">
<label
htmlFor="provider-type-selector"
className="sr-only"
id="provider-type-label"
>
Filter by cloud provider type. Select one or more providers to view
findings.
</label>
<Select
multiple
selectedValues={selectedTypes}
onMultiValueChange={handleMultiValueChange}
ariaLabel="Cloud provider type filter"
>
<SelectTrigger
id="provider-type-selector"
aria-labelledby="provider-type-label"
>
<SelectValue placeholder="All providers">
{selectedLabel()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{availableTypes.length > 0 ? (
availableTypes.map((providerType) => (
<SelectItem
key={providerType}
value={providerType}
aria-label={`${PROVIDER_DATA[providerType].label} provider`}
>
<span aria-hidden="true">{renderIcon(providerType)}</span>
<span>{PROVIDER_DATA[providerType].label}</span>
</SelectItem>
))
) : (
<div className="px-3 py-2 text-sm text-slate-500 dark:text-slate-400">
No connected providers available
</div>
)}
</SelectContent>
</Select>
</div>
);
};
+9 -1
View File
@@ -1,10 +1,13 @@
import { Suspense } from "react";
import { getFindingsByStatus } from "@/actions/overview/overview";
import { getProviders } from "@/actions/providers";
import { ContentLayout } from "@/components/ui";
import { SearchParamsProps } from "@/types";
import { AccountsSelector } from "./components/accounts-selector";
import { CheckFindings } from "./components/check-findings";
import { ProviderTypeSelector } from "./components/provider-type-selector";
const FILTER_PREFIX = "filter[";
@@ -24,10 +27,15 @@ export default async function NewOverviewPage({
searchParams: Promise<SearchParamsProps>;
}) {
const resolvedSearchParams = await searchParams;
const providersData = await getProviders({ page: 1, pageSize: 200 });
return (
<ContentLayout title="New Overview" icon="lucide:square-chart-gantt">
<div className="flex min-h-[60vh] items-center justify-center p-6">
<div className="xxl:grid-cols-4 mb-6 grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<ProviderTypeSelector providers={providersData?.data ?? []} />
<AccountsSelector providers={providersData?.data ?? []} />
</div>
<div className="flex flex-col gap-6 md:flex-row">
<Suspense
fallback={
<div className="flex h-[400px] w-full max-w-md items-center justify-center rounded-xl border border-zinc-900 bg-stone-950">
+32 -14
View File
@@ -94,11 +94,25 @@ export function DonutChart({
change: item.change,
}));
const legendPayload = chartData.map((entry) => ({
value: entry.name,
const total = chartData.reduce((sum, d) => sum + (Number(d.value) || 0), 0);
const isEmpty = total <= 0;
const emptyData = [
{
name: "No data",
value: 1,
fill: "var(--chart-border-emphasis)",
color: "var(--chart-border-emphasis)",
percentage: 0,
change: undefined,
},
];
const legendPayload = (isEmpty ? emptyData : chartData).map((entry) => ({
value: isEmpty ? "No data" : entry.name,
color: entry.color,
payload: {
percentage: entry.percentage,
percentage: isEmpty ? 0 : entry.percentage,
},
}));
@@ -109,9 +123,9 @@ export function DonutChart({
className="mx-auto aspect-square max-h-[350px]"
>
<PieChart>
<Tooltip content={<CustomTooltip />} />
{!isEmpty && <Tooltip content={<CustomTooltip />} />}
<Pie
data={chartData}
data={isEmpty ? emptyData : chartData}
dataKey="value"
nameKey="name"
innerRadius={innerRadius}
@@ -119,7 +133,7 @@ export function DonutChart({
strokeWidth={0}
paddingAngle={0}
>
{chartData.map((entry, index) => {
{(isEmpty ? emptyData : chartData).map((entry, index) => {
const opacity =
hoveredIndex === null ? 1 : hoveredIndex === index ? 1 : 0.5;
return (
@@ -133,14 +147,18 @@ export function DonutChart({
/>
);
})}
{centerLabel && (
{(centerLabel || isEmpty) && (
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
const centerValue = centerLabel ? centerLabel.value : 0;
const centerText = centerLabel
? centerLabel.label
: "No data";
const formattedValue =
typeof centerLabel.value === "number"
? centerLabel.value.toLocaleString()
: centerLabel.value;
typeof centerValue === "number"
? centerValue.toLocaleString()
: centerValue;
return (
<text
@@ -151,8 +169,8 @@ export function DonutChart({
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="text-3xl font-bold text-black dark:text-white"
y={(viewBox.cy || 0) - 6}
className="text-2xl font-bold text-zinc-800 dark:text-zinc-300"
style={{
fill: "currentColor",
}}
@@ -162,12 +180,12 @@ export function DonutChart({
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="text-black dark:text-white"
className="text-xs text-zinc-800 dark:text-zinc-400"
style={{
fill: "currentColor",
}}
>
{centerLabel.label}
{centerText}
</tspan>
</text>
);
+13 -7
View File
@@ -1,11 +1,21 @@
import { cn } from "@/lib/utils";
export const CardVariant = {
default: "default",
fail: "fail",
pass: "pass",
warning: "warning",
info: "info",
} as const;
export type CardVariant = (typeof CardVariant)[keyof typeof CardVariant];
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6",
className,
)}
{...props}
@@ -18,7 +28,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
@@ -64,11 +74,7 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
<div data-slot="card-content" className={cn("", className)} {...props} />
);
}
@@ -8,8 +8,8 @@ const containerVariants = cva(
"rounded-[12px]",
"border",
"backdrop-blur-[46px]",
"border-[rgba(38,38,38,0.70)]",
"bg-[rgba(23,23,23,0.50)]",
"border-slate-300",
"bg-[#F8FAFC80]",
"dark:border-[rgba(38,38,38,0.70)]",
"dark:bg-[rgba(23,23,23,0.50)]",
],
@@ -3,21 +3,13 @@ import { LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { CardVariant } from "../card";
export interface StatItem {
icon: LucideIcon;
label: string;
}
export const CardVariant = {
default: "default",
fail: "fail",
pass: "pass",
warning: "warning",
info: "info",
} as const;
export type CardVariant = (typeof CardVariant)[keyof typeof CardVariant];
const variantColors = {
default: "#868994",
fail: "#f54280",
@@ -3,12 +3,10 @@ import { LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { CardVariant } from "../card";
import { ResourceStatsCardContainer } from "./resource-stats-card-container";
import type { StatItem } from "./resource-stats-card-content";
import {
CardVariant,
ResourceStatsCardContent,
} from "./resource-stats-card-content";
import { ResourceStatsCardContent } from "./resource-stats-card-content";
import { ResourceStatsCardHeader } from "./resource-stats-card-header";
export type { StatItem };
@@ -1,25 +0,0 @@
import { cn } from "@/lib/utils";
interface StatsContainerProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
const StatsContainer = ({
className,
children,
...props
}: StatsContainerProps) => {
return (
<div
className={cn(
"flex rounded-xl border border-slate-200 bg-white px-[19px] py-[9px] dark:border-[rgba(38,38,38,0.7)] dark:bg-[rgba(23,23,23,0.5)] dark:backdrop-blur-[46px]",
className,
)}
{...props}
>
{children}
</div>
);
};
export { StatsContainer };
+1 -1
View File
@@ -5,4 +5,4 @@ export * from "./card/resource-stats-card/resource-stats-card-container";
export * from "./card/resource-stats-card/resource-stats-card-content";
export * from "./card/resource-stats-card/resource-stats-card-divider";
export * from "./card/resource-stats-card/resource-stats-card-header";
export * from "./card/stats-container";
export * from "./select/select";
+350
View File
@@ -0,0 +1,350 @@
"use client";
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon, X } from "lucide-react";
import { createContext, useContext, useId } from "react";
import { cn } from "@/lib/utils";
// Context for managing multi-select state
type SelectContextValue = {
multiple?: boolean;
selectedValues?: string[];
onMultiValueChange?: (values: string[]) => void;
ariaLabel?: string;
liveRegionId?: string;
};
const SelectContext = createContext<SelectContextValue>({});
function Select({
allowDeselect = false,
multiple = false,
value,
onValueChange,
selectedValues = [],
onMultiValueChange,
ariaLabel,
...props
}: Omit<React.ComponentProps<typeof SelectPrimitive.Root>, "onValueChange"> & {
allowDeselect?: boolean;
multiple?: boolean;
selectedValues?: string[];
onValueChange?: (value: string) => void;
onMultiValueChange?: (values: string[]) => void;
ariaLabel?: string;
}) {
const liveRegionId = useId();
const handleValueChange = (nextValue: string) => {
if (multiple && onMultiValueChange) {
// Multi-select: toggle the value
const newValues = selectedValues.includes(nextValue)
? selectedValues.filter((v) => v !== nextValue)
: [...selectedValues, nextValue];
onMultiValueChange(newValues);
} else if (
allowDeselect &&
typeof value === "string" &&
value === nextValue
) {
// Single-select with deselect
onValueChange?.("");
} else {
// Single-select
onValueChange?.(nextValue);
}
};
const contextValue = {
multiple,
selectedValues,
onMultiValueChange,
ariaLabel,
liveRegionId,
};
return (
<SelectContext.Provider value={contextValue}>
<SelectPrimitive.Root
data-slot="select"
value={multiple ? "" : value}
onValueChange={handleValueChange}
{...props}
/>
{/* Live region for screen reader announcements */}
{multiple && (
<div
id={liveRegionId}
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{selectedValues.length > 0
? `${selectedValues.length} ${selectedValues.length === 1 ? "item" : "items"} selected`
: "No items selected"}
</div>
)}
</SelectContext.Provider>
);
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
placeholder,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
const { multiple, selectedValues } = useContext(SelectContext);
// For multi-select, render custom children or placeholder
if (multiple) {
return (
<span data-slot="select-value">
{selectedValues && selectedValues.length > 0 ? children : placeholder}
</span>
);
}
// For single-select, use default Radix behavior
return (
<SelectPrimitive.Value
data-slot="select-value"
placeholder={placeholder}
{...props}
>
{children}
</SelectPrimitive.Value>
);
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
}) {
const { multiple, selectedValues, onMultiValueChange, ariaLabel } =
useContext(SelectContext);
const hasSelection = multiple && selectedValues && selectedValues.length > 0;
const handleClear = (e: React.MouseEvent) => {
e.stopPropagation();
if (onMultiValueChange) {
onMultiValueChange([]);
}
};
const clearButtonLabel = `Clear ${ariaLabel || "selection"}${hasSelection ? ` (${selectedValues.length} selected)` : ""}`;
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
aria-label={ariaLabel}
aria-multiselectable={multiple ? "true" : undefined}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-full items-center justify-between gap-2 rounded-lg border border-slate-400 px-4 py-3 text-base leading-7 whitespace-nowrap text-slate-950 shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-slate-600 focus-visible:ring-2 focus-visible:ring-slate-600 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[52px] data-[size=sm]:h-10 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:border-[#262626] dark:bg-[#171717] dark:text-white dark:focus-visible:ring-slate-400 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-6",
className,
)}
{...props}
>
{children}
<div className="flex items-center gap-1">
{hasSelection && (
<span
role="button"
tabIndex={-1}
onClick={handleClear}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
e.stopPropagation();
handleClear(e as unknown as React.MouseEvent);
}
}}
className="pointer-events-auto cursor-pointer rounded-sm p-0.5 opacity-70 transition-opacity hover:opacity-100 focus:opacity-100 focus:ring-2 focus:ring-slate-600 focus:ring-offset-2 focus:outline-none dark:focus:ring-slate-400"
aria-label={clearButtonLabel}
>
<X
className="size-4 text-slate-950 dark:text-white"
aria-hidden="true"
/>
</span>
)}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon
className="size-6 text-slate-950 dark:text-white"
aria-hidden="true"
/>
</SelectPrimitive.Icon>
</div>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = "popper",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg border border-slate-400 bg-white text-slate-950 shadow-md dark:border-[#262626] dark:bg-[#171717] dark:text-white",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"flex flex-col gap-1 p-3",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
);
}
function SelectItem({
className,
children,
value,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
const { multiple, selectedValues } = useContext(SelectContext);
const isSelected = multiple && selectedValues?.includes(value);
return (
<SelectPrimitive.Item
data-slot="select-item"
value={value}
aria-selected={multiple ? isSelected : undefined}
aria-checked={multiple ? isSelected : undefined}
role={multiple ? "option" : undefined}
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-lg py-2.5 pr-10 pl-3 text-base outline-hidden select-none hover:bg-slate-200 focus:ring-2 focus:ring-slate-600 focus:ring-inset data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:hover:bg-slate-700/50 dark:focus:ring-slate-400 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5",
isSelected && "bg-slate-100 dark:bg-slate-800/50",
className,
)}
{...props}
>
<SelectPrimitive.ItemText asChild>
<span className="flex min-w-0 items-center gap-2">{children}</span>
</SelectPrimitive.ItemText>
<span
className="absolute right-3 flex size-4 items-center justify-center"
aria-hidden="true"
>
{multiple ? (
// Multi-select: show check when selected
isSelected && (
<CheckIcon className="size-5 text-slate-950 dark:text-white" />
)
) : (
// Single-select: use radix indicator
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-5 text-slate-950 dark:text-white" />
</SelectPrimitive.ItemIndicator>
)}
</span>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUpIcon className="size-4 text-slate-950 dark:text-white" />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDownIcon className="size-4 text-slate-950 dark:text-white" />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};
+23 -23
View File
@@ -7,7 +7,7 @@ import {
ChevronUpIcon,
} from "@radix-ui/react-icons";
import * as SelectPrimitive from "@radix-ui/react-select";
import * as React from "react";
import { ComponentPropsWithoutRef, ForwardedRef, forwardRef } from "react";
import { cn } from "@/lib/utils";
@@ -17,9 +17,9 @@ const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
const SelectTrigger = forwardRef<
HTMLButtonElement,
ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
@@ -37,9 +37,9 @@ const SelectTrigger = React.forwardRef<
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
const SelectScrollUpButton = forwardRef<
HTMLDivElement,
ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
@@ -54,9 +54,9 @@ const SelectScrollUpButton = React.forwardRef<
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
const SelectScrollDownButton = forwardRef<
HTMLDivElement,
ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
@@ -72,9 +72,9 @@ const SelectScrollDownButton = React.forwardRef<
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
const SelectContent = forwardRef<
HTMLDivElement,
ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
@@ -104,10 +104,10 @@ const SelectContent = React.forwardRef<
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
const SelectLabel = forwardRef<
HTMLDivElement,
ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref: ForwardedRef<HTMLDivElement>) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
@@ -116,9 +116,9 @@ const SelectLabel = React.forwardRef<
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
const SelectItem = forwardRef<
HTMLDivElement,
ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
@@ -138,9 +138,9 @@ const SelectItem = React.forwardRef<
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
const SelectSeparator = forwardRef<
HTMLDivElement,
ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
+14 -2
View File
@@ -20,7 +20,7 @@
/* Chart Provider Colors */
--chart-provider-aws: #ff9900;
--chart-provider-azure: #00bcd4;
--chart-provider-google: #EA4335;
--chart-provider-google: #ea4335;
/* Chart UI Colors - Dark Theme (defaults) */
--chart-text-primary: #ffffff;
@@ -102,6 +102,19 @@
}
@layer utilities {
/* Screen reader only - visually hidden but accessible */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* Hide scrollbar */
.no-scrollbar {
scrollbar-width: none; /* Firefox */
@@ -125,7 +138,6 @@
transform-box: fill-box;
transform-origin: center;
}
}
@layer base {