Files
jenna-mcp/src/pages/scr/dashboard/apikey/apikey_page.tsx
bipproduction c3ac52cbe5 tambahan
2025-10-13 17:31:01 +08:00

368 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
Button,
Card,
Container,
Group,
Stack,
Table,
Text,
TextInput,
Title,
ScrollArea,
ActionIcon,
Tooltip,
Divider,
Loader,
Badge,
useMantineTheme,
} from "@mantine/core";
import { useEffect, useState } from "react";
import {
IconPlus,
IconCopy,
IconTrash,
IconKey,
IconDatabase,
IconLock,
} from "@tabler/icons-react";
import { showNotification } from "@mantine/notifications";
import apiFetch from "@/lib/apiFetch";
export default function ApiKeyPage() {
return (
<Container size="lg" py="xl" w="100%">
<Stack gap="xl">
<Group justify="space-between" align="center">
<Group gap="xs">
<IconKey size={28} color="var(--mantine-color-cyan-5)" />
<Title order={2} fw={700} c="gray.0">
API Key Management
</Title>
</Group>
</Group>
<CreateApiKey />
</Stack>
</Container>
);
}
function CreateApiKey() {
const theme = useMantineTheme();
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [expiredAt, setExpiredAt] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name || !expiredAt) {
showNotification({
color: "red",
title: "Missing Information",
message: "Please provide a name and expiration date.",
});
return;
}
setLoading(true);
const res = await apiFetch.api.apikey.create.post({
name,
description,
expiredAt,
});
if (res.status === 200) {
setName("");
setDescription("");
setExpiredAt("");
showNotification({
title: "API Key Created",
message: "Your new API key is now active and ready to use.",
color: "teal",
});
} else {
showNotification({
title: "Error",
message: "Failed to create API key. Please try again.",
color: "red",
});
}
setLoading(false);
};
return (
<Card
radius="lg"
p="xl"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Stack gap="md">
<Group justify="space-between" align="center">
<Title order={3} c="gray.0">
Create New API Key
</Title>
<Tooltip label="Generate a new API key for your app">
<ActionIcon
variant="light"
radius="md"
size="lg"
style={{ boxShadow: "0 0 10px rgba(0,255,200,0.2)" }}
>
<IconPlus size={20} />
</ActionIcon>
</Tooltip>
</Group>
<Divider my="sm" />
<form onSubmit={handleSubmit}>
<Stack gap="md">
<TextInput
label="Key Name"
placeholder="Enter a name for this key"
required
radius="md"
value={name}
onChange={(e) => setName(e.target.value)}
styles={{
input: {
backgroundColor: "rgba(255,255,255,0.05)",
borderColor: "rgba(255,255,255,0.1)",
},
}}
/>
<TextInput
label="Description"
placeholder="Describe this keys purpose (optional)"
radius="md"
value={description}
onChange={(e) => setDescription(e.target.value)}
styles={{
input: {
backgroundColor: "rgba(255,255,255,0.05)",
borderColor: "rgba(255,255,255,0.1)",
},
}}
/>
<TextInput
label="Expiration Date"
type="date"
required
radius="md"
value={expiredAt}
onChange={(e) => setExpiredAt(e.target.value)}
styles={{
input: {
backgroundColor: "rgba(255,255,255,0.05)",
borderColor: "rgba(255,255,255,0.1)",
},
}}
/>
<Group justify="flex-end" mt="md">
<Button
variant="light"
color="gray"
radius="md"
onClick={() => {
setName("");
setDescription("");
setExpiredAt("");
}}
>
Clear
</Button>
<Button
type="submit"
loading={loading}
radius="md"
size="md"
variant="gradient"
gradient={{ from: "teal", to: "cyan", deg: 45 }}
style={{
boxShadow: "0 0 12px rgba(0,255,200,0.3)",
transition: "all 0.2s ease",
}}
>
Create Key
</Button>
</Group>
</Stack>
</form>
<Divider my="lg" />
<ListApiKey />
</Stack>
</Card>
);
}
function ListApiKey() {
const theme = useMantineTheme();
const [apiKeys, setApiKeys] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchApiKeys = async () => {
const res = await apiFetch.api.apikey.list.get();
if (res.status === 200) {
setApiKeys(res.data?.apiKeys || []);
}
setLoading(false);
};
fetchApiKeys();
}, []);
const handleDelete = async (id: string) => {
const res = await apiFetch.api.apikey.delete.delete({ id });
if (res.status === 200) {
setApiKeys(apiKeys.filter((api: any) => api.id !== id));
showNotification({
title: "API Key Deleted",
message: "The API key has been successfully removed.",
color: "teal",
});
}
};
const handleCopy = (key: string) => {
navigator.clipboard.writeText(key);
showNotification({
title: "Copied",
message: "API key copied to clipboard.",
color: "cyan",
});
};
return (
<Card
radius="lg"
withBorder
p="xl"
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Stack gap="sm">
<Group justify="space-between" align="center">
<Group gap="xs">
<IconDatabase size={22} color="var(--mantine-color-cyan-5)" />
<Title order={3} c="gray.0">
Active API Keys
</Title>
</Group>
<Badge
size="lg"
radius="sm"
color="teal"
variant="light"
style={{ textTransform: "none" }}
>
{apiKeys.length} Active
</Badge>
</Group>
<Divider my="sm" />
{loading ? (
<Group justify="center" py="xl">
<Loader color="teal" size="lg" />
</Group>
) : apiKeys.length === 0 ? (
<Stack align="center" justify="center" py="xl" gap={4}>
<IconLock size={32} color="gray" />
<Text c="dimmed" size="sm">
No API keys created yet.
</Text>
</Stack>
) : (
<ScrollArea>
<Table
highlightOnHover
withTableBorder
withColumnBorders
style={{
borderColor: "rgba(255,255,255,0.1)",
color: "var(--mantine-color-gray-0)",
fontSize: "0.9rem",
}}
>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Description</Table.Th>
<Table.Th>Expires</Table.Th>
<Table.Th>Created</Table.Th>
<Table.Th>Updated</Table.Th>
<Table.Th style={{ textAlign: "center" }}>Actions</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{apiKeys.map((apiKey: any, index: number) => (
<Table.Tr
key={index}
style={{
backgroundColor: "rgba(255,255,255,0.03)",
transition: "background 0.2s ease",
}}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor =
"rgba(0,255,200,0.05)")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor =
"rgba(255,255,255,0.03)")
}
>
<Table.Td>{apiKey.name}</Table.Td>
<Table.Td>{apiKey.description || "-"}</Table.Td>
<Table.Td>
<Badge
color={
new Date(apiKey.expiredAt) < new Date()
? "red"
: "grape"
}
variant="light"
>
{new Date(apiKey.expiredAt).toLocaleDateString()}
</Badge>
</Table.Td>
<Table.Td>{new Date(apiKey.createdAt).toLocaleDateString()}</Table.Td>
<Table.Td>{new Date(apiKey.updatedAt).toLocaleDateString()}</Table.Td>
<Table.Td>
<Group gap="xs" justify="center">
<Tooltip label="Copy API Key">
<ActionIcon
variant="light"
color="teal"
onClick={() => handleCopy(apiKey.key)}
radius="md"
>
<IconCopy size={18} />
</ActionIcon>
</Tooltip>
<Tooltip label="Delete API Key">
<ActionIcon
variant="light"
color="red"
onClick={() => handleDelete(apiKey.id)}
radius="md"
>
<IconTrash size={18} />
</ActionIcon>
</Tooltip>
</Group>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</ScrollArea>
)}
</Stack>
</Card>
);
}