From e8f1e78c87143142e1aa4c4ef43df50b73c37311 Mon Sep 17 00:00:00 2001 From: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:37:11 -0300 Subject: [PATCH 01/13] refactor(dashboard): /dashboard/tokens on the CI UI Kit (KEA-7886, lote 3) --- src/app/(dashboard)/dashboard/tokens/page.tsx | 359 +++++++++--------- 1 file changed, 176 insertions(+), 183 deletions(-) diff --git a/src/app/(dashboard)/dashboard/tokens/page.tsx b/src/app/(dashboard)/dashboard/tokens/page.tsx index 17a31288aa04..e1561dbd5c0f 100644 --- a/src/app/(dashboard)/dashboard/tokens/page.tsx +++ b/src/app/(dashboard)/dashboard/tokens/page.tsx @@ -2,7 +2,21 @@ import { useState, useEffect, useCallback } from "react"; import { useLocale, useTranslations } from "next-intl"; -import { Card } from "@/shared/components"; +import { Alert } from "@omniroute/ui-kit/alert"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@omniroute/ui-kit/table"; +import { TableLoadingState } from "@omniroute/ui-kit/table-loading"; +import Badge from "@/shared/components/Badge"; +import Button from "@/shared/components/Button"; +import Card from "@/shared/components/Card"; +import EmptyState from "@/shared/components/EmptyState"; +import Input from "@/shared/components/Input"; interface TokenLedgerEntry { id: number; @@ -264,6 +278,27 @@ export default function TokensPage() { } }; + const statusMessage = ( + message: { type: "success" | "error"; text: string } | null, + onDismiss: () => void + ) => + message ? ( + + {message.text} + + ) : null; + + const errorMessage = (text: string, onDismiss: () => void) => + text ? ( + + {text} + + ) : null; + return (
{/* Balance */} @@ -273,128 +308,102 @@ export default function TokensPage() {

{t("tokensTokenBalance")}

{balance.toLocaleString(locale)}

-
🪙
+ {/* Transfer Form */}
-
- - setToApiKeyId(e.target.value)} - placeholder={t("tokensRecipientApiKeyIdPlaceholder")} + setToApiKeyId(e.target.value)} + placeholder={t("tokensRecipientApiKeyIdPlaceholder")} + required + /> +
+ setAmount(e.target.value)} + placeholder="0" + min="1" required - className="w-full px-3 py-2 rounded-lg bg-background border border-border text-sm focus:outline-none focus:ring-1 focus:ring-violet-500" + /> + setReason(e.target.value)} + placeholder={t("tokensReasonPlaceholder")} />
-
-
- - setAmount(e.target.value)} - placeholder="0" - min="1" - required - className="w-full px-3 py-2 rounded-lg bg-background border border-border text-sm focus:outline-none focus:ring-1 focus:ring-violet-500" - /> -
-
- - setReason(e.target.value)} - placeholder={t("tokensReasonPlaceholder")} - className="w-full px-3 py-2 rounded-lg bg-background border border-border text-sm focus:outline-none focus:ring-1 focus:ring-violet-500" - /> -
-
- {transferMsg && ( -
- {transferMsg.text} -
- )} - + {/* Transaction History */} {historyLoading ? ( -
{t("loading")}
+ ) : history.length === 0 ? ( -
{t("tokensNoTransactionsYet")}
+ ) : ( -
- - - - - - - - - - - - - {history.map((entry) => { - const isSent = entry.fromApiKeyId !== "current"; - return ( - - - - - - - - - ); - })} - -
{t("type")}{t("tokensFrom")}{t("tokensTo")}{t("tokensAmount")}{t("tokensReason")}{t("tokensDate")}
- - {isSent ? t("tokensSent") : t("tokensReceived")} - - - {entry.fromApiKeyId.slice(0, 8)}... - {entry.toApiKeyId.slice(0, 8)}... - {isSent ? "-" : "+"} - {entry.amount.toLocaleString(locale)} - {entry.reason || "-"} - {new Date(entry.createdAt).toLocaleDateString(locale)} -
-
+ + + + {t("type")} + {t("tokensFrom")} + {t("tokensTo")} + {t("tokensAmount")} + {t("tokensReason")} + {t("tokensDate")} + + + + {history.map((entry) => { + const isSent = entry.fromApiKeyId !== "current"; + return ( + + + + {isSent ? t("tokensSent") : t("tokensReceived")} + + + + {entry.fromApiKeyId.slice(0, 8)}... + + + {entry.toApiKeyId.slice(0, 8)}... + + + {isSent ? "-" : "+"} + {entry.amount.toLocaleString(locale)} + + {entry.reason || "-"} + + {new Date(entry.createdAt).toLocaleDateString(locale)} + + + ); + })} + +
)}
@@ -402,72 +411,52 @@ export default function TokensPage() {
-
- - setInviteMaxUses(e.target.value)} - min="1" - className="w-full px-3 py-2 rounded-lg bg-background border border-border text-sm focus:outline-none focus:ring-1 focus:ring-violet-500" - /> -
- +
{newInviteCode && ( -
+ setNewInviteCode(null)} + > {t("tokensInviteCreated")}:{" "} {newInviteCode} -
+ )} - {inviteError && ( -
{inviteError}
- )} + {errorMessage(inviteError, () => setInviteError(""))} {/* Redeem */}
-
- - setRedeemCode(e.target.value)} - placeholder={t("tokensRedeemCodePlaceholder")} - required - className="w-full px-3 py-2 rounded-lg bg-background border border-border text-sm focus:outline-none focus:ring-1 focus:ring-violet-500" - /> -
- +
- {redeemMsg && ( -
- {redeemMsg.text} -
- )} + {statusMessage(redeemMsg, () => setRedeemMsg(null))} {/* Active Invites */} {invites.length > 0 && ( @@ -477,7 +466,7 @@ export default function TokensPage() { {invites.map((inv) => (
{inv.code} @@ -485,16 +474,20 @@ export default function TokensPage() { {t("tokensInviteUses", { used: inv.useCount, max: inv.maxUses })} {inv.revokedAt && ( - {t("tokensRevoked")} + + {t("tokensRevoked")} + )}
{!inv.revokedAt && ( - + )}
))} @@ -509,75 +502,73 @@ export default function TokensPage() {
- setServerName(e.target.value)} placeholder={t("tokensServerNamePlaceholder")} + aria-label={t("tokensServerNamePlaceholder")} required - className="px-3 py-2 rounded-lg bg-background border border-border text-sm focus:outline-none focus:ring-1 focus:ring-violet-500" /> - setServerUrl(e.target.value)} placeholder="https://server.example.com" + aria-label="URL" required - className="px-3 py-2 rounded-lg bg-background border border-border text-sm focus:outline-none focus:ring-1 focus:ring-violet-500" /> - setServerApiKey(e.target.value)} placeholder={t("tokensApiKeyPlaceholder")} + aria-label={t("tokensApiKeyPlaceholder")} required - className="px-3 py-2 rounded-lg bg-background border border-border text-sm focus:outline-none focus:ring-1 focus:ring-violet-500" />
- +
- {serverError && ( -
{serverError}
- )} + {errorMessage(serverError, () => setServerError(""))} {servers.length === 0 ? ( -
- {t("tokensNoServersConnected")} -
+ ) : (
{servers.map((server) => (

{server.name}

- {t.has(`tokensServerStatus.${server.status}`) ? t(`tokensServerStatus.${server.status}`) : server.status} - +

{server.url}

{server.errorMessage && ( -

{server.errorMessage}

+

{server.errorMessage}

)} {server.lastSyncAt && (

@@ -587,12 +578,14 @@ export default function TokensPage() {

)}
- +
))}
From 07a9ec1c6cc6dacd921642d0f7cc5571e1421a76 Mon Sep 17 00:00:00 2001 From: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:37:31 -0300 Subject: [PATCH 02/13] refactor(dashboard): /dashboard/api-manager/routing on the CI UI Kit (KEA-7886, lote 3) --- .../api-manager/routing/RoutingPageClient.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/app/(dashboard)/dashboard/api-manager/routing/RoutingPageClient.tsx b/src/app/(dashboard)/dashboard/api-manager/routing/RoutingPageClient.tsx index d3e64bfa2ec4..df1e0acfc142 100644 --- a/src/app/(dashboard)/dashboard/api-manager/routing/RoutingPageClient.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/routing/RoutingPageClient.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { useTranslations } from "next-intl"; +import { PageHeader } from "@omniroute/ui-kit/page-header"; import ReasoningRoutingRules from "@/shared/components/ReasoningRoutingRules"; export default function RoutingPageClient() { @@ -10,13 +11,16 @@ export default function RoutingPageClient() { const searchParams = useSearchParams(); return (
-
- - ← {t("backToKeys")} - -

{t("pageTitle")}

-

{t("pageDescription")}

-
+ + ← {t("backToKeys")} + + } + title={t("pageTitle")} + description={t("pageDescription")} + /> Date: Wed, 21 Oct 2026 00:42:13 -0300 Subject: [PATCH 03/13] refactor(dashboard): /dashboard/onboarding on the CI UI Kit (KEA-7886, lote 3) --- .../onboarding/components/TierFlowDiagram.tsx | 2 +- .../(dashboard)/dashboard/onboarding/page.tsx | 302 +++++++++--------- .../steps/FreeProviderOnboardingCard.tsx | 83 +++-- .../dashboard/onboarding/steps/TierTour.tsx | 6 +- src/i18n/messages/en.json | 3 +- src/i18n/messages/pt-BR.json | 3 +- 6 files changed, 191 insertions(+), 208 deletions(-) diff --git a/src/app/(dashboard)/dashboard/onboarding/components/TierFlowDiagram.tsx b/src/app/(dashboard)/dashboard/onboarding/components/TierFlowDiagram.tsx index ded42fdf0dee..845e14282b06 100644 --- a/src/app/(dashboard)/dashboard/onboarding/components/TierFlowDiagram.tsx +++ b/src/app/(dashboard)/dashboard/onboarding/components/TierFlowDiagram.tsx @@ -19,7 +19,7 @@ export function TierFlowDiagram() { width={800} height={420} priority - className="w-full max-w-2xl rounded-lg border border-white/[0.06]" + className="w-full max-w-2xl rounded-lg border border-border" />

{t("flowCaption")} diff --git a/src/app/(dashboard)/dashboard/onboarding/page.tsx b/src/app/(dashboard)/dashboard/onboarding/page.tsx index adf2720d442d..2e6f2bcebc22 100644 --- a/src/app/(dashboard)/dashboard/onboarding/page.tsx +++ b/src/app/(dashboard)/dashboard/onboarding/page.tsx @@ -3,6 +3,17 @@ import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; +import { Alert } from "@omniroute/ui-kit/alert"; +import { CodePreview } from "@omniroute/ui-kit/code-preview"; +import { ChoiceCard } from "@omniroute/ui-kit/onboarding-patterns"; +import { RadioGroup } from "@omniroute/ui-kit/radio-group"; +import Button from "@/shared/components/Button"; +import Card from "@/shared/components/Card"; +import Checkbox from "@/shared/components/Checkbox"; +import Icon from "@/shared/components/Icon"; +import Input from "@/shared/components/Input"; +import { Spinner } from "@/shared/components/Loading"; +import { cn } from "@/shared/utils/cn"; import { useDisplayBaseUrl } from "@/shared/hooks"; import { FreeProviderOnboardingCard } from "./steps/FreeProviderOnboardingCard"; import { TierTour } from "./steps/TierTour"; @@ -32,7 +43,6 @@ export default function OnboardingWizard() { const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [skipSecurity, setSkipSecurity] = useState(false); - const [capsLockOn, setCapsLockOn] = useState(false); // #14296: fresh Docker/NAT-forwarded installs (peer isn't 127.0.0.1) hit a // 401 on the bootstrap writes below until the operator supplies the @@ -260,7 +270,7 @@ export default function OnboardingWizard() { if (loading) { return (

-
{tc("loading")}
+
); } @@ -269,46 +279,51 @@ export default function OnboardingWizard() {
{/* Progress Indicator */} -
+
    {STEPS.map((s, i) => ( -
    -
    + - {i < step ? ( - check - ) : ( - i + 1 + : "bg-surface-2 text-text-muted" )} -
    + > + {i < step ? : i + 1} + {s.title} + {i < STEPS.length - 1 && ( - + ))} -
    +
{/* Card */} -
+ {/* Step Header */}
- - {currentStep.icon} - +

{currentStep.title}

{currentStep.id === "tiers" && (

@@ -322,27 +337,32 @@ export default function OnboardingWizard() { which step's write actually failed (security step or the Finish/Skip-wizard buttons). */} {errorMessage && ( -

-

{errorMessage}

+ setErrorMessage("")} + > +

{errorMessage}

{needsBootstrapToken && (
- setBootstrapToken(e.target.value)} - className="w-full px-4 py-2.5 bg-white/[0.04] border border-white/10 rounded-lg text-text-main text-sm placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/40" /> - +
)} -
+ )} {/* Step Content */} @@ -359,12 +379,10 @@ export default function OnboardingWizard() { ].map((f) => (
- - {f.icon} - + {f.label}
@@ -380,51 +398,38 @@ export default function OnboardingWizard() { {currentStep.id === "security" && (

{t("securityDesc")}

- + setSkipSecurity(e.target.checked)} + /> {skipSecurity && ( -

+

{t("securityDescSkipWarning")}

)} {!skipSecurity && (
- setPassword(e.target.value)} - onKeyDown={(e) => setCapsLockOn(e.getModifierState("CapsLock"))} - onKeyUp={(e) => setCapsLockOn(e.getModifierState("CapsLock"))} - className="w-full px-4 py-2.5 bg-white/[0.04] border border-white/10 rounded-lg text-text-main text-sm placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/40" /> - setConfirmPassword(e.target.value)} - onKeyDown={(e) => setCapsLockOn(e.getModifierState("CapsLock"))} - onKeyUp={(e) => setCapsLockOn(e.getModifierState("CapsLock"))} - className="w-full px-4 py-2.5 bg-white/[0.04] border border-white/10 rounded-lg text-text-main text-sm placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/40" + error={ + password && confirmPassword && password !== confirmPassword + ? t("passwordsMismatch") + : undefined + } /> - {capsLockOn && ( -

- - Caps Lock is on -

- )} - {password && confirmPassword && password !== confirmPassword && ( -

{t("passwordsMismatch")}

- )}
)}
@@ -435,53 +440,58 @@ export default function OnboardingWizard() {

{t("providerDesc")}

{skipSecurity && ( -
-

{t("providerRequiresPassword")}

-
+

+ {t("providerRequiresPassword")} +

)} {!skipSecurity && } {!skipSecurity && (
- + {t("freeProviders.orUseApiKey")} - +
)} {!skipSecurity && ( -
+ { + const provider = COMMON_PROVIDERS.find((p) => p.id === id); + setSelectedProvider(id); + if (provider) setProviderName(provider.name); + }} + className="[&_.ci-radio-group__items]:grid-cols-2 sm:[&_.ci-radio-group__items]:grid-cols-3 [&_.ci-radio-group__items]:gap-2" + > {COMMON_PROVIDERS.map((p) => ( - + value={p.id} + title={p.name} + tone="accent" + className="text-xs" + /> ))} -
+ )} {!skipSecurity && selectedProvider && (
- setProviderKey(e.target.value)} - className="w-full px-4 py-2.5 bg-white/[0.04] border border-white/10 rounded-lg text-text-main text-sm placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/40" /> - setProviderUrl(e.target.value)} - className="w-full px-4 py-2.5 bg-white/[0.04] border border-white/10 rounded-lg text-text-main text-sm placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/40" />
)} @@ -493,39 +503,31 @@ export default function OnboardingWizard() {

{t("testDesc")}

{testStatus === "idle" && ( - + )} {testStatus === "testing" && (
- - progress_activity - + {testMessage}
)} {testStatus === "success" && ( -
- check_circle +
+ {testMessage}
)} {testStatus === "error" && (
-
- warning +
+ {testMessage}
- +
)}
@@ -535,98 +537,82 @@ export default function OnboardingWizard() { {currentStep.id === "done" && (

{t("doneDesc")}

-
-

{t("yourEndpoint")}

- {apiEndpoint} -
+
)}
{/* Footer Actions */} -
+
{step > 0 && !isLastStep && ( - + )}
{!isLastStep && step > 0 && ( - + )} {currentStep.id === "welcome" && ( - + )} {currentStep.id === "tiers" && ( - + )} {currentStep.id === "security" && ( - + )} {currentStep.id === "provider" && !skipSecurity ? ( - + ) : null} {currentStep.id === "test" && ( - + )} {isLastStep && ( - + )}
-
+ {/* Skip Wizard */} {!isLastStep && (
- +
)}
diff --git a/src/app/(dashboard)/dashboard/onboarding/steps/FreeProviderOnboardingCard.tsx b/src/app/(dashboard)/dashboard/onboarding/steps/FreeProviderOnboardingCard.tsx index 2522275a5e93..3c352790ff40 100644 --- a/src/app/(dashboard)/dashboard/onboarding/steps/FreeProviderOnboardingCard.tsx +++ b/src/app/(dashboard)/dashboard/onboarding/steps/FreeProviderOnboardingCard.tsx @@ -2,6 +2,8 @@ import { useEffect, useMemo, useState } from "react"; import { useTranslations } from "next-intl"; +import { Checkbox } from "@omniroute/ui-kit/checkbox"; +import Button from "@/shared/components/Button"; interface FreeProviderOption { id: string; @@ -102,47 +104,40 @@ export function FreeProviderOnboardingCard() {
{providers.map((provider) => ( - + card + checked={selectedIds.includes(provider.id)} + onChange={() => toggleProvider(provider.id)} + label={{provider.name}} + description={ + <> + + {provider.caution} + + event.stopPropagation()} + > + {t("reviewProviderSite")} + + + } + /> ))}
- + setConfirmed(event.target.checked)} + label={{t("confirmation")}} + /> - {error &&

{error}

} + {error &&

{error}

} {results.length > 0 && (
    {results.map((result) => ( @@ -154,24 +149,24 @@ export function FreeProviderOnboardingCard() { )}
    - + {failedIds.length > 0 && ( - + )}
    diff --git a/src/app/(dashboard)/dashboard/onboarding/steps/TierTour.tsx b/src/app/(dashboard)/dashboard/onboarding/steps/TierTour.tsx index c09276df3dd0..ec2b2e7e07e2 100644 --- a/src/app/(dashboard)/dashboard/onboarding/steps/TierTour.tsx +++ b/src/app/(dashboard)/dashboard/onboarding/steps/TierTour.tsx @@ -39,21 +39,21 @@ export function TierTour() {
    Date: Wed, 21 Oct 2026 00:57:08 -0300 Subject: [PATCH 04/13] refactor(dashboard): /dashboard/api-manager on the CI UI Kit (KEA-7886, lote 3) --- .../api-manager/ApiManagerPageClient.tsx | 578 ++++++++---------- .../components/AllowedCombosSection.tsx | 63 +- .../components/ApiKeyAutoCombosToggle.tsx | 26 +- .../components/ApiKeyCatalogScopeSelect.tsx | 35 +- .../components/ApiKeyCompressionToggle.tsx | 26 +- .../components/ApiKeyFilterBar.tsx | 30 +- .../components/ApiKeyFilterChip.tsx | 17 +- .../components/BypassProviderQuotaToggle.tsx | 26 +- .../components/ChaosModeAccessToggle.tsx | 26 +- .../ProviderConnectionPermissionList.tsx | 107 ++-- .../ProviderModelPermissionList.tsx | 84 +-- .../components/UsageLimitSettings.tsx | 70 +-- tests/e2e/api-keys-flow.spec.ts | 3 +- 13 files changed, 515 insertions(+), 576 deletions(-) diff --git a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx index 1896ddfddbd9..2185f3373bd5 100644 --- a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx @@ -1,8 +1,25 @@ "use client"; +import Icon from "@/shared/components/Icon"; + import { useState, useEffect, useMemo, useCallback, useRef, useId } from "react"; import Link from "next/link"; -import { Card, Button, Input, Modal, CardSkeleton } from "@/shared/components"; +import { Alert } from "@omniroute/ui-kit/alert"; +import { PageHeader } from "@omniroute/ui-kit/page-header"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@omniroute/ui-kit/table"; +import Button from "@/shared/components/Button"; +import Card from "@/shared/components/Card"; +import Input from "@/shared/components/Input"; +import { CardSkeleton } from "@/shared/components/Loading"; +import Modal from "@/shared/components/Modal"; +import Toggle from "@/shared/components/Toggle"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { useLocale, useTranslations } from "next-intl"; import { matchesSearch } from "@/shared/utils/turkishText"; @@ -588,54 +605,42 @@ export default function ApiManagerPageClient() {
    {/* Error Banner */} {pageError && ( -
    - error -

    {pageError}

    - -
    + } + description={pageError} + dismissLabel={tc("close")} + onDismiss={clearPageError} + /> )} -
    -
    -
    -

    {t("keyManagement")}

    -

    {t("keyManagementDesc")}

    -
    -
    - - {t("requestFlowYourApp")} - - - - {t("requestFlowApiKey")} - - - - {t("requestFlowOmniRoute")} - -
    +
    + setShowAddModal(true)} icon="add" className="shrink-0"> + {t("createKey")} + + } + /> +
    + + {t("requestFlowYourApp")} + + + + {t("requestFlowApiKey")} + + + + {t("requestFlowOmniRoute")} +
    -
    @@ -659,8 +664,8 @@ export default function ApiManagerPageClient() {
    -
    - vpn_key +
    +

    @@ -701,7 +706,7 @@ export default function ApiManagerPageClient() { {keys.length === 0 ? (
    - vpn_key +

    {t("noKeys")}

    {t("noKeysDesc")}

    @@ -719,7 +724,7 @@ export default function ApiManagerPageClient() { ) : filteredKeys.length === 0 ? (
    - search_off +

    {t("emptyFilterTitle")}

    @@ -765,62 +770,61 @@ export default function ApiManagerPageClient() { const visibleGroups = groups.slice(0, 3); const extraGroupCount = groups.length - visibleGroups.length; return ( -
    -
    - - {isRestricted ? "lock" : "lock_open"} - - - {key.name} - -
    -
    - - {visibleKeys.has(key.id) ? (revealedKeys.get(key.id) ?? key.key) : key.key} - - {allowKeyReveal ? ( - <> - - - - ) : ( - - lock + + +
    + + + {key.name} - )} -
    -
    +
    +
    + +
    + + {visibleKeys.has(key.id) ? (revealedKeys.get(key.id) ?? key.key) : key.key} + + {allowKeyReveal ? ( + <> +
    +
    +
    {/* QUOTA differentiation chips — prepended before existing badges */} {keyIsQuota && ( - + {t("quotaModeOnly")} )} @@ -828,13 +832,13 @@ export default function ApiManagerPageClient() { visibleGroups.map((groupName) => ( {groupName} ))} {keyIsQuota && extraGroupCount > 0 && ( - + +{extraGroupCount} )} @@ -842,209 +846,210 @@ export default function ApiManagerPageClient() { {isModelRestricted ? ( - lock + {formatProviderModelPermissionSummary(providerCount, modelCount, t, tc)} ) : ( - lock_open + {t("allModels")} )} {hasConnectionRestrictions && ( - cable + {key.allowedConnections!.length} conn )} {hasExclusiveLeaseScope && ( - - key_vertical - + {t("exclusiveLease")} )} {hasComboRestrictions && ( - hub + {key.allowedCombos!.length} combos )} {noLogEnabled && ( - - - visibility_off - + + No-Log )} {key.autoResolve && ( - - - auto_fix_high - + + Auto-Resolve )} {hasJsonStreamDefault && ( - - data_object + + {t("streamDefaultBadge")} )} {hasLocalUsageCommand && ( - - terminal + + {t("localUsageCommandBadge")} )} {key.usageLimitEnabled === true && ( - - paid + + USD quota )} {hasProviderQuotaBypass && ( - - alt_route + + Bypass quota policy )} {hasSessionLimit && ( - - group + + Sessions: {activeSessions}/{maxSessions} )} {deviceCount > 0 && ( - devices + {t("devicesCount", { count: deviceCount })} )} {hasThrottle && ( - - speed+ - {throttleDelayMs}ms + + +{throttleDelayMs}ms )} {hasManageScope && ( - - - admin_panel_settings - + + manage )} {!keyIsActive && ( - - block + + {t("disabled")} )} {hasSchedule && ( - - schedule + + {t("scheduleActive")} )} {key.isBanned && ( - - gavel + + BANNED )} {key.expiresAt && new Date(key.expiresAt).getTime() < Date.now() && ( - - event_busy + + EXPIRED )}
    -
    -
    - - {stats?.totalRequests ?? 0}{" "} - {t("reqs")} - - {(stats?.totalRequests ?? 0) > 0 && ( - - {formatUsdCost(stats?.totalCost ?? 0, locale)} + + +
    + + {stats?.totalRequests ?? 0}{" "} + {t("reqs")} - )} - {stats?.lastUsed ? ( - - {t("lastUsedOn", { date: new Date(stats.lastUsed).toLocaleDateString() })} - - ) : ( - {t("neverUsed")} - )} -
    -
    + {(stats?.totalRequests ?? 0) > 0 && ( + + {formatUsdCost(stats?.totalCost ?? 0, locale)} + + )} + {stats?.lastUsed ? ( + + {t("lastUsedOn", { date: new Date(stats.lastUsed).toLocaleDateString() })} + + ) : ( + {t("neverUsed")} + )} +
    +
    + {new Date(key.createdAt).toLocaleDateString()} -
    -
    - - payments - - - - tune - - -
    -
    + + +
    + + + +
    +
    + ); }; const tableHeader = ( -
    -
    {t("name")}
    -
    {t("key")}
    -
    {t("permissions")}
    -
    {t("usage")}
    -
    {t("created")}
    -
    {t("actions")}
    -
    + + + {t("name")} + {t("key")} + {t("permissions")} + {t("usage")} + {t("created")} + {t("actions")} + + ); return ( @@ -1053,9 +1058,7 @@ export default function ApiManagerPageClient() {
    {/* Normal keys section heading */}
    - - vpn_key - + {t("normalKeysSection")} @@ -1063,37 +1066,31 @@ export default function ApiManagerPageClient() { {normalKeys.length}
    -
    -
    - {tableHeader} - {normalKeys.map(renderKeyRow)} -
    -
    + + {tableHeader} + {normalKeys.map(renderKeyRow)} +
    )} {quotaKeys.length > 0 && (
    {/* Quota keys section heading */}
    - - toll - + {t("quotaKeysSection")} {quotaKeys.length} - + {t("quotaPill")}
    -
    -
    - {tableHeader} - {quotaKeys.map(renderKeyRow)} -
    -
    + + {tableHeader} + {quotaKeys.map(renderKeyRow)} +
    )}
    @@ -1120,10 +1117,8 @@ export default function ApiManagerPageClient() { >
    - { @@ -1134,28 +1129,21 @@ export default function ApiManagerPageClient() { maxLength={MAX_KEY_NAME_LENGTH} error={nameError} autoFocus + hint={t("keyNameDesc")} /> -

    {t("keyNameDesc")}

    {t("managementAccess")}

    {t("managementAccessDesc")}

    - +
    @@ -1167,73 +1155,55 @@ export default function ApiManagerPageClient() {

    {t("ownUsageVisibility")}

    {t("ownUsageVisibilityDesc")}

    - + ariaLabel={t("ownUsageVisibility")} + />

    {t("sharedAccountQuotaVisibility")}

    {t("sharedAccountQuotaVisibilityDesc")}

    - + onChange={setNewKeyAccountQuotaEnabled} + ariaLabel={t("sharedAccountQuotaVisibility")} + />

    {t("localUsageCommand")}

    {t("localUsageCommandDesc")}

    - +
    {createError && ( -
    - error -

    {createError}

    -
    + } + description={createError} + dismissLabel={tc("close")} + onDismiss={() => setCreateError(null)} + /> )}
    - +
    ); } @@ -56,28 +59,30 @@ function ComboOptionRow({ onToggle: (comboName: string) => void; }) { return ( - + ); } @@ -100,7 +105,7 @@ function PreservedComboRules({ rules }: { rules: string[] }) { title={t("preservedComboRuleHint")} className="inline-flex items-center gap-1 px-2 py-0.5 rounded border border-border bg-surface text-[11px] text-text-muted" > - lock + {rule} diff --git a/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyAutoCombosToggle.tsx b/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyAutoCombosToggle.tsx index d9a42ef07d0c..b134b3bd7e6f 100644 --- a/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyAutoCombosToggle.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyAutoCombosToggle.tsx @@ -1,5 +1,7 @@ "use client"; +import Toggle from "@/shared/components/Toggle"; + import { useTranslations } from "next-intl"; /** @@ -18,7 +20,6 @@ export function ApiKeyAutoCombosToggle({ onToggle: () => void; }) { const tSettings = useTranslations("settings"); - const tc = useTranslations("common"); return (
    @@ -26,22 +27,13 @@ export function ApiKeyAutoCombosToggle({

    {tSettings("autoCombosTitle")}

    {tSettings("autoCombosDesc")}

    - + onToggle()} + ariaLabel={tSettings("autoCombosTitle")} + />
    ); } diff --git a/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyCatalogScopeSelect.tsx b/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyCatalogScopeSelect.tsx index 73bdee89fd5e..fd414f66e24f 100644 --- a/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyCatalogScopeSelect.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyCatalogScopeSelect.tsx @@ -1,6 +1,9 @@ "use client"; +import Icon from "@/shared/components/Icon"; + import { useTranslations } from "next-intl"; +import { RadioGroup, RadioGroupItem } from "@omniroute/ui-kit/radio-group"; export type CatalogScope = "all" | "combos" | "models"; @@ -32,25 +35,25 @@ export function ApiKeyCatalogScopeSelect({

    {tSettings("catalogScopeTitle")}

    {tSettings("catalogScopeDesc")}

    -
    + onChange(next as CatalogScope)} + > {OPTIONS.map((opt) => ( - + value={opt.value} + label={ + + + {tSettings(opt.labelKey)} + + } + /> ))} -
    +
    ); } diff --git a/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyCompressionToggle.tsx b/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyCompressionToggle.tsx index ed912ee92ca6..1c53cb66cb0d 100644 --- a/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyCompressionToggle.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyCompressionToggle.tsx @@ -1,5 +1,7 @@ "use client"; +import Toggle from "@/shared/components/Toggle"; + import { useTranslations } from "next-intl"; /** Per-key prompt-compression policy used by the API key permissions modal. */ @@ -11,7 +13,6 @@ export function ApiKeyCompressionToggle({ onToggle: () => void; }) { const tSettings = useTranslations("settings"); - const tc = useTranslations("common"); return (
    @@ -19,22 +20,13 @@ export function ApiKeyCompressionToggle({

    {tSettings("compressionTitle")}

    {tSettings("compressionDesc")}

    - + onToggle()} + ariaLabel={tSettings("compressionTitle")} + />
    ); } diff --git a/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyFilterBar.tsx b/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyFilterBar.tsx index 81a0546648e0..1801643b1b4a 100644 --- a/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyFilterBar.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyFilterBar.tsx @@ -1,7 +1,10 @@ "use client"; import { useTranslations } from "next-intl"; -import { Card, Input, Toggle } from "@/shared/components"; +import Button from "@/shared/components/Button"; +import Card from "@/shared/components/Card"; +import Input from "@/shared/components/Input"; +import Toggle from "@/shared/components/Toggle"; import ApiKeyFilterChip from "./ApiKeyFilterChip"; import type { KeyStatus, KeyType, ApiKeyCounts } from "../apiManagerPageUtils"; @@ -41,25 +44,25 @@ export default function ApiKeyFilterBar({ { value: "active", label: t("filterStatusActive"), - dotColor: "bg-green-500", + dotColor: "bg-success", count: counts.active, }, { value: "disabled", label: t("filterStatusDisabled"), - dotColor: "bg-gray-500", + dotColor: "bg-text-muted", count: counts.disabled, }, { value: "banned", label: t("filterStatusBanned"), - dotColor: "bg-red-500", + dotColor: "bg-error", count: counts.banned, }, { value: "expired", label: t("filterStatusExpired"), - dotColor: "bg-amber-500", + dotColor: "bg-warning", count: counts.expired, }, ]; @@ -74,19 +77,19 @@ export default function ApiKeyFilterBar({ { value: "standard", label: t("filterTypeStandard"), - dotColor: "bg-slate-500", + dotColor: "bg-text-muted", count: counts.standard, }, { value: "manage", label: t("filterTypeManage"), - dotColor: "bg-rose-500", + dotColor: "bg-error", count: counts.manage, }, { value: "restricted", label: t("filterTypeRestricted"), - dotColor: "bg-amber-500", + dotColor: "bg-warning", count: counts.restricted, }, ]; @@ -106,13 +109,14 @@ export default function ApiKeyFilterBar({ inputClassName={searchQuery ? "pr-9" : ""} /> {searchQuery && ( - + icon="close" + /> )}
    {dotColor && } {label} {count !== undefined && ( - + {count} )} - + ); } diff --git a/src/app/(dashboard)/dashboard/api-manager/components/BypassProviderQuotaToggle.tsx b/src/app/(dashboard)/dashboard/api-manager/components/BypassProviderQuotaToggle.tsx index 27dc90964146..38f7d93d506f 100644 --- a/src/app/(dashboard)/dashboard/api-manager/components/BypassProviderQuotaToggle.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/components/BypassProviderQuotaToggle.tsx @@ -1,5 +1,7 @@ "use client"; +import Toggle from "@/shared/components/Toggle"; + import { useTranslations } from "next-intl"; /** @@ -15,29 +17,21 @@ export function BypassProviderQuotaToggle({ enabled: boolean; onToggle: () => void; }) { - const tc = useTranslations("common"); const t = useTranslations("apiManager"); return ( -
    +

    {t("bypassProviderQuota")}

    {t("bypassProviderQuotaDescription")}

    - + onToggle()} + ariaLabel={t("bypassProviderQuota")} + />
    ); } diff --git a/src/app/(dashboard)/dashboard/api-manager/components/ChaosModeAccessToggle.tsx b/src/app/(dashboard)/dashboard/api-manager/components/ChaosModeAccessToggle.tsx index 30cf2d228e55..35412935083b 100644 --- a/src/app/(dashboard)/dashboard/api-manager/components/ChaosModeAccessToggle.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/components/ChaosModeAccessToggle.tsx @@ -1,5 +1,7 @@ "use client"; +import Toggle from "@/shared/components/Toggle"; + import { useTranslations } from "next-intl"; /** @@ -18,28 +20,20 @@ export function ChaosModeAccessToggle({ onToggle: () => void; }) { const tChaos = useTranslations("chaosConfig"); - const tc = useTranslations("common"); return ( -
    +

    {tChaos("keyPermission")}

    {tChaos("keyPermissionDesc")}

    - + onToggle()} + ariaLabel={tChaos("keyPermission")} + />
    ); } diff --git a/src/app/(dashboard)/dashboard/api-manager/components/ProviderConnectionPermissionList.tsx b/src/app/(dashboard)/dashboard/api-manager/components/ProviderConnectionPermissionList.tsx index 2ed1556b8f10..e9f3f7e8e6f0 100644 --- a/src/app/(dashboard)/dashboard/api-manager/components/ProviderConnectionPermissionList.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/components/ProviderConnectionPermissionList.tsx @@ -1,9 +1,13 @@ "use client"; -import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { memo, useCallback, useMemo, useState } from "react"; import { useTranslations } from "next-intl"; +import { Checkbox } from "@omniroute/ui-kit/checkbox"; import { getProviderDisplayName } from "@/lib/display/names"; -import { Input } from "@/shared/components"; +import Button from "@/shared/components/Button"; +import Icon from "@/shared/components/Icon"; +import Input from "@/shared/components/Input"; +import { cn } from "@/shared/utils/cn"; import { compareTr, matchesSearch } from "@/shared/utils/turkishText"; export interface ProviderConnection { @@ -33,19 +37,13 @@ function ProviderSelectionCheckbox({ indeterminate: boolean; onChange: () => void; }) { - const inputRef = useRef(null); - useEffect(() => { - if (inputRef.current) inputRef.current.indeterminate = indeterminate; - }, [indeterminate]); - return ( - ); } @@ -220,20 +218,21 @@ export const ProviderConnectionPermissionList = memo(function ProviderConnection icon="search" /> {search && ( - + className="absolute inset-y-0 right-1 my-auto text-text-muted" + aria-label={tc("clear")} + icon="close" + /> )}
    {filteredGroups.length === 0 ? (
    - search_off +

    {tc("noResults")}

    ) : ( @@ -253,20 +252,20 @@ export const ProviderConnectionPermissionList = memo(function ProviderConnection return (
    - + +
    handleToggleAllInProvider(provider, conns)} /> - + {totalCount} @@ -297,37 +297,36 @@ export const ProviderConnectionPermissionList = memo(function ProviderConnection {conns.map((conn) => { const isSelected = safeSelectedConnections.includes(conn.id); return ( - + ); })}
    diff --git a/src/app/(dashboard)/dashboard/api-manager/components/ProviderModelPermissionList.tsx b/src/app/(dashboard)/dashboard/api-manager/components/ProviderModelPermissionList.tsx index 8b94eaa3dc48..701ea1ac1c85 100644 --- a/src/app/(dashboard)/dashboard/api-manager/components/ProviderModelPermissionList.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/components/ProviderModelPermissionList.tsx @@ -1,9 +1,13 @@ "use client"; -import { memo, useCallback, useEffect, useMemo, useRef } from "react"; +import { memo, useCallback, useMemo } from "react"; import { useTranslations } from "next-intl"; +import { Checkbox } from "@omniroute/ui-kit/checkbox"; import { getProviderDisplayName } from "@/lib/display/names"; -import { Input } from "@/shared/components"; +import Button from "@/shared/components/Button"; +import Icon from "@/shared/components/Icon"; +import Input from "@/shared/components/Input"; +import { cn } from "@/shared/utils/cn"; import { isModelIndividuallyToggleable, restoreProviderScopeSelection, @@ -66,19 +70,13 @@ function ProviderSelectionCheckbox({ indeterminate: boolean; onChange: () => void; }) { - const inputRef = useRef(null); - useEffect(() => { - if (inputRef.current) inputRef.current.indeterminate = indeterminate; - }, [indeterminate]); - return ( - ); } @@ -96,6 +94,7 @@ const ProviderModelPermissionList = memo(function ProviderModelPermissionList({ onClaudeCodeDefaultDeselected, }: ProviderModelPermissionListProps) { const t = useTranslations("apiManager"); + const tc = useTranslations("common"); const providerModelRefs = useMemo(() => allModels.map(getModelRef), [allModels]); @@ -180,20 +179,21 @@ const ProviderModelPermissionList = memo(function ProviderModelPermissionList({ icon="search" /> {searchModel && ( - + className="absolute inset-y-0 right-1 my-auto text-text-muted" + aria-label={tc("clear")} + icon="close" + /> )}
    {modelsByProvider.length === 0 ? (
    - search_off +

    {t("noModelsFound")}

    ) : ( @@ -215,20 +215,23 @@ const ProviderModelPermissionList = memo(function ProviderModelPermissionList({ return (
    - + +
    - + {models.length} @@ -270,20 +274,18 @@ const ProviderModelPermissionList = memo(function ProviderModelPermissionList({ { ownedBy: model.owned_by } ); return ( - + ); })}
    diff --git a/src/app/(dashboard)/dashboard/api-manager/components/UsageLimitSettings.tsx b/src/app/(dashboard)/dashboard/api-manager/components/UsageLimitSettings.tsx index abbd9d9f3ed5..a8dce2cbef8c 100644 --- a/src/app/(dashboard)/dashboard/api-manager/components/UsageLimitSettings.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/components/UsageLimitSettings.tsx @@ -1,14 +1,14 @@ "use client"; -import { Input } from "@/shared/components"; +import Toggle from "@/shared/components/Toggle"; + +import Input from "@/shared/components/Input"; import { useTranslations } from "next-intl"; export function UsageLimitSettings({ enabled, dailyLimitUsd, weeklyLimitUsd, - enabledLabel, - disabledLabel, onEnabledChange, onDailyLimitUsdChange, onWeeklyLimitUsdChange, @@ -16,6 +16,7 @@ export function UsageLimitSettings({ enabled: boolean; dailyLimitUsd: string; weeklyLimitUsd: string; + /** Kept for callers; the switch state is conveyed by the kit Switch itself. */ enabledLabel: string; disabledLabel: string; onEnabledChange: (enabled: boolean) => void; @@ -24,50 +25,39 @@ export function UsageLimitSettings({ }) { const t = useTranslations("usageLimits"); return ( -
    +

    {t("usdUsageQuota")}

    {t("usdUsageQuotaDescription")}

    - +
    -
    - - onDailyLimitUsdChange(event.target.value)} - placeholder="0.00" - /> -
    -
    - - onWeeklyLimitUsdChange(event.target.value)} - placeholder="0.00" - /> -
    + onDailyLimitUsdChange(event.target.value)} + placeholder="0.00" + /> + onWeeklyLimitUsdChange(event.target.value)} + placeholder="0.00" + />

    {t("quotaWindowDescription")}

    diff --git a/tests/e2e/api-keys-flow.spec.ts b/tests/e2e/api-keys-flow.spec.ts index 336d84fe956a..2c6eae6f72ac 100644 --- a/tests/e2e/api-keys-flow.spec.ts +++ b/tests/e2e/api-keys-flow.spec.ts @@ -292,7 +292,8 @@ test.describe("API keys flow", () => { await expect(inlineError).toBeInViewport(); // No page-level error banner should appear behind the modal - const pageErrorBanner = page.locator('.flex.flex-col.gap-8 > [class~="bg-red-500/10"]'); + // KEA-7886: the page-level banner is the kit Alert (critical variant). + const pageErrorBanner = page.locator('.flex.flex-col.gap-8 > [data-slot="alert"]'); await expect(pageErrorBanner).not.toBeVisible(); // Typing should clear the error From e667a8c9d6701451d3904c2e0fe183167fa3fa3c Mon Sep 17 00:00:00 2001 From: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:57:37 -0300 Subject: [PATCH 05/13] refactor(dashboard): /dashboard/api-manager/[id]/access on the CI UI Kit (KEA-7886, lote 3) --- .../[id]/access/ApiKeyAccessEditorClient.tsx | 216 ++++++++---------- .../[id]/access/tabs/BehaviourTab.tsx | 106 ++++----- .../[id]/access/tabs/ConnectionsTab.tsx | 29 ++- .../[id]/access/tabs/GeneralTab.tsx | 208 +++++++---------- .../[id]/access/tabs/LimitsTab.tsx | 128 +++++------ .../[id]/access/tabs/ModelsTab.tsx | 185 ++++++++------- .../[id]/access/tabs/TabErrorList.tsx | 10 +- src/i18n/messages/en.json | 3 +- src/i18n/messages/pt-BR.json | 3 +- tests/e2e/api-key-model-scope.spec.ts | 15 +- tests/unit/api-manager-page-static.test.ts | 28 +-- 11 files changed, 411 insertions(+), 520 deletions(-) diff --git a/src/app/(dashboard)/dashboard/api-manager/[id]/access/ApiKeyAccessEditorClient.tsx b/src/app/(dashboard)/dashboard/api-manager/[id]/access/ApiKeyAccessEditorClient.tsx index d5638233d55f..427e22d9b1b2 100644 --- a/src/app/(dashboard)/dashboard/api-manager/[id]/access/ApiKeyAccessEditorClient.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/[id]/access/ApiKeyAccessEditorClient.tsx @@ -1,10 +1,17 @@ "use client"; +import Icon from "@/shared/components/Icon"; + import { useState, useEffect, useMemo, useCallback, useRef } from "react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { useTranslations } from "next-intl"; -import { Card, Button, CardSkeleton } from "@/shared/components"; +import { buttonVariants } from "@omniroute/ui-kit/button"; +import { PageHeader } from "@omniroute/ui-kit/page-header"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@omniroute/ui-kit/tabs"; +import Button from "@/shared/components/Button"; +import Card from "@/shared/components/Card"; +import { CardSkeleton } from "@/shared/components/Loading"; import { useNotificationStore } from "@/store/notificationStore"; import { getProviderDisplayName } from "@/lib/display/names"; import { compareTr, matchesSearch } from "@/shared/utils/turkishText"; @@ -408,100 +415,79 @@ function AccessBehaviourPanelPanelContent({ context }: { context: AccessFormCont function AccessHeader({ context }: { context: AccessFormContext }) { const { apiKey, t } = context; return ( -
    -
    - - - {t("keyManagement")} - - / - {apiKey.name} - / - {t("accessBreadcrumb")} -
    -
    -
    -

    - - {t("accessEditorTitle", { name: apiKey.name })} -

    -

    {t("accessEditorDesc")}

    + + + + {t("keyManagement")} + + / + {apiKey.name} + / + {t("accessBreadcrumb")}
    -
    -
    + } + title={t("accessEditorTitle", { name: apiKey.name })} + description={t("accessEditorDesc")} + /> ); } function AccessTabBar({ context }: { context: AccessFormContext }) { const { form, t, tabListRef, activeTab, switchTab, handleTabKeyDown } = context; return ( -
    -
    - {TABS.map((tabDef, index) => { - const isSelected = activeTab === tabDef.id; - const errorCount = form.getTabErrorCount(tabDef.id); - - return ( - - ); - })} -
    -
    + {errorCount > 0 && " "} + {errorCount > 0 && ( + + + {t("errorBadgeLabel", { count: errorCount })} + + )} + + ); + })} + ); } function AccessPanel({ context }: { context: AccessFormContext }) { const { activeTab, isSubmitting } = context; return ( -
    @@ -525,7 +511,7 @@ function AccessPanel({ context }: { context: AccessFormContext }) { {activeTab === "behaviour" && } -
    + ); } @@ -536,27 +522,20 @@ function AccessSaveBar({ context }: { context: AccessFormContext }) {
    {form.isDirty ? ( - - + + {t("unsavedChanges")} ) : ( - + {ts("saved")} )} {form.hasErrors && ( - - + + {t("fixValidationErrors")} )} @@ -576,16 +555,10 @@ function AccessSaveBar({ context }: { context: AccessFormContext }) { type="button" onClick={handleSave} disabled={!form.isDirty || isSubmitting || form.hasErrors} + loading={isSubmitting} className="flex-1 sm:flex-none" > - {isSubmitting ? ( - - - {ts("saving")} - - ) : ( - t("saveChanges") - )} + {isSubmitting ? {ts("saving")} : t("saveChanges")}
    @@ -597,8 +570,15 @@ function ApiKeyAccessEditorForm(props: ApiKeyAccessEditorFormProps) { return (
    - - + context.switchTab(next as typeof context.activeTab)} + className="flex flex-col gap-6" + > + + +
    ); @@ -841,22 +821,16 @@ export default function ApiKeyAccessEditorClient({ apiKeyId }: ApiKeyAccessEdito if (notFound) { return ( -
    - +
    +

    {t("keyNotFound")}

    {t("keyNotFoundDesc")}

    - - + + + {t("backToApiKeys")} ); @@ -866,21 +840,15 @@ export default function ApiKeyAccessEditorClient({ apiKeyId }: ApiKeyAccessEdito if (fetchError || !apiKey) { return ( -
    - +
    +

    {fetchError || t("failedLoadKey")}

    - - + + + {t("backToApiKeys")} ); diff --git a/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/BehaviourTab.tsx b/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/BehaviourTab.tsx index 0c09b65ac198..68f9196b055a 100644 --- a/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/BehaviourTab.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/BehaviourTab.tsx @@ -1,5 +1,9 @@ "use client"; +import Button from "@/shared/components/Button"; +import Icon from "@/shared/components/Icon"; +import Toggle from "@/shared/components/Toggle"; + import { useTranslations } from "next-intl"; import { ApiKeyCompressionToggle } from "@/app/(dashboard)/dashboard/api-manager/components/ApiKeyCompressionToggle"; import { ChaosModeAccessToggle } from "@/app/(dashboard)/dashboard/api-manager/components/ChaosModeAccessToggle"; @@ -26,7 +30,6 @@ type BehaviourSectionProps = Pick< function PayloadPrivacyToggle({ formState, setNoLog }: BehaviourSectionProps<"setNoLog">) { const t = useTranslations("apiManager"); - const tc = useTranslations("common"); return (
    @@ -36,29 +39,19 @@ function PayloadPrivacyToggle({ formState, setNoLog }: BehaviourSectionProps<"se Disable request/response payload persistence for this API key.

    - + setNoLog(!formState.noLog)} + ariaLabel={t("noLogPayloadPrivacy")} + />
    ); } function AutoResolveToggle({ formState, setAutoResolve }: BehaviourSectionProps<"setAutoResolve">) { const t = useTranslations("apiManager"); - const tc = useTranslations("common"); return (
    @@ -66,22 +59,13 @@ function AutoResolveToggle({ formState, setAutoResolve }: BehaviourSectionProps<

    {t("autoResolve")}

    {t("autoResolveDesc")}

    - + setAutoResolve(!formState.autoResolve)} + ariaLabel={t("autoResolve")} + />
    ); } @@ -99,30 +83,26 @@ function StreamDefaultModeSelector({

    {t("streamDefaultModeDesc")}

    - - +
    ); @@ -140,7 +120,6 @@ export default function BehaviourTab({ errors, }: BehaviourTabProps) { const t = useTranslations("apiManager"); - const tc = useTranslations("common"); return (
    @@ -176,20 +155,13 @@ export default function BehaviourTab({

    {t("localUsageCommand")}

    {t("localUsageCommandDesc")}

    - + setAllowUsageCommand(!formState.allowUsageCommand)} + ariaLabel={t("localUsageCommand")} + />
    {/* Advanced Provider Quota Policy Override */} diff --git a/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/ConnectionsTab.tsx b/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/ConnectionsTab.tsx index 759aef4ad8ab..d6df5472ab91 100644 --- a/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/ConnectionsTab.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/ConnectionsTab.tsx @@ -1,6 +1,7 @@ "use client"; import { useTranslations } from "next-intl"; +import Button from "@/shared/components/Button"; import ProviderConnectionPermissionList, { type ProviderConnection, } from "@/app/(dashboard)/dashboard/api-manager/components/ProviderConnectionPermissionList"; @@ -41,31 +42,27 @@ export default function ConnectionsTab({

    - - +
    diff --git a/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/GeneralTab.tsx b/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/GeneralTab.tsx index 117439827165..c271de660dfe 100644 --- a/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/GeneralTab.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/GeneralTab.tsx @@ -1,7 +1,12 @@ "use client"; +import Button from "@/shared/components/Button"; +import Icon from "@/shared/components/Icon"; +import Toggle from "@/shared/components/Toggle"; + import { useTranslations } from "next-intl"; -import { Input } from "@/shared/components"; +import Input from "@/shared/components/Input"; +import { cn } from "@/shared/utils/cn"; import { ENDPOINT_CATEGORIES } from "@/shared/constants/endpointCategories"; import { toLocalDateTimeInputValue } from "@/app/(dashboard)/dashboard/api-manager/apiManagerPageUtils"; import RoutingEntryLink from "@/shared/components/routing/RoutingEntryLink"; @@ -69,14 +74,12 @@ function ExclusiveLeaseNotice({ apiKey }: Pick) { <> {/* Exclusive Lease Notice */} {hasExclusiveLeaseScope && ( -
    -
    - key_vertical +
    +
    + {t("exclusiveLeaseNoticeTitle")}
    -

    - {t("exclusiveLeaseNoticeDesc")} -

    +

    {t("exclusiveLeaseNoticeDesc")}

    )} @@ -85,7 +88,6 @@ function ExclusiveLeaseNotice({ apiKey }: Pick) { function KeyActiveToggle({ formState, setIsActive }: GeneralSectionProps<"setIsActive">) { const t = useTranslations("apiManager"); - const tc = useTranslations("common"); return (
    @@ -94,22 +96,13 @@ function KeyActiveToggle({ formState, setIsActive }: GeneralSectionProps<"setIsA

    {t("keyActive")}

    {t("keyActiveDesc")}

    - + setIsActive(!formState.isActive)} + ariaLabel={t("keyActive")} + />
    ); } @@ -118,30 +111,21 @@ function BanToggle({ formState, setIsBanned }: GeneralSectionProps<"setIsBanned" const t = useTranslations("apiManager"); return ( -
    +
    {/* Ban Toggle (SECURITY) */}
    -

    {t("bannedStatus")}

    -

    +

    {t("bannedStatus")}

    +

    Immediately revoke all access. Used for suspected abuse or compromised keys.

    - + setIsBanned(!formState.isBanned)} + ariaLabel={t("bannedStatus")} + />
    ); } @@ -160,8 +144,10 @@ function ExpirationDateField({ formState, setExpiresAt }: GeneralSectionProps<"s

    - { const val = e.target.value; @@ -174,16 +160,15 @@ function ExpirationDateField({ formState, setExpiresAt }: GeneralSectionProps<"s setExpiresAt(date.toISOString()); } }} - className="min-w-0 flex-1 px-2.5 py-1.5 text-sm border border-border rounded-md bg-background text-text-main focus:outline-none focus:ring-1 focus:ring-primary" /> - +
    ); @@ -194,7 +179,6 @@ function ManagementAccessToggle({ setManageEnabled, }: GeneralSectionProps<"setManageEnabled">) { const t = useTranslations("apiManager"); - const tc = useTranslations("common"); return (
    @@ -203,20 +187,13 @@ function ManagementAccessToggle({

    {t("managementAccess")}

    {t("managementAccessDesc")}

    - + setManageEnabled(!formState.manageEnabled)} + ariaLabel={t("managementAccess")} + />
    ); } @@ -227,7 +204,6 @@ function SelfServiceVisibility({ setSelfAccountQuotaEnabled, }: GeneralSectionProps<"setSelfUsageEnabled" | "setSelfAccountQuotaEnabled">) { const t = useTranslations("apiManager"); - const tc = useTranslations("common"); return (
    @@ -242,20 +218,13 @@ function SelfServiceVisibility({

    {t("ownUsageVisibility")}

    {t("ownUsageVisibilityDesc")}

    - + setSelfUsageEnabled(!formState.selfUsageEnabled)} + ariaLabel={t("ownUsageVisibility")} + />
    @@ -263,21 +232,14 @@ function SelfServiceVisibility({

    {t("sharedAccountQuotaVisibility")}

    {t("sharedAccountQuotaVisibilityDesc")}

    - + onChange={() => setSelfAccountQuotaEnabled(!formState.selfAccountQuotaEnabled)} + ariaLabel={t("sharedAccountQuotaVisibility")} + />
    ); @@ -292,30 +254,30 @@ function EndpointCategoryPicker({ {ENDPOINT_CATEGORIES.map((cat) => { const isSelected = formState.selectedEndpoints.includes(cat.id); return ( - + ); })}
    @@ -344,28 +306,24 @@ function AllowedEndpointsSection({

    - - +
{!formState.allowAllEndpoints && ( diff --git a/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/LimitsTab.tsx b/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/LimitsTab.tsx index 343fcb91b40b..89f71f21bc1d 100644 --- a/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/LimitsTab.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/LimitsTab.tsx @@ -1,7 +1,11 @@ "use client"; +import Button from "@/shared/components/Button"; +import Toggle from "@/shared/components/Toggle"; + +import { useId } from "react"; import { useTranslations } from "next-intl"; -import { Input } from "@/shared/components"; +import Input from "@/shared/components/Input"; import { UsageLimitSettings } from "@/app/(dashboard)/dashboard/api-manager/components/UsageLimitSettings"; import type { ApiKeyAccessFormState } from "../useApiKeyAccessForm"; import TabErrorList from "./TabErrorList"; @@ -40,6 +44,7 @@ function MaxSessionsField({ formState, setMaxSessions }: LimitsSectionProps<"set type="number" min={0} step={1} + aria-label={t("maxActiveSessions")} value={String(formState.maxSessions)} onChange={(e) => { const parsed = Number.parseInt(e.target.value || "0", 10); @@ -69,6 +74,7 @@ function ThrottleDelayField({ min={0} max={300000} step={100} + aria-label={t("throttleDelay")} value={String(formState.throttleDelayMs)} onChange={(e) => { const parsed = Number.parseInt(e.target.value || "0", 10); @@ -98,14 +104,15 @@ function CustomRateLimits({

{t("apiManagerCustomRateLimits")}

{t("apiManagerCustomRateLimitsDesc")}

- +
{formState.rateLimits.length > 0 && (
@@ -120,6 +127,7 @@ function CustomRateLimits({ updateRateLimit(index, val, rl.window); }} placeholder={t("apiManagerRateLimitRequestsPlaceholder")} + aria-label={t("apiManagerRateLimitRequestsPlaceholder")} /> {t("apiManagerRateLimitReqPer")} @@ -133,16 +141,18 @@ function CustomRateLimits({ updateRateLimit(index, rl.limit, val); }} placeholder={t("apiManagerRateLimitSecondsPlaceholder")} + aria-label={t("apiManagerRateLimitSecondsPlaceholder")} /> sec - + aria-label={t("apiManagerRemoveLimitTitle")} + icon="delete" + />
))}
@@ -153,11 +163,14 @@ function CustomRateLimits({ function ScheduleDayPicker({ formState, setScheduleDays }: LimitsSectionProps<"setScheduleDays">) { const t = useTranslations("apiManager"); + const labelId = useId(); return (
- -
+

+ {t("scheduleDays")} +

+
{( [ [0, t("daySun")], @@ -171,9 +184,11 @@ function ScheduleDayPicker({ formState, setScheduleDays }: LimitsSectionProps<"s ).map(([dayIdx, label]) => { const selected = formState.scheduleDays.includes(dayIdx); return ( - + ); })}
@@ -211,37 +222,30 @@ function ScheduleDetails({ return (
-
- - setScheduleFrom(e.target.value)} - className="w-full px-2.5 py-1.5 text-sm border border-border rounded-md bg-background text-text-main focus:outline-none focus:ring-1 focus:ring-primary" - /> -
-
- - setScheduleUntil(e.target.value)} - className="w-full px-2.5 py-1.5 text-sm border border-border rounded-md bg-background text-text-main focus:outline-none focus:ring-1 focus:ring-primary" - /> -
-
- -
- - setScheduleTz(e.target.value)} - placeholder={t("apiManagerTimezonePlaceholder")} - className="w-full max-w-sm px-2.5 py-1.5 text-sm border border-border rounded-md bg-background text-text-main font-mono focus:outline-none focus:ring-1 focus:ring-primary" + setScheduleFrom(e.target.value)} + /> + setScheduleUntil(e.target.value)} /> -

{t("scheduleTimezoneHint")}

+ + setScheduleTz(e.target.value)} + placeholder={t("apiManagerTimezonePlaceholder")} + hint={t("scheduleTimezoneHint")} + className="max-w-sm" + inputClassName="font-mono" + />
); } @@ -255,7 +259,6 @@ function AccessScheduleSection({ setScheduleTz, }: LimitsSectionProps<"setScheduleEnabled" | ScheduleDetailSetter>) { const t = useTranslations("apiManager"); - const tc = useTranslations("common"); return (
@@ -264,20 +267,13 @@ function AccessScheduleSection({

{t("accessSchedule")}

{t("accessScheduleDesc")}

- + setScheduleEnabled(!formState.scheduleEnabled)} + ariaLabel={t("accessSchedule")} + />
{formState.scheduleEnabled && ( {/* Access Mode Toggle */} - - +
); } @@ -226,25 +227,17 @@ function ModelAccessInfoBanner({
{/* Info Banner */} - - {formState.allowAll ? "info" : "warning"} - -

+ +

{formState.allowAll ? t("allowAllDesc") : !modelsLoaded @@ -269,19 +262,16 @@ function ForcedModelSetting({ return (

- -

{t("forcedModelDesc")}

- setForcedModel(e.target.value)} placeholder={t("forcedModelPlaceholder")} - className="w-full px-3 py-2 text-sm rounded-md border border-border bg-transparent text-text-main" /> {allModels.map((model) => ( @@ -298,7 +288,6 @@ function CatalogVisibilitySettings({ setDisableNonPublicModels, }: Pick) { const t = useTranslations("apiManager"); - const tc = useTranslations("common"); return ( <> @@ -311,22 +300,13 @@ function CatalogVisibilitySettings({

{t("disableNonPublicModels")}

{t("disableNonPublicModelsDesc")}

- + setDisableNonPublicModels(!formState.disableNonPublicModels)} + ariaLabel={t("disableNonPublicModels")} + />
); @@ -371,20 +351,22 @@ function SelectedModelsSummary({
{selectedPermissionSummary}
- - +
@@ -458,28 +440,33 @@ function ClaudeCodeDefaultScopeChip({ return (
- - + + + {claudeCodeFamiliesExpanded && ( @@ -525,8 +512,8 @@ function ClaudeCodeFamilyChips({ key={family.id} className={`inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] rounded border ${ canBlock - ? "bg-white dark:bg-surface text-text-main border-border" - : "bg-black/5 dark:bg-white/5 text-text-muted border-border" + ? "bg-surface text-text-main border-border" + : "bg-surface-2 text-text-muted border-border" }`} title={ canBlock @@ -536,14 +523,16 @@ function ClaudeCodeFamilyChips({ > {family.label} {canBlock && ( - + + )} ); @@ -556,6 +545,7 @@ function ProviderScopeChip({ scope, toggleModel, }: Pick & { scope: string }) { + const t = useTranslations("apiManager"); const provider = scope.slice(0, -2); return ( {provider} - + + ); } @@ -578,18 +570,21 @@ function SelectedModelChip({ modelId, toggleModel, }: Pick & { modelId: string }) { + const t = useTranslations("apiManager"); return ( - + {getModelDisplayName(modelId)} - + + ); } diff --git a/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/TabErrorList.tsx b/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/TabErrorList.tsx index 653b033083cc..534bb079ac2a 100644 --- a/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/TabErrorList.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/[id]/access/tabs/TabErrorList.tsx @@ -1,5 +1,7 @@ "use client"; +import Icon from "@/shared/components/Icon"; + interface TabErrorListProps { errors?: readonly string[]; } @@ -14,13 +16,11 @@ export default function TabErrorList({ errors }: TabErrorListProps) { return (
{errors.map((message) => ( -

- +

+ {message}

))} diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index d90f33f4943f..461217f3e4df 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -2500,7 +2500,8 @@ "onlySelectedConnections": "Only selected connections", "selectAtLeastOneConnection": "Select at least one connection or choose All connections.", "allConnectionsDesc": "This key can use any active connection.", - "restrictedToConnections": "Restricted to {count, plural, one {# connection} other {# connections}}." + "restrictedToConnections": "Restricted to {count, plural, one {# connection} other {# connections}}.", + "removeSelection": "Remove {name}" }, "auditLog": { "title": "Audit Log", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 69030331c427..1347e3bb1fef 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -2500,7 +2500,8 @@ "onlySelectedConnections": "Apenas as conexões selecionadas", "selectAtLeastOneConnection": "Selecione pelo menos uma conexão ou escolha Todas as conexões.", "allConnectionsDesc": "Esta chave pode usar qualquer conexão ativa.", - "restrictedToConnections": "Restrita a {count, plural, one {# conexão} other {# conexões}}." + "restrictedToConnections": "Restrita a {count, plural, one {# conexão} other {# conexões}}.", + "removeSelection": "Remover {name}" }, "auditLog": { "title": "Log de Auditoria", diff --git a/tests/e2e/api-key-model-scope.spec.ts b/tests/e2e/api-key-model-scope.spec.ts index 5bbc6d5099c2..7043582a0d6b 100644 --- a/tests/e2e/api-key-model-scope.spec.ts +++ b/tests/e2e/api-key-model-scope.spec.ts @@ -235,9 +235,10 @@ test.describe("API key model scope", () => { const permissionsEditor = await openKeyAccessEditor(page, "Provider Scope Key"); await permissionsEditor.getByRole("tab", { name: "Models", exact: true }).click(); - // Enter Restrict mode via the primary Access Mode toggle (accessible name includes the lock icon text). + // Enter Restrict mode via the primary Access Mode toggle (KEA-7886: the lock icon is an + // aria-hidden Lucide svg, so the accessible name is just "Restrict"). const accessModeRestrict = permissionsEditor.getByRole("button", { - name: /^lock\s+restrict$/i, + name: /^restrict$/i, }); await expect(accessModeRestrict).toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); await accessModeRestrict.click(); @@ -288,8 +289,9 @@ test.describe("API key model scope", () => { await reopened.getByRole("tab", { name: "Models", exact: true }).click(); // Restrict mode should remain selected after reopen. - await expect(reopened.getByRole("button", { name: /^lock\s+restrict$/i })).toHaveClass( - /bg-primary/ + await expect(reopened.getByRole("button", { name: /^restrict$/i })).toHaveAttribute( + "aria-pressed", + "true" ); const reopenedProviderCheckbox = reopened.getByRole("checkbox", { @@ -320,8 +322,9 @@ test.describe("API key model scope", () => { await returnToKeyList(page); const denyAllReopened = await openKeyAccessEditor(page, "Provider Scope Key"); await denyAllReopened.getByRole("tab", { name: "Models", exact: true }).click(); - await expect(denyAllReopened.getByRole("button", { name: /^lock\s+restrict$/i })).toHaveClass( - /bg-primary/ + await expect(denyAllReopened.getByRole("button", { name: /^restrict$/i })).toHaveAttribute( + "aria-pressed", + "true" ); await expect( denyAllReopened.getByRole("checkbox", { name: /ollama[- ]?cloud/i }) diff --git a/tests/unit/api-manager-page-static.test.ts b/tests/unit/api-manager-page-static.test.ts index efacafdbd3e2..4f562357139f 100644 --- a/tests/unit/api-manager-page-static.test.ts +++ b/tests/unit/api-manager-page-static.test.ts @@ -89,9 +89,10 @@ test("general tab uses i18n for management access description", () => { test("API manager page renders purpose-first header", () => { const source = readApiManagerPage(); + // KEA-7886: the header is the kit PageHeader (title/description/actions) + the request flow. const headerBlock = source.slice( - source.indexOf('

{ +test("access editor switches never submit the form (kit Switch via the Toggle adapter)", () => { + // KEA-7886: the switches are the Toggle adapter (kit Switch = input[type=checkbox][role=switch]), + // which cannot submit a form; a hand-rolled + {allTags.slice(0, 8).map((tag) => ( - + ))} {allTags.length > 8 && ( @@ -449,17 +441,15 @@ export default function ApiEndpointsTab() { )}

{/* Security tier filter */} -
+
{(["all", "auth", "loopback", "always-protected", "public"] as const).map((tier) => ( - + ))} - +
@@ -492,17 +480,15 @@ export default function ApiEndpointsTab() { {/* Endpoint groups */} {Object.entries(groupedEndpoints).map(([tag, endpoints]) => ( -
- folder +
+

{tag}

- - {endpoints.length} - + {endpoints.length}
-
+
{endpoints.map((ep) => { const key = `${ep.method}:${ep.path}`; const isExpanded = expandedEndpoint === key; @@ -510,17 +496,19 @@ export default function ApiEndpointsTab() { return (
-
setExpandedEndpoint(isExpanded ? null : key)} > - {ep.method} - + {ep.path} @@ -529,23 +517,26 @@ export default function ApiEndpointsTab() { {ep.security && ( - - lock - + )} - - expand_more - -
+ + {/* Expanded detail */} {isExpanded && ( -
+

{ep.summary}

@@ -555,17 +546,13 @@ export default function ApiEndpointsTab() {
{ep.security && ( - - lock - + {t("bearerAuth")} )} {ep.requestBody && ( - - description - + {t("requestBody")} )} @@ -574,40 +561,35 @@ export default function ApiEndpointsTab() {
- +
{/* curl example */} -
-

- {t("example")} -

- - curl -X {ep.method} {baseUrl} - {ep.path} - {ep.security ? ' -H "Authorization: Bearer YOUR_KEY"' : ""} - {ep.requestBody + -
+ : "" + }`} + /> {/* Try It panel */} {isTrying && ( @@ -615,44 +597,43 @@ export default function ApiEndpointsTab() { {ep.security && (
-
{useManualKey ? ( - setManualApiKey(e.target.value)} placeholder={t("pasteApiKey")} - className="w-full px-3 py-2 text-xs font-mono rounded-lg border border-black/10 - dark:border-white/10 bg-white dark:bg-black/30 focus:outline-none - focus:ring-1 focus:ring-primary" + aria-label={t("apiKey")} + inputClassName="font-mono text-xs" /> ) : availableApiKeys.length > 0 ? ( - + ) : ( -

+

{apiKeyLoadError || t("noActiveApiKeys")}

)} @@ -660,46 +641,43 @@ export default function ApiEndpointsTab() { )} {ep.method !== "GET" && (
-