Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new filter logic #2804

Merged
merged 9 commits into from
Jan 13, 2025
104 changes: 104 additions & 0 deletions apps/dashboard/app/(app)/logs-v2/components/control-cloud/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { KeyboardButton } from "@/components/keyboard-button";
import { cn } from "@/lib/utils";
import { XMark } from "@unkey/icons";
import { Button } from "@unkey/ui";
import { useCallback } from "react";
import { useKeyboardShortcut } from "../../hooks/use-keyboard-shortcut";
import { type FilterValue, useFilters } from "../../query-state";

const formatFieldName = (field: string): string => {
switch (field) {
case "status":
return "Status";
case "paths":
return "Path";
case "methods":
return "Method";
case "requestId":
return "Request ID";
default:
// Capitalize first letter
return field.charAt(0).toUpperCase() + field.slice(1);
}
};

const formatValue = (value: string | number): string => {
if (typeof value === "string" && /^\d+$/.test(value)) {
const statusFamily = Math.floor(Number.parseInt(value) / 100);
switch (statusFamily) {
case 5:
return "5XX (Error)";
case 4:
return "4XX (Warning)";
case 2:
return "2XX (Success)";
default:
return `${statusFamily}xx`;
}
}
return String(value);
};

type ControlPillProps = {
filter: FilterValue;
onRemove: (id: string) => void;
};

const ControlPill = ({ filter, onRemove }: ControlPillProps) => {
const { field, operator, value, metadata } = filter;

return (
<div className="flex gap-0.5 font-mono">
<div className="bg-gray-3 px-2 rounded-l-md text-accent-12 font-medium py-[2px]">
{formatFieldName(field)}
</div>
<div className="bg-gray-3 px-2 text-accent-12 font-medium py-[2px] flex gap-1 items-center">
{operator}
</div>
<div className="bg-gray-3 px-2 text-accent-12 font-medium py-[2px] flex gap-1 items-center">
{metadata?.colorClass && (
<div className={cn("size-2 rounded-[2px]", metadata.colorClass)} />
)}
{metadata?.icon}
<span className="text-accent-12 text-xs font-mono">{formatValue(value)}</span>
</div>
<Button
onClick={() => onRemove(filter.id)}
className="bg-gray-3 rounded-none rounded-r-md py-[2px] px-2 [&_svg]:stroke-[2px] [&_svg]:size-3 flex items-center border-none h-auto"
>
<XMark className="text-gray-9" />
</Button>
</div>
);
};

