mirror of
https://github.com/moghtech/komodo.git
synced 2026-09-08 16:01:18 +00:00
manage api keys
This commit is contained in:
@@ -20,7 +20,7 @@ import {
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
} from "@ui/dialog";
|
||||
import { toast } from "@ui/use-toast";
|
||||
import { toast, useToast } from "@ui/use-toast";
|
||||
import { RESOURCE_TARGETS, cn } from "@lib/utils";
|
||||
import {
|
||||
useInvalidate,
|
||||
@@ -434,3 +434,34 @@ export const UserDropdown = () => {
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export const CopyButton = ({ content }: { content: string | undefined }) => {
|
||||
const { toast } = useToast();
|
||||
const [copied, set] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (copied) {
|
||||
toast({ title: `Copied "${content}"` });
|
||||
const timeout = setTimeout(() => set(false), 3000);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
}, [content, copied, toast]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="shrink-0"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (!content) return;
|
||||
navigator.clipboard.writeText(content);
|
||||
set(true);
|
||||
}}
|
||||
disabled={!content}
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
+162
-33
@@ -1,19 +1,32 @@
|
||||
import { Page, Section } from "@components/layouts";
|
||||
import { ConfirmButton } from "@components/util";
|
||||
import { Page } from "@components/layouts";
|
||||
import { ConfirmButton, CopyButton } from "@components/util";
|
||||
import { useInvalidate, useRead, useWrite } from "@lib/hooks";
|
||||
import { fmt_date } from "@lib/utils";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@ui/card";
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@ui/dialog";
|
||||
import { Button } from "@ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@ui/card";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { Key, Trash } from "lucide-react";
|
||||
import { Trash, PlusCircle, Loader2, Check } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Input } from "@ui/input";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@ui/dropdown-menu";
|
||||
|
||||
export const Keys = () => {
|
||||
return (
|
||||
<Page title="">
|
||||
<Page title="Api Keys" actions={<CreateKey />}>
|
||||
<ApiKeysList />
|
||||
</Page>
|
||||
);
|
||||
@@ -22,29 +35,145 @@ export const Keys = () => {
|
||||
export const ApiKeysList = () => {
|
||||
const keys = useRead("ListApiKeys", {}).data;
|
||||
return (
|
||||
<Section title="Api Keys" icon={<Key className="w-4 h-4" />}>
|
||||
<div className="flex flex-col lg:flex-row gap-4 w-full">
|
||||
{keys?.map((key) => (
|
||||
<Card
|
||||
id={key.key}
|
||||
className="h-full hover:bg-accent/50 group-focus:bg-accent/50 transition-colors"
|
||||
>
|
||||
<CardHeader className="flex-row justify-between items-center">
|
||||
<CardTitle>{key.name}</CardTitle>
|
||||
<DeleteKey api_key={key.key} />
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
<div>created at: {fmt_date(new Date(key.created_at))}</div>
|
||||
<div>
|
||||
expires:{" "}
|
||||
{key.expires === 0 ? "never" : fmt_date(new Date(key.expires))}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{keys?.map((key) => (
|
||||
<Card
|
||||
id={key.key}
|
||||
className="h-full hover:bg-accent/50 group-focus:bg-accent/50 transition-colors"
|
||||
>
|
||||
<CardHeader className="flex-row justify-between items-center">
|
||||
<CardTitle>{key.name}</CardTitle>
|
||||
<DeleteKey api_key={key.key} />
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
<div>created at: {fmt_date(new Date(key.created_at))}</div>
|
||||
<div>
|
||||
expires:{" "}
|
||||
{key.expires === 0 ? "never" : fmt_date(new Date(key.expires))}
|
||||
</div>
|
||||
<div>{key.key}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ONE_DAY_MS = 1000 * 60 * 60 * 24;
|
||||
|
||||
type ExpiresOptions = "90 days" | "180 days" | "1 year" | "never";
|
||||
|
||||
const CreateKey = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [expires, setExpires] = useState<ExpiresOptions>("never");
|
||||
const [submitted, setSubmitted] = useState<{ key: string; secret: string }>();
|
||||
const invalidate = useInvalidate();
|
||||
const { mutate, isPending } = useWrite("CreateApiKey", {
|
||||
onSuccess: ({ key, secret }) => {
|
||||
invalidate(["ListApiKeys"]);
|
||||
setSubmitted({ key, secret });
|
||||
},
|
||||
});
|
||||
const now = Date.now();
|
||||
const expiresOptions: Record<ExpiresOptions, number> = {
|
||||
"90 days": now + ONE_DAY_MS * 90,
|
||||
"180 days": now + ONE_DAY_MS * 180,
|
||||
"1 year": now + ONE_DAY_MS * 365,
|
||||
never: 0,
|
||||
};
|
||||
const submit = () => mutate({ name, expires: expiresOptions[expires] });
|
||||
const onOpenChange = (open: boolean) => {
|
||||
setOpen(open);
|
||||
if (!open) {
|
||||
setName("");
|
||||
setExpires("never");
|
||||
setSubmitted(undefined);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="items-center gap-2">
|
||||
New Api Key <PlusCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
{submitted ? (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Api Key Created</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-8 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
Key
|
||||
<Input className="w-72" value={submitted.key} disabled />
|
||||
<CopyButton content={submitted.key} />
|
||||
</div>
|
||||
<div>{key.key}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
<div className="flex items-center justify-between">
|
||||
Secret
|
||||
<Input className="w-72" value={submitted.secret} disabled />
|
||||
<CopyButton content={submitted.secret} />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="flex justify-end">
|
||||
<Button className="gap-4" onClick={() => onOpenChange(false)}>
|
||||
Confirm <Check className="w-4" />
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Api Key</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-8 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
Name
|
||||
<Input
|
||||
className="w-72"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
Expiry
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="w-36 justify-between px-3">
|
||||
{expires}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-36" side="bottom">
|
||||
<DropdownMenuGroup>
|
||||
{Object.keys(expiresOptions)
|
||||
.filter((option) => option !== expires)
|
||||
.map((option) => (
|
||||
<DropdownMenuItem
|
||||
onClick={() => setExpires(option as any)}
|
||||
>
|
||||
{option}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="flex justify-end">
|
||||
<Button className="gap-4" onClick={submit} disabled={isPending}>
|
||||
Submit
|
||||
{isPending ? (
|
||||
<Loader2 className="w-4 animate-spin" />
|
||||
) : (
|
||||
<Check className="w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -57,8 +186,8 @@ const DeleteKey = ({ api_key }: { api_key: string }) => {
|
||||
toast({ title: "Api Key Deleted" });
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: "Failed to delte api key", })
|
||||
}
|
||||
toast({ title: "Failed to delte api key" });
|
||||
},
|
||||
});
|
||||
return (
|
||||
<ConfirmButton
|
||||
|
||||
Reference in New Issue
Block a user