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