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
Expand Up @@ -185,14 +185,14 @@ export const KeysOverviewLogsTable = ({ apiId, setSelectedLog, log: selectedLog
hide: true,
}}
emptyState={
<div className="w-full justify-center items-center md:h-full">
<Empty className="w-full max-w-[400px] flex mx-auto items-center md:items-start">
<div className="w-full flex justify-center items-center h-full">
<Empty className="w-[400px] flex items-start">
<Empty.Icon className="w-auto" />
<Empty.Title>Key Verification Logs</Empty.Title>
<Empty.Description className="text-center md:text-left">
<Empty.Description className="text-left">
No key verification data to show. Once requests are made with API keys, you'll see a
summary of successful and failed verification attempts.
</Empty.Description>
</Empty.Description>{" "}
<Empty.Actions className="mt-4 justify-center md:justify-start">
<a
href="https://www.unkey.com/docs/introduction"
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/app/(app)/apis/[apiId]/api-id-navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CreateKeyButton } from "@/components/dashboard/create-key-button";
import { QuickNavPopover } from "@/components/navbar-popover";
import { Navbar } from "@/components/navigation/navbar";
import { Badge } from "@/components/ui/badge";
import { useResponsive } from "@/hooks/use-responsive";
import { useIsMobile } from "@/hooks/use-mobile";
import { ChevronExpandY, Gauge } from "@unkey/icons";

