hts/apps/migrant/app/[locale]/manage/keys/[keyAuthId]/[keyId]/page.tsx

275 lines
8.8 KiB
TypeScript

import { StackedColumnChart } from "@/components/dashboard/charts";
import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { getTenantId } from "@/lib/auth";
// import { and, db, eq, isNull, schema } from "@/lib/db";
import { formatNumber } from "@/lib/fmt";
// import {
// getLastUsed,
// getLatestVerifications,
// getVerificationsDaily,
// getVerificationsHourly,
// } from "@/lib/tinybird";
import { BarChart, Minus } from "lucide-react";
// import ms from "ms";
import { notFound } from "next/navigation";
import { type Interval, IntervalSelect } from "../../../apis/[apiId]/select";
import { AccessTable } from "./table";
export const dynamic = "force-dynamic";
export const runtime = "edge";
export default async function KeyPage(props: {
params: { keyId: string };
searchParams: {
interval?: Interval;
};
}) {
const tenantId = getTenantId();
// const key = await db.query.keys.findFirst({
// where: and(eq(schema.keys.id, props.params.keyId), isNull(schema.keys.deletedAt)),
// with: {
// workspace: true,
// },
// });
// if (!key || key.workspace.tenantId !== tenantId) {
// return notFound();
// }
// const api = await db.query.apis.findFirst({
// where: (table, { eq, and, isNull }) =>
// and(eq(table.keyAuthId, key.keyAuthId), isNull(table.deletedAt)),
// });
// if (!api) {
// return notFound();
// }
const interval = props.searchParams.interval ?? "7d";
const { getVerificationsPerInterval, start, end, granularity } = prepareInterval(interval);
// const query = {
// workspaceId: api.workspaceId,
// apiId: api.id,
// keyId: key.id,
// start,
// end,
// };
// const [verifications, totalUsage, latestVerifications, lastUsed] = await Promise.all([
// getVerificationsPerInterval(query),
// getVerificationsPerInterval({
// workspaceId: api.workspaceId,
// apiId: api.id,
// keyId: key.id,
// }).then((res) => res.data.at(0) ?? { success: 0, rateLimited: 0, usageExceeded: 0 }), // no interval -> a
// getLatestVerifications({
// workspaceId: key.workspaceId,
// apiId: api.id,
// keyId: key.id,
// }),
// getLastUsed({ keyId: key.id }).then((res) => res.data.at(0)?.lastUsed ?? 0),
// ]);
const successOverTime: { x: string; y: number }[] = [];
const ratelimitedOverTime: { x: string; y: number }[] = [];
const usageExceededOverTime: { x: string; y: number }[] = [];
// for (const d of verifications.data.sort((a, b) => a.time - b.time)) {
// const x = new Date(d.time).toISOString();
// successOverTime.push({ x, y: d.success });
// ratelimitedOverTime.push({ x, y: d.rateLimited });
// usageExceededOverTime.push({ x, y: d.usageExceeded });
// }
const verificationsData = [
...successOverTime.map((d) => ({
...d,
category: "Successful Verifications",
})),
...ratelimitedOverTime.map((d) => ({ ...d, category: "Ratelimited" })),
...usageExceededOverTime.map((d) => ({ ...d, category: "Usage Exceeded" })),
];
return (
<div className="flex flex-col gap-4">
{/* <Card>
<CardContent className="grid grid-cols-6 divide-x">
<Metric
label={key.expires && key.expires.getTime() < Date.now() ? "Expired" : "Expires"}
value={key.expires ? ms(key.expires.getTime() - Date.now()) : <Minus />}
/>
<Metric
label="Remaining"
value={typeof key.remaining === "number" ? formatNumber(key.remaining) : <Minus />}
/>
<Metric
label="Last Used"
value={lastUsed ? `${ms(Date.now() - lastUsed)} ago` : <Minus />}
/>
<Metric
label="Success"
value={formatNumber(totalUsage.success)}
tooltip="The total number of successful verifications for this key"
/>
<Metric
label="Ratelimited"
value={formatNumber(totalUsage.rateLimited)}
tooltip="The total number of ratelimited and therefore rejected verifications. These are not billed."
/>
<Metric
label="Usage Exceeded"
value={formatNumber(totalUsage.usageExceeded)}
tooltip="The total number of verifications that exceeded the limit and were rejected. These are not billed."
/>
</CardContent>
</Card> */}
<Separator className="my-8" />
<div className="flex items-center justify-between">
<h2 className="text-2xl font-semibold leading-none tracking-tight">Verifications</h2>
<div>
<IntervalSelect defaultSelected={interval} />
</div>
</div>
{verificationsData.some(({ y }) => y > 0) ? (
<Card>
<CardHeader>
<div className="grid grid-cols-3 divide-x">
<Metric
label="Successful Verifications"
value={formatNumber(0)}
/>
<Metric
label="Ratelimited"
value={formatNumber(
// verifications.data.reduce((sum, day) => sum + day.rateLimited, 0),
0
)}
/>
<Metric
label="Usage Exceeded"
value={formatNumber(
// verifications.data.reduce((sum, day) => sum + day.usageExceeded, 0),
0
)}
/>
</div>
</CardHeader>
<CardContent>
<StackedColumnChart
data={verificationsData}
timeGranularity={
granularity >= 1000 * 60 * 60 * 24 * 30
? "month"
: granularity >= 1000 * 60 * 60 * 24
? "day"
: "hour"
}
/>
</CardContent>
</Card>
) : (
<EmptyPlaceholder>
<EmptyPlaceholder.Icon>
<BarChart />
</EmptyPlaceholder.Icon>
<EmptyPlaceholder.Title>Not used</EmptyPlaceholder.Title>
<EmptyPlaceholder.Description>
This key was not used in the last {interval}
</EmptyPlaceholder.Description>
{/* <CreateNewRole trigger={<Button variant="primary">Create New Role</Button>} /> */}
</EmptyPlaceholder>
)}
<Separator className="my-8" />
{/* <AccessTable verifications={latestVerifications.data} /> */}
</div>
);
}
const getVerificationsHourly = 0
const getVerificationsDaily = 0
function prepareInterval(interval: Interval) {
const now = new Date();
switch (interval) {
case "24h": {
const end = now.setUTCHours(now.getUTCHours() + 1, 0, 0, 0);
const intervalMs = 1000 * 60 * 60 * 24;
return {
start: end - intervalMs,
end,
intervalMs,
granularity: 1000 * 60 * 60,
getVerificationsPerInterval: getVerificationsHourly,
};
}
case "7d": {
now.setUTCDate(now.getUTCDate() + 1);
const end = now.setUTCHours(0, 0, 0, 0);
const intervalMs = 1000 * 60 * 60 * 24 * 7;
return {
start: end - intervalMs,
end,
intervalMs,
granularity: 1000 * 60 * 60 * 24,
getVerificationsPerInterval: getVerificationsDaily,
};
}
case "30d": {
now.setUTCDate(now.getUTCDate() + 1);
const end = now.setUTCHours(0, 0, 0, 0);
const intervalMs = 1000 * 60 * 60 * 24 * 30;
return {
start: end - intervalMs,
end,
intervalMs,
granularity: 1000 * 60 * 60 * 24,
getVerificationsPerInterval: getVerificationsDaily,
};
}
case "90d": {
now.setUTCDate(now.getUTCDate() + 1);
const end = now.setUTCHours(0, 0, 0, 0);
const intervalMs = 1000 * 60 * 60 * 24 * 90;
return {
start: end - intervalMs,
end,
intervalMs,
granularity: 1000 * 60 * 60 * 24,
getVerificationsPerInterval: getVerificationsDaily,
};
}
}
}
const Metric: React.FC<{
label: React.ReactNode;
value: React.ReactNode;
tooltip?: React.ReactNode;
}> = ({ label, value, tooltip }) => {
const component = (
<div className="flex flex-col items-start justify-center px-4 py-2">
<p className="text-sm text-content-subtle">{label}</p>
<div className="text-2xl font-semibold leading-none tracking-tight">{value}</div>
</div>
);
if (tooltip) {
return (
<Tooltip>
<TooltipTrigger asChild>{component}</TooltipTrigger>
<TooltipContent>
<p className="text-sm text-content-subtle">{tooltip}</p>
</TooltipContent>
</Tooltip>
);
}
return component;
};
// pnpm install 20.92s user 44.80s system 167% cpu 39.265 total