Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
9e83c8d
feat: add initial overview
ogzhanolguncu Feb 11, 2025
e1fa1d8
fix: run formatter
ogzhanolguncu Feb 11, 2025
5c01407
refactor: Add card back
ogzhanolguncu Feb 11, 2025
8323bec
feat: add first draft of barchart
ogzhanolguncu Feb 12, 2025
3ffdd3b
feat: add latency line chart
ogzhanolguncu Feb 12, 2025
c14754a
refactor: update timestamp colors
ogzhanolguncu Feb 12, 2025
e4372a8
feat: add initial draft of filters for overvioew
ogzhanolguncu Feb 12, 2025
8b6ce2f
feat: add overview filters
ogzhanolguncu Feb 12, 2025
2a33711
fix: round to nearest for latencies
ogzhanolguncu Feb 12, 2025
81f482c
feat: add data fetching for overviews
ogzhanolguncu Feb 12, 2025
0b152ba
fix: add chart states
ogzhanolguncu Feb 13, 2025
4eb831f
fix: add mvs for timeseries latency
ogzhanolguncu Feb 13, 2025
a5b07e7
fix: chart colors
ogzhanolguncu Feb 14, 2025
2eea8c7
refactor: organize files
ogzhanolguncu Feb 14, 2025
8d5aaa0
feat: add new optinos to action menu
ogzhanolguncu Feb 14, 2025
fcab8df
feat: add override modal
ogzhanolguncu Feb 14, 2025
5209c9e
feat: add new override fields
ogzhanolguncu Feb 14, 2025
d2c12ef
feat: extend logs data
ogzhanolguncu Feb 14, 2025
2f6decd
fix: styles
ogzhanolguncu Feb 14, 2025
f479630
fix: small ui issues
ogzhanolguncu Feb 17, 2025
580b859
feat: add status to filters for log
ogzhanolguncu Feb 17, 2025
c025068
fix: bugs
ogzhanolguncu Feb 17, 2025
2e3e67f
feat: add new sebmenu
ogzhanolguncu Feb 17, 2025
b96cec1
feat: add delete and update for quick navbar
ogzhanolguncu Feb 18, 2025
52a57fc
feat: add new quickbar to every ratelimit page
ogzhanolguncu Feb 18, 2025
10efc04
feat: add charts for ratelimit list
ogzhanolguncu Feb 18, 2025
f0ae734
feat: add missing search to ratelimit
ogzhanolguncu Feb 18, 2025
499d978
fix: ui issues
ogzhanolguncu Feb 19, 2025
54ba243
fix: ordering issues
ogzhanolguncu Feb 19, 2025
b2f7427
feat: add more granular timeseries for logs
ogzhanolguncu Feb 19, 2025
68c0d14
feat: add more granular options for charts
ogzhanolguncu Feb 19, 2025
c77360a
feat: add new search for identifiers and paths
ogzhanolguncu Feb 19, 2025
bc2ccaa
chore: cleanup
ogzhanolguncu Feb 19, 2025
954324f
fix: typo
ogzhanolguncu Feb 20, 2025
0d074c6
feat: add sorting
ogzhanolguncu Feb 20, 2025
1a78349
chore: remove mvs
ogzhanolguncu Feb 20, 2025
4272fb7
chore: formatter
ogzhanolguncu Feb 20, 2025
65f07de
feat: replace icons and remove latency
ogzhanolguncu Feb 20, 2025
8c408d6
fix: icon
ogzhanolguncu Feb 20, 2025
ff96dcb
Merge branch 'main' of github.com:unkeyed/unkey into ratelimit-overvi…
ogzhanolguncu Feb 20, 2025
95c6c15
fix: conflict issue
ogzhanolguncu Feb 20, 2025
c38d27b
fix: coderabit issues
ogzhanolguncu Feb 20, 2025
6e1bddc
fix: review comments
ogzhanolguncu Feb 20, 2025
99ecae9
Merge branch 'main' of github.com:unkeyed/unkey into ratelimit-overvi…
ogzhanolguncu Feb 20, 2025
b893b39
chore: remove unused icon
ogzhanolguncu Feb 20, 2025
f20639c
fix: load issue
ogzhanolguncu Feb 20, 2025
341c340
fix: missing error check
ogzhanolguncu Feb 20, 2025
8d47a24
fix: build issue
ogzhanolguncu Feb 21, 2025
825aecf
Merge branch 'main' of github.com:unkeyed/unkey into ratelimit-overvi…
ogzhanolguncu Feb 21, 2025
4316e95
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 21, 2025
5ca0947
fix: missing action for logs action menu item
ogzhanolguncu Feb 21, 2025
6259a45
Merge branch 'ratelimit-overview-v2' of github.com:unkeyed/unkey into…
ogzhanolguncu Feb 21, 2025
ad19525
fix: ch data fetching
ogzhanolguncu Feb 21, 2025
4ae4323
feat: add new table structure and refactor dialogs
ogzhanolguncu Feb 21, 2025
04a9624
fix: use different keys
ogzhanolguncu Feb 21, 2025
0138ed9
fix: chart selection issue
ogzhanolguncu Feb 21, 2025
7707e6c
Merge branch 'main' of github.com:unkeyed/unkey into ratelimit-overri…
ogzhanolguncu Feb 21, 2025
399a2e8
fix: typo
ogzhanolguncu Feb 21, 2025
9db985b
chore: remove unuseds
ogzhanolguncu Feb 21, 2025
6ba9a35
fix: modal openning issue
ogzhanolguncu Feb 21, 2025
a70746a
fix: update override bug
ogzhanolguncu Feb 21, 2025
06970cc
fix: correctly handle timezones
ogzhanolguncu Feb 21, 2025
1c921a1
fix: timestamp issue
ogzhanolguncu Feb 21, 2025
34e52ea
fix: use same type of error for charts
ogzhanolguncu Feb 21, 2025
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
5 changes: 2 additions & 3 deletions apps/dashboard/app/(app)/logs/components/charts/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";
import { LogsTimeseriesBarChart } from "@/components/logs/chart";
import { convertDateToLocal } from "@/components/logs/chart/utils/convert-date-to-local";
import { useFilters } from "../../hooks/use-filters";
import { useFetchTimeseries } from "./hooks/use-fetch-timeseries";