export const ApisNavbar = ({
Expand All @@ -29,7 +29,7 @@ export const ApisNavbar = ({
};
keyId?: string;
}) => {
const { isMobile } = useResponsive();
const isMobile = useIsMobile();
return (
<div className="w-full">
<Navbar className="w-full flex justify-between">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,22 @@ const FILTER_ITEMS: FilterItemConfig[] = [
{
id: "status",
label: "Status",
shortcut: "e",
shortcut: "E",
shortcutLabel: "E",
component: <StatusFilter />,
},
{
id: "methods",
label: "Method",
shortcut: "m",
shortcut: "M",
shortcutLabel: "M",
component: <MethodsFilter />,
},
{
id: "paths",
label: "Path",
shortcut: "p",
shortcut: "P",
shortcutLabel: "P",
component: <PathsFilter />,
},
];
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/components/keyboard-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const KeyboardButton = ({
{...props}
>
{modifierKey && <kbd>{modifierKey}+</kbd>}
<kbd>{shortcut.toUpperCase()}</kbd>
<kbd>{shortcut?.toUpperCase()}</kbd>
</span>
);
};
45 changes: 15 additions & 30 deletions apps/dashboard/components/logs/checkbox/filter-checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,24 +132,6 @@ export const FilterCheckbox = <
[selectionMode, handleCheckboxChange, handleSingleSelection],
);

// Handle keyboard event
const handleKeyboardEvent = useCallback(
(event: React.KeyboardEvent<HTMLLabelElement>, index?: number) => {
if (event.key === " " || event.key === "Enter") {
event.preventDefault();
if (index !== undefined) {
handleCheckboxClick(index);
} else if (selectionMode === "multiple") {
handleSelectAll();
}
}

// Use the handleKeyDown from the hook for other keyboard navigation
handleKeyDown(event, index);
},
[handleCheckboxClick, handleSelectAll, handleKeyDown, selectionMode],
);

// Handle applying the filter
const handleApplyFilter = useCallback(() => {
const selectedCheckboxes = checkboxes.filter((c) => c.checked);
Expand Down Expand Up @@ -211,18 +193,20 @@ export const FilterCheckbox = <
{selectionMode === "multiple" && (
<div className="flex justify-between items-center">
<label
htmlFor="select-all-checkbox"
// "checkbox-999 required to transfer focus from single checkboxes to this "select" all"
htmlFor={"checkbox-999"}
className="flex items-center gap-[18px] cursor-pointer"
// biome-ignore lint/a11y/useSemanticElements lint/a11y/noNoninteractiveElementToInteractiveRole: its okay
role="checkbox"
aria-checked={checkboxes.every((checkbox) => checkbox.checked)}
onKeyDown={handleKeyboardEvent}
tabIndex={0}
onKeyDown={(e) => handleKeyDown(e)}
>
<Checkbox
id={"checkbox-999"}
checked={checkboxes.every((checkbox) => checkbox.checked)}
className="size-4 rounded border-gray-4 [&_svg]:size-3"
onClick={handleSelectAll}
onClick={(e) => {
e.stopPropagation();
handleSelectAll();
}}
/>
<span className="text-xs text-accent-12">
{checkboxes.every((checkbox) => checkbox.checked) ? "Unselect All" : "Select All"}
Expand All @@ -236,16 +220,17 @@ export const FilterCheckbox = <
key={checkbox.id}
htmlFor={`checkbox-${checkbox.id}`}
className="flex gap-[18px] items-center py-1 cursor-pointer"
// biome-ignore lint/a11y/useSemanticElements lint/a11y/noNoninteractiveElementToInteractiveRole: its okay
role="checkbox"
aria-checked={checkbox.checked}
onKeyDown={(e) => handleKeyboardEvent(e, index)}
tabIndex={0}
onKeyDown={(e) => handleKeyDown(e, index)}
>
<Checkbox
id={`checkbox-${checkbox.id}`}
checked={checkbox.checked}
className="size-4 rounded border-gray-4 [&_svg]:size-3"
onClick={() => handleCheckboxClick(index)}
onClick={(e) => {
e.stopPropagation();
handleCheckboxClick(index);
}}
/>
{renderOptionContent ? renderOptionContent(checkbox) : null}
</label>
Expand All @@ -257,7 +242,7 @@ export const FilterCheckbox = <
{renderBottomGradient && <div className="border-t border-gray-4" />}
<Button
variant="primary"
className="font-sans mt-2 w-full h-9 rounded-md"
className="mt-2 w-full h-9 rounded-md focus:ring-4 focus:ring-accent-9 focus:ring-offset-2"
onClick={handleApplyFilter}
>
Apply Filter
Expand Down
171 changes: 171 additions & 0 deletions apps/dashboard/components/logs/checkbox/filter-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { KeyboardButton } from "@/components/keyboard-button";
import { Drover } from "@/components/ui/drover";
import { CaretRight } from "@unkey/icons";
import { Button } from "@unkey/ui";
import { cn } from "@unkey/ui/src/lib/utils";
import type React from "react";
import { type KeyboardEvent, useCallback, useEffect, useRef, useState } from "react";

export type FilterItemConfig = {
id: string;
label: string;
shortcut?: string;
component: React.ReactNode;
};

type FilterItemProps = FilterItemConfig & {
isFocused?: boolean; // Highlighted in the main list?
isActive?: boolean; // Is this item's popover the active one?
filterCount: number;
setActiveFilter: (id: string | null) => void;
};

export const FilterItem = ({
id,
label,
shortcut,
component,
isFocused,
isActive,
filterCount,
setActiveFilter,
}: FilterItemProps) => {
// Internal open state, primarily controlled by 'isActive' prop effect
const [open, setOpen] = useState(isActive ?? false);
const itemRef = useRef<HTMLDivElement>(null); // Ref for the trigger div
const contentRef = useRef<HTMLDivElement>(null); // Ref for the DroverContent

// Synchronize internal open state with the parent's isActive prop
useEffect(() => {
setOpen(isActive ?? false);
}, [isActive]);

// Focus the trigger div when parent indicates it's focused in the main list
// biome-ignore lint/correctness/useExhaustiveDependencies: no need to react for label
useEffect(() => {
if (isFocused && !isActive && itemRef.current) {
// Only focus trigger if not active
itemRef.current.focus({ preventScroll: true });
}
}, [isFocused, isActive, label]); // Depend on isActive too

// Focus content when drover becomes active and open
// biome-ignore lint/correctness/useExhaustiveDependencies: no need to react for label
useEffect(() => {
if (isActive && open && contentRef.current) {
// Find and focus the first focusable element within the content
const focusableElements = contentRef.current.querySelectorAll<HTMLElement>(
'button, [href], input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])',
);
if (focusableElements.length > 0) {
focusableElements[0].focus({ preventScroll: true });
} else {
// Fallback: focus the content container itself if nothing else is focusable
contentRef.current.focus({ preventScroll: true });
}
}
}, [isActive, open, label]); // Depend on isActive and open

const handleItemDroverKeyDown = useCallback(
(e: KeyboardEvent) => {
// No need to check isInputFocused here as parent handles ArrowLeft navigation back
// We only care about Escape to close *this* drover.
if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation(); // Stop Escape from bubbling further (e.g., closing main drover)
// Request parent to deactivate this filter and focus the trigger
setActiveFilter(null);
// Focus should return naturally because parent will set isFocused=true
// based on lastFocusedIndex after setActiveFilter(null) is processed.
}
// Allow other keys (like arrows in inputs) to behave normally
},
[setActiveFilter], // Depend on the callback from parent
);

// Handler for Drover's open state changes (e.g., clicking outside)
const handleOpenChange = useCallback(
(newOpenState: boolean) => {
// This function is called when the drover intends to close
// (e.g., click outside, Escape press handled internally if not stopped)
setOpen(newOpenState); // Keep internal state synced

// If the drover closed AND the parent still thinks it's active,
// we MUST inform the parent to update its state.
if (!newOpenState && isActive) {
setActiveFilter(null);
}
// If it opened via interaction (shouldn't happen if controlled),
// or closed when parent already knew, do nothing extra.
},
[isActive, setActiveFilter],
);

// Handler for clicking the trigger element
const handleTriggerClick = useCallback(() => {
// Toggle activation by telling the parent
setActiveFilter(isActive ? null : id);
}, [isActive, id, setActiveFilter]);

return (
<Drover.Nested open={open} onOpenChange={handleOpenChange}>
<Drover.Trigger asChild>
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
<div
ref={itemRef}
className={cn(
"flex w-full items-center px-2 py-1.5 justify-between rounded-lg group cursor-pointer",
"hover:bg-gray-3 data-[state=open]:bg-gray-3",
"focus:outline-none focus:ring-2 focus:ring-accent-7",
isFocused && !isActive ? "bg-gray-4" : "",
isActive ? "bg-gray-3" : "",
)}
tabIndex={-1}
role="menuitem"
aria-haspopup="true"
aria-expanded={open}
onClick={handleTriggerClick}
>
<div className="flex gap-2 items-center pointer-events-none">
{shortcut && (
<KeyboardButton
shortcut={shortcut}
role="presentation"
aria-haspopup="true"
title={`Press '${shortcut?.toUpperCase()}' to toggle ${label} options`}
/>
)}
<span className="text-[13px] text-accent-12 font-medium select-none">{label}</span>
</div>
<div className="flex items-center gap-1.5 pointer-events-none">
{filterCount > 0 && (
<div className="bg-gray-6 rounded size-4 text-[11px] font-medium text-accent-12 text-center flex items-center justify-center">
{filterCount}
</div>
)}
<Button
variant="ghost"
size="icon"
tabIndex={-1} // Non-interactive button
className="size-5 [&_svg]:size-2"
aria-hidden="true"
>
<CaretRight className="text-gray-7 group-hover:text-gray-10" />
</Button>
</div>
</div>
</Drover.Trigger>
<Drover.Content
ref={contentRef}
className="min-w-60 w-full bg-gray-1 dark:bg-black drop-shadow-2xl p-0 border-gray-6 rounded-lg"
side="right"
align="start"
sideOffset={12}
onKeyDown={handleItemDroverKeyDown}
tabIndex={-1}
>
{component}
</Drover.Content>
</Drover.Nested>
);
};
Loading