forked from github-starred/komodo
api key page
This commit is contained in:
@@ -50,6 +50,7 @@ export const Layout = () => {
|
||||
<WsStatusIndicator />
|
||||
<HeaderUpdates />
|
||||
<ThemeToggle />
|
||||
{/* <UserSettings /> */}
|
||||
<Logout />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,12 +10,9 @@ import { Button } from "@ui/button";
|
||||
import { Calendar, User } from "lucide-react";
|
||||
import { UpdateDetails, UpdateUser } from "./details";
|
||||
import { ResourceComponents } from "@components/resources";
|
||||
import { cn, fmt_version } from "@lib/utils";
|
||||
import { cn, fmt_date, fmt_version } from "@lib/utils";
|
||||
import { Types } from "@monitor/client";
|
||||
|
||||
const fmt_date = (d: Date) =>
|
||||
`${d.getDate()}/${d.getMonth() + 1} @ ${d.getHours()}:${d.getMinutes()}`;
|
||||
|
||||
export const SingleUpdate = ({ update }: { update: Types.UpdateListItem }) => {
|
||||
const Components =
|
||||
update.target.type !== "System"
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { Link } from "react-router-dom";
|
||||
import { Types } from "@monitor/client";
|
||||
import { Section } from "@components/layouts";
|
||||
import { fmt_update_date, fmt_version } from "@lib/utils";
|
||||
import { fmt_date, fmt_version } from "@lib/utils";
|
||||
import { UpdateDetails, UpdateUser } from "./details";
|
||||
import { UpdateStatus } from "@monitor/client/dist/types";
|
||||
|
||||
@@ -57,7 +57,7 @@ const UpdateCard = ({ update }: { update: Types.UpdateListItem }) => {
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Calendar className="w-4" />
|
||||
{fmt_update_date(new Date(update.start_ts))}
|
||||
{fmt_date(new Date(update.start_ts))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<User className="w-4" />
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
Loader2,
|
||||
LogOut,
|
||||
Moon,
|
||||
Settings,
|
||||
SunMedium,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { Input } from "../ui/input";
|
||||
import {
|
||||
@@ -35,7 +37,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@ui/dropdown-menu";
|
||||
import { ResourceComponents } from "./resources";
|
||||
import { WsStatusIndicator } from "@lib/socket";
|
||||
|
||||
export const WithLoading = ({
|
||||
children,
|
||||
@@ -95,6 +96,16 @@ export const ThemeToggle = () => {
|
||||
export const ActionButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
{
|
||||
variant?:
|
||||
| "link"
|
||||
| "default"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "secondary"
|
||||
| "ghost"
|
||||
| null
|
||||
| undefined;
|
||||
size?: "default" | "sm" | "lg" | "icon" | null | undefined;
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
disabled?: boolean;
|
||||
@@ -102,17 +113,23 @@ export const ActionButton = forwardRef<
|
||||
onClick?: () => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
>(({ title, icon, disabled, className, loading, onClick }, ref) => (
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("flex items-center justify-between w-[150px]", className)}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
ref={ref}
|
||||
>
|
||||
{title} {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : icon}
|
||||
</Button>
|
||||
));
|
||||
>(
|
||||
(
|
||||
{ variant, size, title, icon, disabled, className, loading, onClick },
|
||||
ref
|
||||
) => (
|
||||
<Button
|
||||
size={size}
|
||||
variant={variant || "outline"}
|
||||
className={cn("flex items-center justify-between w-[150px]", className)}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
ref={ref}
|
||||
>
|
||||
{title} {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : icon}
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
|
||||
export const ActionWithDialog = ({
|
||||
name,
|
||||
@@ -235,12 +252,24 @@ export const CopyResource = ({
|
||||
};
|
||||
|
||||
export const ConfirmButton = ({
|
||||
variant,
|
||||
size,
|
||||
title,
|
||||
icon,
|
||||
disabled,
|
||||
loading,
|
||||
onClick,
|
||||
}: {
|
||||
variant?:
|
||||
| "link"
|
||||
| "default"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "secondary"
|
||||
| "ghost"
|
||||
| null
|
||||
| undefined;
|
||||
size?: "default" | "sm" | "lg" | "icon" | null | undefined;
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
onClick: () => void;
|
||||
@@ -252,6 +281,8 @@ export const ConfirmButton = ({
|
||||
return (
|
||||
<>
|
||||
<ActionButton
|
||||
variant={variant}
|
||||
size={size}
|
||||
title={confirmed ? "Confirm" : title}
|
||||
icon={confirmed ? <Check className="w-4 h-4" /> : icon}
|
||||
disabled={disabled}
|
||||
@@ -309,6 +340,12 @@ export const ResourceTypeDropdown = () => {
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
<Link to="/keys">
|
||||
<DropdownMenuItem className="flex items-center gap-2">
|
||||
<Box className="w-4 h-4" />
|
||||
Api Keys
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -374,18 +411,26 @@ export const Logout = () => (
|
||||
</Button>
|
||||
);
|
||||
|
||||
export const UserDropdown = () => {
|
||||
const user = useRead("GetUser", {}).data;
|
||||
export const UserSettings = () => (
|
||||
<Link to="/settings">
|
||||
<Button variant="ghost" size="icon">
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
|
||||
export const UserDropdown = () => {
|
||||
// const user = useRead("GetUser", {}).data;
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="gap-2" variant="outline">
|
||||
<WsStatusIndicator />
|
||||
{user?.username}
|
||||
<Button variant="ghost" size="icon">
|
||||
<User className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent></DropdownMenuContent>
|
||||
<DropdownMenuContent>
|
||||
<Logout />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ export const RESOURCE_TARGETS: UsableResource[] = [
|
||||
"Procedure",
|
||||
];
|
||||
|
||||
export const fmt_update_date = (d: Date) => {
|
||||
export const fmt_date = (d: Date) => {
|
||||
return `${d.getDate()}/${d.getMonth() + 1} @ ${d.getHours()}:${d.getMinutes()}`;
|
||||
};
|
||||
|
||||
|
||||
71
frontend/src/pages/keys.tsx
Normal file
71
frontend/src/pages/keys.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Page, Section } from "@components/layouts";
|
||||
import { ConfirmButton } from "@components/util";
|
||||
import { useInvalidate, useRead, useWrite } from "@lib/hooks";
|
||||
import { fmt_date } from "@lib/utils";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@ui/card";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { Key, Trash } from "lucide-react";
|
||||
|
||||
export const Keys = () => {
|
||||
return (
|
||||
<Page title="">
|
||||
<ApiKeysList />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
<div>{key.key}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const DeleteKey = ({ api_key }: { api_key: string }) => {
|
||||
const invalidate = useInvalidate();
|
||||
const { toast } = useToast();
|
||||
const { mutate, isPending } = useWrite("DeleteApiKey", {
|
||||
onSuccess: () => {
|
||||
invalidate(["ListApiKeys"]);
|
||||
toast({ title: "Api Key Deleted" });
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: "Failed to delte api key", })
|
||||
}
|
||||
});
|
||||
return (
|
||||
<ConfirmButton
|
||||
title="Delete"
|
||||
icon={<Trash className="w-4 h-4" />}
|
||||
onClick={() => mutate({ key: api_key })}
|
||||
loading={isPending}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { Dashboard } from "@pages/dashboard";
|
||||
import { Login } from "@pages/login";
|
||||
import { Resource } from "@pages/resource";
|
||||
import { Resources } from "@pages/resources";
|
||||
import { Keys } from "@pages/keys";
|
||||
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
@@ -12,6 +13,7 @@ const router = createBrowserRouter([
|
||||
element: <Layout />,
|
||||
children: [
|
||||
{ path: "", element: <Dashboard /> },
|
||||
{ path: "keys", element: <Keys /> },
|
||||
{
|
||||
path: ":type",
|
||||
children: [
|
||||
|
||||
Reference in New Issue
Block a user