mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-14 08:19:51 +02:00
copy key without clicking reveal first
This commit is contained in:
@@ -8,29 +8,40 @@ import { useTranslations } from "next-intl";
|
|||||||
type CopyTextBoxProps = {
|
type CopyTextBoxProps = {
|
||||||
text?: string;
|
text?: string;
|
||||||
displayText?: string;
|
displayText?: string;
|
||||||
|
getCopyText?: () => Promise<string>;
|
||||||
wrapText?: boolean;
|
wrapText?: boolean;
|
||||||
outline?: boolean;
|
outline?: boolean;
|
||||||
|
centered?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CopyTextBox({
|
export default function CopyTextBox({
|
||||||
text = "",
|
text = "",
|
||||||
displayText,
|
displayText,
|
||||||
|
getCopyText,
|
||||||
wrapText = false,
|
wrapText = false,
|
||||||
outline = true
|
outline = true,
|
||||||
|
centered = false
|
||||||
}: CopyTextBoxProps) {
|
}: CopyTextBoxProps) {
|
||||||
const [isCopied, setIsCopied] = useState(false);
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
const [isCopying, setIsCopying] = useState(false);
|
||||||
const textRef = useRef<HTMLPreElement>(null);
|
const textRef = useRef<HTMLPreElement>(null);
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const copyToClipboard = async () => {
|
const copyToClipboard = async () => {
|
||||||
if (textRef.current) {
|
if (!textRef.current || isCopying) {
|
||||||
try {
|
return;
|
||||||
await navigator.clipboard.writeText(text);
|
}
|
||||||
setIsCopied(true);
|
|
||||||
setTimeout(() => setIsCopied(false), 2000);
|
setIsCopying(true);
|
||||||
} catch (err) {
|
try {
|
||||||
console.error(t("copyTextFailed"), err);
|
const value = getCopyText ? await getCopyText() : text;
|
||||||
}
|
await navigator.clipboard.writeText(value);
|
||||||
|
setIsCopied(true);
|
||||||
|
setTimeout(() => setIsCopied(false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(t("copyTextFailed"), err);
|
||||||
|
} finally {
|
||||||
|
setIsCopying(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -40,7 +51,9 @@ export default function CopyTextBox({
|
|||||||
>
|
>
|
||||||
<pre
|
<pre
|
||||||
ref={textRef}
|
ref={textRef}
|
||||||
className={`p-4 pr-16 text-sm w-full ${
|
className={`py-4 text-sm w-full ${
|
||||||
|
centered ? "px-16 text-center" : "pl-4 pr-16"
|
||||||
|
} ${
|
||||||
wrapText
|
wrapText
|
||||||
? "whitespace-pre-wrap break-words"
|
? "whitespace-pre-wrap break-words"
|
||||||
: "overflow-x-auto"
|
: "overflow-x-auto"
|
||||||
@@ -54,6 +67,7 @@ export default function CopyTextBox({
|
|||||||
type="button"
|
type="button"
|
||||||
className="absolute top-0.5 right-0 z-10 bg-card"
|
className="absolute top-0.5 right-0 z-10 bg-card"
|
||||||
onClick={copyToClipboard}
|
onClick={copyToClipboard}
|
||||||
|
loading={isCopying}
|
||||||
aria-label={t("copyTextClipboard")}
|
aria-label={t("copyTextClipboard")}
|
||||||
>
|
>
|
||||||
{isCopied ? (
|
{isCopied ? (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { Check, Copy } from "lucide-react";
|
import { Check, Copy, Loader2 } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -7,6 +7,7 @@ import { useTranslations } from "next-intl";
|
|||||||
type CopyToClipboardProps = {
|
type CopyToClipboardProps = {
|
||||||
text: string;
|
text: string;
|
||||||
displayText?: string;
|
displayText?: string;
|
||||||
|
getCopyText?: () => Promise<string>;
|
||||||
isLink?: boolean;
|
isLink?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
@@ -14,18 +15,31 @@ type CopyToClipboardProps = {
|
|||||||
const CopyToClipboard = ({
|
const CopyToClipboard = ({
|
||||||
text,
|
text,
|
||||||
displayText,
|
displayText,
|
||||||
|
getCopyText,
|
||||||
isLink,
|
isLink,
|
||||||
className
|
className
|
||||||
}: CopyToClipboardProps) => {
|
}: CopyToClipboardProps) => {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [copying, setCopying] = useState(false);
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = async () => {
|
||||||
navigator.clipboard.writeText(text);
|
if (copying) {
|
||||||
setCopied(true);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setCopying(true);
|
||||||
setCopied(false);
|
try {
|
||||||
}, 2000);
|
const value = getCopyText ? await getCopyText() : text;
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 2000);
|
||||||
|
} catch {
|
||||||
|
// Fetch errors are toasted by the caller; clipboard failures stay silent.
|
||||||
|
} finally {
|
||||||
|
setCopying(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayValue = displayText ?? text;
|
const displayValue = displayText ?? text;
|
||||||
@@ -38,11 +52,14 @@ const CopyToClipboard = ({
|
|||||||
type="button"
|
type="button"
|
||||||
className="h-4 w-4 p-0 flex items-center justify-center cursor-pointer flex-shrink-0"
|
className="h-4 w-4 p-0 flex items-center justify-center cursor-pointer flex-shrink-0"
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
|
disabled={copying}
|
||||||
>
|
>
|
||||||
{!copied ? (
|
{copying ? (
|
||||||
<Copy className="h-4 w-4" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
) : (
|
) : copied ? (
|
||||||
<Check className="text-green-500 h-4 w-4" />
|
<Check className="text-green-500 h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
<span className="sr-only">{t("copyText")}</span>
|
<span className="sr-only">{t("copyText")}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { AxiosResponse } from "axios";
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import CopyTextBox from "@app/components/CopyTextBox";
|
import CopyTextBox from "@app/components/CopyTextBox";
|
||||||
@@ -17,73 +15,18 @@ import {
|
|||||||
SettingsSectionHeader,
|
SettingsSectionHeader,
|
||||||
SettingsSectionTitle as SectionTitle
|
SettingsSectionTitle as SectionTitle
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { useMyVirtualApiKeySecret } from "@app/hooks/useMyVirtualApiKeySecret";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
||||||
import { toast } from "@app/hooks/useToast";
|
|
||||||
import type {
|
import type {
|
||||||
GetMyVirtualApiKeyResponse,
|
|
||||||
ListMyVirtualApiKeysResponse,
|
ListMyVirtualApiKeysResponse,
|
||||||
VirtualApiKeyWithResources
|
VirtualApiKeyWithResources
|
||||||
} from "@server/routers/virtualApiKey/types";
|
} from "@server/routers/virtualApiKey/types";
|
||||||
import {
|
import { formatVirtualApiKeyPreview } from "@app/lib/virtualApiKeyFormat";
|
||||||
formatVirtualApiKeyCredential,
|
|
||||||
formatVirtualApiKeyPreview
|
|
||||||
} from "@app/lib/virtualApiKeyFormat";
|
|
||||||
|
|
||||||
type UserVirtualApiKeysProps = {
|
type UserVirtualApiKeysProps = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
initialData: ListMyVirtualApiKeysResponse;
|
initialData: ListMyVirtualApiKeysResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
function useRevealSecret(orgId: string, virtualApiKeyId: string) {
|
|
||||||
const t = useTranslations();
|
|
||||||
const api = createApiClient(useEnvContext());
|
|
||||||
const [credential, setCredential] = useState<string | null>(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const revealSecret = () => {
|
|
||||||
if (credential || loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
api.get<AxiosResponse<GetMyVirtualApiKeyResponse>>(
|
|
||||||
`/org/${orgId}/my-virtual-api-keys/${virtualApiKeyId}`
|
|
||||||
)
|
|
||||||
.then((res) => {
|
|
||||||
const secret = res.data.data.virtualApiKey.secret;
|
|
||||||
if (secret) {
|
|
||||||
setCredential(
|
|
||||||
formatVirtualApiKeyCredential(virtualApiKeyId, secret)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("virtualApiKeysErrorFetchSecret"),
|
|
||||||
description: t(
|
|
||||||
"virtualApiKeysErrorFetchSecretDescription"
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("virtualApiKeysErrorFetchSecret"),
|
|
||||||
description: formatAxiosError(
|
|
||||||
e,
|
|
||||||
t("virtualApiKeysErrorFetchSecretDescription")
|
|
||||||
)
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return { credential, loading, revealSecret };
|
|
||||||
}
|
|
||||||
|
|
||||||
function OwnedKeySecret({
|
function OwnedKeySecret({
|
||||||
orgId,
|
orgId,
|
||||||
virtualApiKeyId,
|
virtualApiKeyId,
|
||||||
@@ -95,11 +38,9 @@ function OwnedKeySecret({
|
|||||||
}) {
|
}) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const preview = formatVirtualApiKeyPreview(virtualApiKeyId, lastChars);
|
const preview = formatVirtualApiKeyPreview(virtualApiKeyId, lastChars);
|
||||||
const { credential, loading, revealSecret } = useRevealSecret(
|
const { credential, revealed, loading, getCopyText, revealSecret } =
|
||||||
orgId,
|
useMyVirtualApiKeySecret(orgId, virtualApiKeyId);
|
||||||
virtualApiKeyId
|
const displayValue = revealed && credential ? credential : preview;
|
||||||
);
|
|
||||||
const displayValue = credential ?? preview;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
@@ -107,9 +48,10 @@ function OwnedKeySecret({
|
|||||||
<CopyToClipboard
|
<CopyToClipboard
|
||||||
text={displayValue}
|
text={displayValue}
|
||||||
displayText={displayValue}
|
displayText={displayValue}
|
||||||
|
getCopyText={getCopyText}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!credential ? (
|
{!revealed ? (
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -137,11 +79,9 @@ function IdentityKeyCenterpiece({
|
|||||||
}) {
|
}) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const preview = formatVirtualApiKeyPreview(virtualApiKeyId, lastChars);
|
const preview = formatVirtualApiKeyPreview(virtualApiKeyId, lastChars);
|
||||||
const { credential, loading, revealSecret } = useRevealSecret(
|
const { credential, revealed, loading, getCopyText, revealSecret } =
|
||||||
orgId,
|
useMyVirtualApiKeySecret(orgId, virtualApiKeyId);
|
||||||
virtualApiKeyId
|
const displayValue = revealed && credential ? credential : preview;
|
||||||
);
|
|
||||||
const displayValue = credential ?? preview;
|
|
||||||
const headline = resourceName
|
const headline = resourceName
|
||||||
? t("myVirtualApiKeysIdentityResourceHeadline", { resourceName })
|
? t("myVirtualApiKeysIdentityResourceHeadline", { resourceName })
|
||||||
: t("myVirtualApiKeysIdentityHeadline");
|
: t("myVirtualApiKeysIdentityHeadline");
|
||||||
@@ -159,9 +99,14 @@ function IdentityKeyCenterpiece({
|
|||||||
</p>
|
</p>
|
||||||
<div className="mt-8 w-full max-w-2xl">
|
<div className="mt-8 w-full max-w-2xl">
|
||||||
<div className="[&_pre]:text-base [&_code]:font-mono [&_code]:tracking-wide">
|
<div className="[&_pre]:text-base [&_code]:font-mono [&_code]:tracking-wide">
|
||||||
<CopyTextBox text={displayValue} wrapText={false} />
|
<CopyTextBox
|
||||||
|
text={displayValue}
|
||||||
|
getCopyText={getCopyText}
|
||||||
|
wrapText={false}
|
||||||
|
centered
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!credential ? (
|
{!revealed ? (
|
||||||
<div className="mt-3 flex justify-center">
|
<div className="mt-3 flex justify-center">
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
|
|||||||
@@ -12,78 +12,19 @@ import {
|
|||||||
SettingsSubsectionTitle
|
SettingsSubsectionTitle
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { useMyVirtualApiKeySecret } from "@app/hooks/useMyVirtualApiKeySecret";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
||||||
import { toast } from "@app/hooks/useToast";
|
|
||||||
import { launcherQueries } from "@app/lib/queries";
|
import { launcherQueries } from "@app/lib/queries";
|
||||||
import type {
|
import type { VirtualApiKeyWithResources } from "@server/routers/virtualApiKey/types";
|
||||||
GetMyVirtualApiKeyResponse,
|
import { formatVirtualApiKeyPreview } from "@app/lib/virtualApiKeyFormat";
|
||||||
VirtualApiKeyWithResources
|
|
||||||
} from "@server/routers/virtualApiKey/types";
|
|
||||||
import {
|
|
||||||
formatVirtualApiKeyCredential,
|
|
||||||
formatVirtualApiKeyPreview
|
|
||||||
} from "@app/lib/virtualApiKeyFormat";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import type { AxiosResponse } from "axios";
|
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
type LauncherInferenceApiKeysSectionProps = {
|
type LauncherInferenceApiKeysSectionProps = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
resourceGuid: string;
|
resourceGuid: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function useRevealSecret(orgId: string, virtualApiKeyId: string) {
|
|
||||||
const t = useTranslations();
|
|
||||||
const api = createApiClient(useEnvContext());
|
|
||||||
const [credential, setCredential] = useState<string | null>(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const revealSecret = () => {
|
|
||||||
if (credential || loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
api.get<AxiosResponse<GetMyVirtualApiKeyResponse>>(
|
|
||||||
`/org/${orgId}/my-virtual-api-keys/${virtualApiKeyId}`
|
|
||||||
)
|
|
||||||
.then((res) => {
|
|
||||||
const secret = res.data.data.virtualApiKey.secret;
|
|
||||||
if (secret) {
|
|
||||||
setCredential(
|
|
||||||
formatVirtualApiKeyCredential(virtualApiKeyId, secret)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("virtualApiKeysErrorFetchSecret"),
|
|
||||||
description: t(
|
|
||||||
"virtualApiKeysErrorFetchSecretDescription"
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("virtualApiKeysErrorFetchSecret"),
|
|
||||||
description: formatAxiosError(
|
|
||||||
e,
|
|
||||||
t("virtualApiKeysErrorFetchSecretDescription")
|
|
||||||
)
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return { credential, loading, revealSecret };
|
|
||||||
}
|
|
||||||
|
|
||||||
function PanelKeySecret({
|
function PanelKeySecret({
|
||||||
orgId,
|
orgId,
|
||||||
virtualApiKeyId,
|
virtualApiKeyId,
|
||||||
@@ -95,11 +36,9 @@ function PanelKeySecret({
|
|||||||
}) {
|
}) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const preview = formatVirtualApiKeyPreview(virtualApiKeyId, lastChars);
|
const preview = formatVirtualApiKeyPreview(virtualApiKeyId, lastChars);
|
||||||
const { credential, loading, revealSecret } = useRevealSecret(
|
const { credential, revealed, loading, getCopyText, revealSecret } =
|
||||||
orgId,
|
useMyVirtualApiKeySecret(orgId, virtualApiKeyId);
|
||||||
virtualApiKeyId
|
const displayValue = revealed && credential ? credential : preview;
|
||||||
);
|
|
||||||
const displayValue = credential ?? preview;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
@@ -107,9 +46,10 @@ function PanelKeySecret({
|
|||||||
<CopyToClipboard
|
<CopyToClipboard
|
||||||
text={displayValue}
|
text={displayValue}
|
||||||
displayText={displayValue}
|
displayText={displayValue}
|
||||||
|
getCopyText={getCopyText}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!credential ? (
|
{!revealed ? (
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
import type { GetMyVirtualApiKeyResponse } from "@server/routers/virtualApiKey/types";
|
||||||
|
import { formatVirtualApiKeyCredential } from "@app/lib/virtualApiKeyFormat";
|
||||||
|
|
||||||
|
export function useMyVirtualApiKeySecret(
|
||||||
|
orgId: string,
|
||||||
|
virtualApiKeyId: string
|
||||||
|
) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const env = useEnvContext();
|
||||||
|
const [credential, setCredential] = useState<string | null>(null);
|
||||||
|
const [revealed, setRevealed] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const credentialRef = useRef<string | null>(null);
|
||||||
|
const inFlightRef = useRef<Promise<string | null> | null>(null);
|
||||||
|
|
||||||
|
const getCredential = useCallback(async (): Promise<string | null> => {
|
||||||
|
if (credentialRef.current) {
|
||||||
|
return credentialRef.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inFlightRef.current) {
|
||||||
|
return inFlightRef.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = createApiClient(env);
|
||||||
|
const request = api
|
||||||
|
.get<AxiosResponse<GetMyVirtualApiKeyResponse>>(
|
||||||
|
`/org/${orgId}/my-virtual-api-keys/${virtualApiKeyId}`
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
const secret = res.data.data.virtualApiKey.secret;
|
||||||
|
if (!secret) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("virtualApiKeysErrorFetchSecret"),
|
||||||
|
description: t(
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = formatVirtualApiKeyCredential(
|
||||||
|
virtualApiKeyId,
|
||||||
|
secret
|
||||||
|
);
|
||||||
|
credentialRef.current = formatted;
|
||||||
|
setCredential(formatted);
|
||||||
|
return formatted;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("virtualApiKeysErrorFetchSecret"),
|
||||||
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
t("virtualApiKeysErrorFetchSecretDescription")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
inFlightRef.current = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
inFlightRef.current = request;
|
||||||
|
return request;
|
||||||
|
}, [env, orgId, t, virtualApiKeyId]);
|
||||||
|
|
||||||
|
const getCopyText = useCallback(async () => {
|
||||||
|
const value = await getCredential();
|
||||||
|
if (!value) {
|
||||||
|
throw new Error("Failed to fetch virtual API key secret");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}, [getCredential]);
|
||||||
|
|
||||||
|
const revealSecret = useCallback(async () => {
|
||||||
|
if (revealed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = await getCredential();
|
||||||
|
if (value) {
|
||||||
|
setRevealed(true);
|
||||||
|
}
|
||||||
|
}, [getCredential, revealed]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
credential,
|
||||||
|
revealed,
|
||||||
|
loading,
|
||||||
|
getCredential,
|
||||||
|
getCopyText,
|
||||||
|
revealSecret
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user