mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-12 15:30:53 +02:00
allow retrieving api key secret
This commit is contained in:
+2
-2
@@ -1658,7 +1658,7 @@
|
||||
"virtualApiKeysName": "Name",
|
||||
"virtualApiKeysDescriptionOptional": "Description (optional)",
|
||||
"virtualApiKeysAssociateUserOptional": "Associate User (optional)",
|
||||
"virtualApiKeysAssociateUserDescription": "Attribution only. Does not grant access by itself.",
|
||||
"virtualApiKeysAssociateUserDescription": "Associate this key with a user to track usage.",
|
||||
"virtualApiKeysAllResources": "All public inference resources",
|
||||
"virtualApiKeysAllResourcesDescription": "Allow this key to access every public inference resource in the organization",
|
||||
"virtualApiKeysSelectResources": "Public Inference Resources",
|
||||
@@ -1666,7 +1666,7 @@
|
||||
"virtualApiKeysSelectResourcesDescription": "Choose which public inference resources this key can access",
|
||||
"virtualApiKeysNoResources": "No resources",
|
||||
"virtualApiKeysSecret": "Key",
|
||||
"virtualApiKeysSeeOnce": "Copy this key now. You can also view it again later from the table.",
|
||||
"virtualApiKeysCopyKey": "Copy this key. You can view it again later from the table or when editing.",
|
||||
"virtualApiKeysSecretHint": "Use this value as a Bearer token: vk-[id].[secret]",
|
||||
"virtualApiKeysViewSecret": "View Secret",
|
||||
"virtualApiKeysViewSecretTitle": "Virtual API Key Secret",
|
||||
|
||||
@@ -391,7 +391,7 @@ export default function CreateVirtualApiKeyForm({
|
||||
)}
|
||||
{credential && (
|
||||
<div className="space-y-4">
|
||||
<p>{t("virtualApiKeysSeeOnce")}</p>
|
||||
<p>{t("virtualApiKeysCopyKey")}</p>
|
||||
<CopyTextBox
|
||||
text={credential}
|
||||
wrapText={false}
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Label } from "@app/components/ui/label";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AxiosResponse } from "axios";
|
||||
@@ -46,7 +46,9 @@ import {
|
||||
} from "@app/components/multi-resource-selector";
|
||||
import type { SelectedResource } from "@app/components/resource-selector";
|
||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||
import CopyTextBox from "@app/components/CopyTextBox";
|
||||
import type { CreatedVirtualApiKey } from "@app/components/CreateVirtualApiKeyForm";
|
||||
import type { GetVirtualApiKeyResponse } from "@server/routers/virtualApiKey/types";
|
||||
|
||||
type FormProps = {
|
||||
open: boolean;
|
||||
@@ -96,6 +98,8 @@ export default function EditVirtualApiKeyForm({
|
||||
const [selectedResources, setSelectedResources] = useState<
|
||||
SelectedResource[]
|
||||
>([]);
|
||||
const [credential, setCredential] = useState<string | null>(null);
|
||||
const [credentialLoading, setCredentialLoading] = useState(false);
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
@@ -132,6 +136,55 @@ export default function EditVirtualApiKeyForm({
|
||||
form.reset({
|
||||
allResources: virtualApiKey.allResources
|
||||
});
|
||||
|
||||
let cancelled = false;
|
||||
setCredentialLoading(true);
|
||||
setCredential(null);
|
||||
|
||||
api.get<AxiosResponse<GetVirtualApiKeyResponse>>(
|
||||
`/virtual-api-key/${virtualApiKey.virtualApiKeyId}`
|
||||
)
|
||||
.then((res) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
const secret = res.data.data.virtualApiKey.secret;
|
||||
if (secret) {
|
||||
setCredential(
|
||||
`vk-${virtualApiKey.virtualApiKeyId}.${secret}`
|
||||
);
|
||||
} else {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("virtualApiKeysErrorFetchSecret"),
|
||||
description: t(
|
||||
"virtualApiKeysErrorFetchSecretDescription"
|
||||
)
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("virtualApiKeysErrorFetchSecret"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("virtualApiKeysErrorFetchSecretDescription")
|
||||
)
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) {
|
||||
setCredentialLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [open, virtualApiKey, form]);
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
@@ -228,11 +281,11 @@ export default function EditVirtualApiKeyForm({
|
||||
id="edit-virtual-api-key-form"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<FormLabel>
|
||||
<Label>
|
||||
{t(
|
||||
"virtualApiKeysAssociateUserOptional"
|
||||
)}
|
||||
</FormLabel>
|
||||
</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
@@ -316,11 +369,11 @@ export default function EditVirtualApiKeyForm({
|
||||
|
||||
{!allResources && (
|
||||
<div className="space-y-2">
|
||||
<FormLabel>
|
||||
<Label>
|
||||
{t(
|
||||
"virtualApiKeysSelectResources"
|
||||
)}
|
||||
</FormLabel>
|
||||
</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
|
||||
@@ -35,6 +35,7 @@ import CreateVirtualApiKeyForm, {
|
||||
} from "@app/components/CreateVirtualApiKeyForm";
|
||||
import EditVirtualApiKeyForm from "@app/components/EditVirtualApiKeyForm";
|
||||
import ViewVirtualApiKeySecret from "@app/components/ViewVirtualApiKeySecret";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||
import { UserSelector, type SelectedUser } from "@app/components/user-selector";
|
||||
@@ -44,6 +45,8 @@ import {
|
||||
} from "@app/components/resource-selector";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilterPopover";
|
||||
import type { GetVirtualApiKeyResponse } from "@server/routers/virtualApiKey/types";
|
||||
import { AxiosResponse } from "axios";
|
||||
|
||||
export type VirtualApiKeyRow = CreatedVirtualApiKey;
|
||||
|
||||
@@ -344,8 +347,12 @@ export default function VirtualApiKeysTable({
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) =>
|
||||
`vk-${row.original.virtualApiKeyId}.••••${row.original.lastChars}`
|
||||
cell: ({ row }) => (
|
||||
<VirtualApiKeySecretCell
|
||||
virtualApiKeyId={row.original.virtualApiKeyId}
|
||||
lastChars={row.original.lastChars}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
@@ -396,7 +403,7 @@ export default function VirtualApiKeysTable({
|
||||
cell: ({ row }) => {
|
||||
const keyRow = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
@@ -514,3 +521,54 @@ export default function VirtualApiKeysTable({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function VirtualApiKeySecretCell({
|
||||
virtualApiKeyId,
|
||||
lastChars
|
||||
}: {
|
||||
virtualApiKeyId: string;
|
||||
lastChars: string;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const preview = `vk-${virtualApiKeyId}••••${lastChars}`;
|
||||
const [credential, setCredential] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
api.get<AxiosResponse<GetVirtualApiKeyResponse>>(
|
||||
`/virtual-api-key/${virtualApiKeyId}`
|
||||
)
|
||||
.then((res) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
const secret = res.data.data.virtualApiKey.secret;
|
||||
if (secret) {
|
||||
setCredential(`vk-${virtualApiKeyId}.${secret}`);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("virtualApiKeysErrorFetchSecret"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("virtualApiKeysErrorFetchSecretDescription")
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [virtualApiKeyId]);
|
||||
|
||||
return (
|
||||
<CopyToClipboard text={credential ?? preview} displayText={preview} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,14 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { DataTablePagination } from "@app/components/DataTablePagination";
|
||||
import { dataTableFilterDropdownContentClassName } from "@app/lib/dataTableFilterPopover";
|
||||
import { ChevronDown, Plus, Search, RefreshCw, Columns, Filter } from "lucide-react";
|
||||
import {
|
||||
ChevronDown,
|
||||
Plus,
|
||||
Search,
|
||||
RefreshCw,
|
||||
Columns,
|
||||
Filter
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -523,29 +530,33 @@ export function DataTable<TData, TValue>({
|
||||
addButtonText && ((addActions && addActions.length > 0) || onAdd)
|
||||
);
|
||||
const showAddActionInEmptyState = !hasRows && hasAddAction;
|
||||
const addAction = addActions && addActions.length > 0 && addButtonText ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button disabled={addButtonDisabled}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{addButtonText}
|
||||
<ChevronDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{addActions.map((action, i) => (
|
||||
<DropdownMenuItem key={i} onSelect={() => action.onSelect()}>
|
||||
{action.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : onAdd && addButtonText ? (
|
||||
<Button onClick={onAdd} disabled={addButtonDisabled}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{addButtonText}
|
||||
</Button>
|
||||
) : null;
|
||||
const addAction =
|
||||
addActions && addActions.length > 0 && addButtonText ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button disabled={addButtonDisabled}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{addButtonText}
|
||||
<ChevronDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{addActions.map((action, i) => (
|
||||
<DropdownMenuItem
|
||||
key={i}
|
||||
onSelect={() => action.onSelect()}
|
||||
>
|
||||
{action.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : onAdd && addButtonText ? (
|
||||
<Button onClick={onAdd} disabled={addButtonDisabled}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{addButtonText}
|
||||
</Button>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-12xl">
|
||||
@@ -674,7 +685,9 @@ export function DataTable<TData, TValue>({
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onRefresh}
|
||||
disabled={isRefreshing || refreshButtonDisabled}
|
||||
disabled={
|
||||
isRefreshing || refreshButtonDisabled
|
||||
}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`mr-0 sm:mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||
@@ -757,13 +770,6 @@ export function DataTable<TData, TValue>({
|
||||
align="end"
|
||||
className="w-48"
|
||||
>
|
||||
<DropdownMenuLabel>
|
||||
{t(
|
||||
"toggleColumns"
|
||||
) ||
|
||||
"Toggle columns"}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter(
|
||||
@@ -891,13 +897,11 @@ export function DataTable<TData, TValue>({
|
||||
<DataTableEmptyState
|
||||
colSpan={columns.length}
|
||||
action={
|
||||
showAddActionInEmptyState
|
||||
? (
|
||||
<div className="hidden sm:block">
|
||||
{addAction}
|
||||
</div>
|
||||
)
|
||||
: undefined
|
||||
showAddActionInEmptyState ? (
|
||||
<div className="hidden sm:block">
|
||||
{addAction}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user