Expand All @@ -27,13 +26,13 @@ export function LogsChart({
...activeFilters,
{
field: "startTime",
value: convertDateToLocal(start),
value: start,
id: crypto.randomUUID(),
operator: "is",
},
{
field: "endTime",
value: convertDateToLocal(end),
value: end,
id: crypto.randomUUID(),
operator: "is",
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"use client";

import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { toast } from "@/components/ui/toaster";
import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@unkey/ui";
import type { PropsWithChildren } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";

const formSchema = z.object({
identifier: z
.string()
// biome-ignore lint/suspicious/noSelfCompare: <explanation>
.refine((v) => v === v, "Please confirm the identifier"),
});

type FormValues = z.infer<typeof formSchema>;

type Props = PropsWithChildren<{
isModalOpen: boolean;
onOpenChange: (value: boolean) => void;
overrideId: string;
identifier: string;
}>;

export const DeleteDialog = ({ isModalOpen, onOpenChange, overrideId, identifier }: Props) => {
const { ratelimit } = trpc.useUtils();

const {
register,
handleSubmit,
watch,
formState: { isSubmitting },
} = useForm<FormValues>({
mode: "onChange",
resolver: zodResolver(formSchema),
defaultValues: {
identifier: "",
},
});

const isValid = watch("identifier") === identifier;

const deleteOverride = trpc.ratelimit.override.delete.useMutation({
onSuccess() {
toast.success("Override has been deleted", {
description: "Changes may take up to 60s to propagate globally",
});
onOpenChange(false);
ratelimit.overview.logs.query.invalidate();
ratelimit.logs.queryRatelimitTimeseries.invalidate();
},
onError(err) {
toast.error("Failed to delete override", {
description: err.message,
});
},
});

const onSubmit = async () => {
try {
await deleteOverride.mutateAsync({ id: overrideId });
} catch (error) {
console.error("Delete error:", error);
}
};

return (
<Dialog open={isModalOpen} onOpenChange={onOpenChange}>
<DialogContent
className="bg-gray-1 dark:bg-black drop-shadow-2xl border-gray-4 rounded-lg p-0 gap-0"
onOpenAutoFocus={(e) => {
e.preventDefault();
}}
>
<DialogHeader className="border-b border-gray-4">
<DialogTitle className="px-6 py-4 text-gray-12 font-medium text-base">
Delete Override
</DialogTitle>
</DialogHeader>

<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col gap-4 py-4 px-6 bg-accent-2">
<p className="text-gray-11 text-[13px]">
<span className="font-medium">Warning: </span>
Are you sure you want to delete this override? The identifier associated with this
override will now use the default limits.
</p>

<div className="space-y-1">
<p className="text-gray-11 text-[13px]">
Type <span className="text-gray-12 font-medium">{identifier}</span> to confirm
</p>

<Input
{...register("identifier")}
placeholder={`Enter "${identifier}" to confirm`}
className="border border-gray-5 focus:border focus:border-gray-4 px-3 py-1 hover:bg-gray-4 hover:border-gray-8 focus:bg-gray-4 rounded-md placeholder:text-gray-8 h-9"
/>
</div>
</div>

<DialogFooter className="p-6 border-t border-gray-4">
<div className="w-full flex flex-col gap-2 items-center justify-center">
<Button
type="submit"
variant="destructive"
disabled={!isValid || deleteOverride.isLoading || isSubmitting}
loading={deleteOverride.isLoading || isSubmitting}
className="h-10 w-full rounded-lg"
>
Delete Override
</Button>
<div className="text-gray-9 text-xs">
This action cannot be undone – proceed with caution
</div>
</div>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Label } from "@/components/ui/label";
import { CircleInfo } from "@unkey/icons";
import type { ReactNode } from "react";
import { InputTooltip } from "../_overview/components/table/components/logs-actions/components/input-tooltip";
import { InputTooltip } from "./input-tooltip";

type FormFieldProps = {
label: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { Button } from "@unkey/ui";
import type { PropsWithChildren, ReactNode } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
import type { OverrideDetails } from "../../../logs-table";
import type { OverrideDetails } from "../types";
import { InputTooltip } from "./input-tooltip";

const overrideValidationSchema = z.object({
Expand Down Expand Up @@ -183,7 +183,7 @@ export const IdentifierDialog = ({
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit(onSubmitForm)}>
<div className="flex flex-col gap-4 py-4 px-6 bg-accent-2">
<div className="flex flex-col gap-4 p-5 pt-4 bg-accent-2">
<FormField
label="Identifier"
tooltip="The identifier you use when ratelimiting."
Expand Down Expand Up @@ -252,7 +252,7 @@ export const IdentifierDialog = ({
</FormField>
</div>

<DialogFooter className="px-6 py-4 border-t border-gray-4">
<DialogFooter className="p-6 border-t border-gray-4">
<div className="w-full flex flex-col gap-2 items-center justify-center">
<Button
type="submit"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { useRouter } from "next/navigation";
import type { PropsWithChildren, ReactNode } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { InputTooltip } from "../_overview/components/table/components/logs-actions/components/input-tooltip";
import { InputTooltip } from "./input-tooltip";

const formSchema = z.object({
name: validation.name,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Dots } from "@unkey/icons";
import { cn } from "@unkey/ui/src/lib/utils";

export const TableActionButton = () => {
return (
<button
type="button"
className={cn(
"group-data-[state=open]:bg-gray-6 bg-gray-5 hover:bg-gray-6 group size-5 p-0 rounded m-0 items-center flex justify-center",
"border border-gray-6 hover:border-gray-8 ring-2 ring-transparent focus-visible:ring-gray-7 focus-visible:border-gray-7",
)}
>
<Dots className="group-hover:text-gray-12 text-gray-11" size="sm-regular" />
</button>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@unkey/ui/src/lib/utils";
import { type PropsWithChildren, useEffect, useRef, useState } from "react";
import { TableActionButton } from "./table-action-button";

export type MenuItem = {
id: string;
label: string;
icon: React.ReactNode;
onClick: (e: React.MouseEvent<Element, MouseEvent> | React.KeyboardEvent<Element>) => void;
className?: string;
disabled?: boolean;
};

type BaseTableActionPopoverProps = PropsWithChildren<{
items: MenuItem[];
align?: "start" | "end";
headerContent?: React.ReactNode;
}>;

export const TableActionPopover = ({
items,
align = "end",
headerContent,
children,
}: BaseTableActionPopoverProps) => {
const [open, setOpen] = useState(false);
const [focusIndex, setFocusIndex] = useState(0);
const menuItems = useRef<HTMLDivElement[]>([]);

useEffect(() => {
if (open) {
const firstEnabledIndex = items.findIndex((item) => !item.disabled);
setFocusIndex(firstEnabledIndex >= 0 ? firstEnabledIndex : 0);
if (firstEnabledIndex >= 0) {
menuItems.current[firstEnabledIndex]?.focus();
}
}
}, [open, items]);

const handleKeyDown = (e: React.KeyboardEvent<Element>) => {
e.stopPropagation();

const activeElement = document.activeElement;
const currentIndex = menuItems.current.findIndex((item) => item === activeElement);
const itemCount = items.length;

const findNextEnabledIndex = (startIndex: number, direction: 1 | -1) => {
let index = startIndex;
for (let i = 0; i < itemCount; i++) {
index = (index + direction + itemCount) % itemCount;
if (!items[index].disabled) {
return index;
}
}
return startIndex;
};

switch (e.key) {
case "Tab": {
e.preventDefault();
const nextIndex = findNextEnabledIndex(currentIndex, e.shiftKey ? -1 : 1);
setFocusIndex(nextIndex);
menuItems.current[nextIndex]?.focus();
break;
}

case "j":
case "ArrowDown": {
e.preventDefault();
const nextDownIndex = findNextEnabledIndex(currentIndex, 1);
setFocusIndex(nextDownIndex);
menuItems.current[nextDownIndex]?.focus();
break;
}

case "k":
case "ArrowUp": {
e.preventDefault();
const nextUpIndex = findNextEnabledIndex(currentIndex, -1);
setFocusIndex(nextUpIndex);
menuItems.current[nextUpIndex]?.focus();
break;
}

case "Escape":
e.preventDefault();
setOpen(false);
break;

case "Enter":
case "ArrowRight":
case "l":
case " ":
e.preventDefault();
if (activeElement === menuItems.current[currentIndex] && !items[currentIndex].disabled) {
items[currentIndex].onClick(e);
}
break;
}
};

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger onClick={(e) => e.stopPropagation()}>
{children ? children : <TableActionButton />}
</PopoverTrigger>

<PopoverContent
className="w-60 bg-gray-1 dark:bg-black drop-shadow-2xl p-2 border-gray-6 rounded-lg"
align={align}
onOpenAutoFocus={(e) => {
e.preventDefault();
const firstEnabledIndex = items.findIndex((item) => !item.disabled);
if (firstEnabledIndex >= 0) {
menuItems.current[firstEnabledIndex]?.focus();
}
}}
onCloseAutoFocus={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => {
e.preventDefault();
setOpen(false);
}}
onInteractOutside={(e) => {
e.preventDefault();
setOpen(false);
}}
>
<div
className="flex flex-col gap-2"
role="menu"
onClick={(e) => e.stopPropagation()}
onKeyDown={handleKeyDown}
>
{headerContent ?? <PopoverHeader />}

{items.map((item, index) => (
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
<div
key={item.id}
ref={(el) => {
if (el) {
menuItems.current[index] = el;
}
}}
role="menuitem"
aria-disabled={item.disabled}
tabIndex={!item.disabled && focusIndex === index ? 0 : -1}
className={cn(
"flex w-full items-center px-2 py-1.5 gap-3 rounded-lg group",
!item.disabled &&
"cursor-pointer hover:bg-gray-3 data-[state=open]:bg-gray-3 focus:outline-none focus:bg-gray-3",
item.disabled && "cursor-not-allowed opacity-50",
item.className,
)}
onClick={(e) => {
if (!item.disabled) {
item.onClick(e);
setOpen(false);
}
}}
>
{item.icon}
<span className="text-[13px] font-medium">{item.label}</span>
</div>
))}
</div>
</PopoverContent>
</Popover>
);
};

const PopoverHeader = () => {
return (
<div className="flex w-full justify-between items-center px-2 py-1">
<span className="text-gray-9 text-[13px]">Actions...</span>
</div>
);
};
Loading
Loading