export const ControlCloud = () => {
const { filters, removeFilter, updateFilters } = useFilters();

useKeyboardShortcut({ key: "d", meta: true }, () => {
updateFilters([]);
});

const handleRemoveFilter = useCallback(
(id: string) => {
removeFilter(id);
},
[removeFilter],
);

if (filters.length === 0) {
return null;
}

return (
<div className="px-3 py-2 w-full flex items-center min-h-10 border-b border-gray-4 gap-2 text-xs flex-wrap">
{filters.map((filter) => (
<ControlPill key={filter.id} filter={filter} onRemove={handleRemoveFilter} />
))}
<div className="flex items-center px-2 py-1 gap-1 ml-auto">
<span className="text-gray-9 text-[13px]">Clear filters</span>
<KeyboardButton shortcut="d" modifierKey="⌘" />
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useKeyboardShortcut } from "@/app/(app)/logs-v2/hooks/use-keyboard-shortcut";
import { useFilters } from "@/app/(app)/logs-v2/query-state";
import { KeyboardButton } from "@/components/keyboard-button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { CaretRight } from "@unkey/icons";
Expand All @@ -17,19 +18,19 @@ type FilterItemConfig = {

const FILTER_ITEMS: FilterItemConfig[] = [
{
id: "status",
id: "responseStatus",
label: "Status",
shortcut: "s",
component: <StatusFilter />,
},
{
id: "method",
id: "methods",
label: "Method",
shortcut: "m",
component: <MethodsFilter />,
},
{
id: "path",
id: "paths",
label: "Path",
shortcut: "p",
component: <PathsFilter />,
Expand Down Expand Up @@ -70,7 +71,8 @@ const PopoverHeader = () => {
);
};

export const FilterItem = ({ label, shortcut, component }: FilterItemConfig) => {
export const FilterItem = ({ label, shortcut, id, component }: FilterItemConfig) => {
const { filters } = useFilters();
const [open, setOpen] = useState(false);

// Add keyboard shortcut for each filter item when main filter is open
Expand All @@ -85,7 +87,7 @@ export const FilterItem = ({ label, shortcut, component }: FilterItemConfig) =>
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<div className="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">
<div className="flex w-full items-center px-2 py-1.5 justify-between rounded-lg group cursor-pointer group hover:bg-gray-3 data-[state=open]:bg-gray-3">
<div className="flex gap-2 items-center">
{shortcut && (
<KeyboardButton
Expand All @@ -99,6 +101,12 @@ export const FilterItem = ({ label, shortcut, component }: FilterItemConfig) =>
<span className="text-[13px] text-accent-12 font-medium">{label}</span>
</div>
<div className="flex items-center gap-1.5">
{filters.filter((filter) => filter.field === id).length > 0 && (
<div className="bg-gray-6 rounded size-4 text-[11px] font-medium text-accent-12 text-center flex items-center justify-center">
{filters.filter((filter) => filter.field === id).length}
</div>
)}
ogzhanolguncu marked this conversation as resolved.
Show resolved Hide resolved

<Button variant="ghost" size="icon" tabIndex={-1} className="size-5 [&_svg]:size-2">
<CaretRight className="text-gray-7 group-hover:text-gray-10" />
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,40 @@
import { type FilterValue, type HttpMethod, useFilters } from "@/app/(app)/logs-v2/query-state";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@unkey/ui";
import { useState } from "react";
import { useCallback, useEffect, useState } from "react";

interface CheckboxOption {
id: number;
method: string;
method: HttpMethod;
checked: boolean;
}

const options: CheckboxOption[] = [
{
id: 1,
method: "GET",
checked: false,
},
{
id: 2,
method: "POST",
checked: false,
},
{
id: 3,
method: "PUT",
checked: false,
},
{
id: 4,
method: "DELETE",
checked: false,
},
{
id: 5,
method: "PATCH",
checked: false,
},
{ id: 1, method: "GET", checked: false },
{ id: 2, method: "POST", checked: false },
{ id: 3, method: "PUT", checked: false },
{ id: 4, method: "DELETE", checked: false },
{ id: 5, method: "PATCH", checked: false },
] as const;

export const MethodsFilter = () => {
const { filters, updateFilters } = useFilters();
const [checkboxes, setCheckboxes] = useState<CheckboxOption[]>(options);

// Sync checkboxes with filters on mount and when filters change
useEffect(() => {
const methodFilters = filters
.filter((f) => f.field === "methods")
.map((f) => f.value as HttpMethod);

setCheckboxes((prev) =>
prev.map((checkbox) => ({
...checkbox,
checked: methodFilters.includes(checkbox.method),
})),
);
}, [filters]);

const handleCheckboxChange = (index: number): void => {
setCheckboxes((prevCheckboxes) => {
const newCheckboxes = [...prevCheckboxes];
Expand All @@ -60,17 +56,36 @@ export const MethodsFilter = () => {
});
};

const handleApplyFilter = useCallback(() => {
const selectedMethods = checkboxes.filter((c) => c.checked).map((c) => c.method);

// Keep all non-method filters and add new method filters
const otherFilters = filters.filter((f) => f.field !== "methods");
const methodFilters: FilterValue[] = selectedMethods.map((method) => ({
id: crypto.randomUUID(),
field: "methods",
operator: "is",
value: method,
}));

updateFilters([...otherFilters, ...methodFilters]);
}, [checkboxes, filters, updateFilters]);

return (
<div className="flex flex-col p-2">
<div className="flex flex-col gap-2 font-mono px-2 py-2">
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={checkboxes.every((checkbox) => checkbox.checked)}
className="size-[14px] rounded border-gray-4 [&_svg]:size-3"
onClick={handleSelectAll}
/>
<span className="text-xs text-accent-12 ml-2">Select All</span>
</label>
<div className="flex justify-between items-center">
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={checkboxes.every((checkbox) => checkbox.checked)}
className="size-[14px] rounded border-gray-4 [&_svg]:size-3"
onClick={handleSelectAll}
/>
<span className="text-xs text-accent-12 ml-2">
{checkboxes.every((checkbox) => checkbox.checked) ? "Unselect All" : "Select All"}
</span>
</label>
</div>
ogzhanolguncu marked this conversation as resolved.
Show resolved Hide resolved
{checkboxes.map((checkbox, index) => (
<label key={checkbox.id} className="flex gap-4 items-center py-1 cursor-pointer">
<Checkbox
Expand All @@ -85,10 +100,7 @@ export const MethodsFilter = () => {
<Button
variant="primary"
className="font-sans mt-2 w-full h-9 rounded-md"
onClick={() => {
const selectedMethods = checkboxes.filter((c) => c.checked);
console.info("Selected Methods:", selectedMethods);
}}
onClick={handleApplyFilter}
>
Apply Filter
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { type FilterValue, useFilters } from "@/app/(app)/logs-v2/query-state";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@unkey/ui";
import { useCallback, useEffect, useRef, useState } from "react";
Expand Down Expand Up @@ -87,10 +88,23 @@ const options: CheckboxOption[] = [
] as const;

export const PathsFilter = () => {
const { filters, updateFilters } = useFilters();
const [checkboxes, setCheckboxes] = useState<CheckboxOption[]>(options);
const [isAtBottom, setIsAtBottom] = useState(false);
const scrollContainerRef = useRef<HTMLDivElement>(null);

// Sync checkboxes with filters on mount and when filters change
useEffect(() => {
const pathFilters = filters.filter((f) => f.field === "paths").map((f) => f.value as string);

setCheckboxes((prev) =>
prev.map((checkbox) => ({
...checkbox,
checked: pathFilters.includes(checkbox.path),
})),
);
}, [filters]);

const handleScroll = useCallback(() => {
if (scrollContainerRef.current) {
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
Expand All @@ -103,9 +117,7 @@ export const PathsFilter = () => {
const scrollContainer = scrollContainerRef.current;
if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll);
// Check initial scroll position
handleScroll();

return () => {
scrollContainer.removeEventListener("scroll", handleScroll);
};
Expand Down Expand Up @@ -133,6 +145,21 @@ export const PathsFilter = () => {
});
};

const handleApplyFilter = useCallback(() => {
const selectedPaths = checkboxes.filter((c) => c.checked).map((c) => c.path);

// Keep all non-paths filters and add new path filters
const otherFilters = filters.filter((f) => f.field !== "paths");
const pathFilters: FilterValue[] = selectedPaths.map((path) => ({
id: crypto.randomUUID(),
field: "paths",
operator: "is",
value: path,
}));

updateFilters([...otherFilters, ...pathFilters]);
}, [checkboxes, filters, updateFilters]);

return (
<div className="flex flex-col font-mono">
<label className="flex items-center gap-2 px-4 pb-2 pt-4 cursor-pointer">
Expand All @@ -141,7 +168,9 @@ export const PathsFilter = () => {
className="size-[14px] rounded border-gray-4 [&_svg]:size-3"
onClick={handleSelectAll}
/>
<span className="text-xs text-accent-12 ml-2">Select All</span>
<span className="text-xs text-accent-12 ml-2">
{checkboxes.every((checkbox) => checkbox.checked) ? "Unselect All" : "Select All"}
</span>
</label>
<div className="relative px-2">
<div
Expand Down Expand Up @@ -170,10 +199,7 @@ export const PathsFilter = () => {
<Button
variant="primary"
className="font-sans w-full h-9 rounded-md"
onClick={() => {
const selectedPaths = checkboxes.filter((c) => c.checked);
console.info("Selected Paths:", selectedPaths);
}}
onClick={handleApplyFilter}
>
Apply Filter
</Button>
Expand Down
Loading
Loading