diff --git a/ui/src/pages/Secrets.render.test.tsx b/ui/src/pages/Secrets.render.test.tsx
index bd5d8f83b04..e80e31c68bb 100644
--- a/ui/src/pages/Secrets.render.test.tsx
+++ b/ui/src/pages/Secrets.render.test.tsx
@@ -517,7 +517,7 @@ describe("Secrets page layout", () => {
});
});
- it("opens reference details from the secrets table count", async () => {
+ it("keeps references reachable from the compact secrets row and detail drawer", async () => {
mockSecretsApi.list.mockResolvedValue([makeCompanySecret()]);
mockSecretsApi.usage.mockResolvedValue({
secretId: "secret-openai",
@@ -563,17 +563,29 @@ describe("Secrets page layout", () => {
await flushReact();
const referencesButton = container.querySelector(
- 'button[aria-label="View references for OPENAI_API_KEY"]',
+ 'button[aria-label="Actions for OPENAI_API_KEY"]',
) as HTMLButtonElement | null;
- expect(referencesButton?.textContent).toBe("2");
+ expect(referencesButton).not.toBeNull();
+ const companyRow = Array.from(container.querySelectorAll("[role='row']")).find(
+ (row) => row.textContent?.includes("OPENAI_API_KEY"),
+ ) as HTMLElement | undefined;
await act(async () => {
- referencesButton?.click();
+ companyRow?.click();
+ });
+ await flushReact();
+
+ const usageButton = Array.from(document.body.querySelectorAll("button")).find(
+ (button) => button.textContent?.includes("Usage"),
+ ) as HTMLButtonElement | undefined;
+ await act(async () => {
+ usageButton?.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true }));
+ usageButton?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
+ usageButton?.click();
});
await flushReact();
expect(mockSecretsApi.usage).toHaveBeenCalledWith("secret-openai");
- expect(document.body.textContent).toContain("Secret references");
expect(document.body.textContent).toContain("CodexCoder");
expect(document.body.textContent).toContain("env.OPENAI_API_KEY");
@@ -606,7 +618,7 @@ describe("Secrets page layout", () => {
expect(container.textContent).toContain("Personal GitHub token");
expect(container.textContent).toContain("Company");
expect(container.textContent).toContain("Each user");
- expect(container.textContent).toContain("3 of 5 set");
+ expect(container.textContent).toContain("3/5 set");
expect(container.textContent).not.toContain("User secret definitions");
expect(mockSecretsApi.list).toHaveBeenCalledWith("company-1");
expect(mockSecretsApi.listUserSecretDefinitions).toHaveBeenCalledWith("company-1");
@@ -712,11 +724,11 @@ describe("Secrets page layout", () => {
await flushReact();
await flushReact();
- const rowOpenButton = Array.from(container.querySelectorAll("button")).find(
- (button) => button.textContent?.trim() === "Open",
- ) as HTMLButtonElement | undefined;
+ const definitionRow = Array.from(container.querySelectorAll("[role='row']")).find(
+ (row) => row.textContent?.includes("Personal GitHub token"),
+ ) as HTMLElement | undefined;
await act(async () => {
- rowOpenButton?.click();
+ definitionRow?.click();
});
await flushReact();
diff --git a/ui/src/pages/Secrets.tsx b/ui/src/pages/Secrets.tsx
index b8da13bd529..94ab638740a 100644
--- a/ui/src/pages/Secrets.tsx
+++ b/ui/src/pages/Secrets.tsx
@@ -8,12 +8,15 @@ import {
Ban,
CheckCircle2,
Cloud,
+ Copy,
Database,
Edit3,
ExternalLink,
KeyRound,
Link2,
+ Lock,
Loader2,
+ MoreHorizontal,
Plus,
RefreshCw,
Search,
@@ -79,7 +82,16 @@ import {
} from "@/components/ui/dialog";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "../lib/utils";
+import { copyTextToClipboard } from "../lib/clipboard";
import { PageTabBar } from "../components/PageTabBar";
import { ImportFromVaultDialog } from "./secrets/ImportFromVaultDialog";
import { MyUserSecretsTab } from "./secrets/MyUserSecretsTab";
@@ -284,6 +296,107 @@ function modeDescription(managedMode: SecretManagedMode) {
: "Paperclip resolves this provider reference but does not rotate the provider value.";
}
+function statusLabel(status: SecretStatus) {
+ return status.charAt(0).toUpperCase() + status.slice(1);
+}
+
+function statusDotTone(status: SecretStatus) {
+ switch (status) {
+ case "active":
+ return "bg-emerald-500";
+ case "disabled":
+ return "bg-amber-500";
+ case "archived":
+ return "bg-muted-foreground";
+ case "deleted":
+ return "bg-destructive";
+ default:
+ return "bg-muted-foreground";
+ }
+}
+
+function StatusBadge({ status }: { status: SecretStatus }) {
+ return (
+
+
+ {statusLabel(status)}
+
+ );
+}
+
+function MetaChip({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function providerIndicatorLabel(
+ secret: CompanySecret,
+ providers: SecretProviderDescriptor[],
+ providerConfigs: CompanySecretProviderConfig[],
+) {
+ const provider = providerLabel(providers, secret.provider);
+ const vault = providerVaultLabel(providerConfigs, secret.providerConfigId);
+ const custody = modeLabel(secret.managedMode);
+ return [
+ `${custody} · ${provider}`,
+ vault ? `Vault: ${vault}` : null,
+ secret.externalRef ? `Reference: ${secret.externalRef}` : null,
+ ]
+ .filter(Boolean)
+ .join("\n");
+}
+
+function SecretProviderIndicator({
+ secret,
+ providers,
+ providerConfigs,
+}: {
+ secret: CompanySecret;
+ providers: SecretProviderDescriptor[];
+ providerConfigs: CompanySecretProviderConfig[];
+}) {
+ const label = providerIndicatorLabel(secret, providers, providerConfigs);
+ const Icon = secret.managedMode === "external_reference" ? ExternalLink : Lock;
+ return (
+
+
+
+
+
+
+ {label}
+
+ );
+}
+
+function UpdatedWithTooltip({
+ updatedAt,
+ tooltip,
+}: {
+ updatedAt: Date | string | null | undefined;
+ tooltip: string;
+}) {
+ return (
+
+
+
+ {formatRelative(updatedAt)}
+
+
+ {tooltip}
+
+ );
+}
+
function healthEntryForProvider(
health: SecretProviderHealthResponse | null,
providerId: SecretProvider,
@@ -1137,6 +1250,172 @@ export function Secrets() {
}));
}
+ function openCompanySecret(secret: CompanySecret) {
+ setSecretDetailTab("details");
+ setSelectedSecretId(secret.id);
+ setSelectedDefinitionId(null);
+ }
+
+ function openUserDefinition(definition: UserSecretDefinition) {
+ setSecretDetailTab("details");
+ setSelectedDefinitionId(definition.id);
+ setSelectedSecretId(null);
+ }
+
+ function openRotateSecret(secret: CompanySecret) {
+ openCompanySecret(secret);
+ setRotateOpen(true);
+ setRotateValue("");
+ setRotateExternalRef("");
+ setRotateProviderConfigId(
+ secret.providerConfigId ?? getDefaultProviderConfigId(providerConfigs, secret.provider),
+ );
+ setRotateError(null);
+ }
+
+ function copySecretKey(key: string) {
+ void copyTextToClipboard(key)
+ .then(() => pushToast({ title: "Secret key copied", body: key, tone: "success" }))
+ .catch((error) =>
+ pushToast({
+ title: "Copy failed",
+ body: error instanceof Error ? error.message : "Unable to copy secret key",
+ tone: "error",
+ }),
+ );
+ }
+
+ function renderRowActions(row: UnifiedSecretRow) {
+ const name = row.kind === "company" ? row.secret.name : row.definition.name;
+ return (
+
+
+
+
+
+ {
+ if (row.kind === "company") openCompanySecret(row.secret);
+ else openUserDefinition(row.definition);
+ }}
+ >
+ View details
+
+ {row.kind === "company" ? (
+ <>
+ setUsageDialogSecretId(row.secret.id)}>
+ View references ({row.secret.referenceCount ?? 0})
+
+ openRotateSecret(row.secret)}>
+
+ {row.secret.managedMode === "external_reference" ? "Update reference" : "Update value"}
+
+
+
+ statusMutation.mutate({
+ id: row.secret.id,
+ status: row.secret.status === "active" ? "disabled" : "active",
+ })
+ }
+ >
+ {row.secret.status === "active" ? : }
+ {row.secret.status === "active" ? "Disable" : "Activate"}
+
+
+ statusMutation.mutate({
+ id: row.secret.id,
+ status: row.secret.status === "archived" ? "active" : "archived",
+ })
+ }
+ >
+ {row.secret.status === "archived" ? (
+
+ ) : (
+
+ )}
+ {row.secret.status === "archived" ? "Unarchive" : "Archive"}
+
+
+ setDeleteConfirm(row.secret)}>
+ Delete secret
+
+ >
+ ) : (
+ <>
+
+ setSetMyValueFor(
+ myUserSecrets.find((entry) => entry.definition.id === row.definition.id) ?? {
+ definition: row.definition,
+ secret: null,
+ },
+ )
+ }
+ >
+
+ {myUserSecrets.find((entry) => entry.definition.id === row.definition.id)?.secret
+ ? "Update my value"
+ : "Set my value"}
+
+ openEditDefinition(row.definition)}>
+ Edit definition
+
+
+
+ definitionStatusMutation.mutate({
+ definition: row.definition,
+ status: row.definition.status === "active" ? "disabled" : "active",
+ })
+ }
+ >
+ {row.definition.status === "active" ? (
+
+ ) : (
+
+ )}
+ {row.definition.status === "active" ? "Disable" : "Activate"}
+
+
+ definitionStatusMutation.mutate({
+ definition: row.definition,
+ status: row.definition.status === "archived" ? "active" : "archived",
+ })
+ }
+ >
+ {row.definition.status === "archived" ? (
+
+ ) : (
+
+ )}
+ {row.definition.status === "archived" ? "Unarchive" : "Archive"}
+
+
+ setDefinitionDeleteConfirm(row.definition)}>
+ Delete definition
+
+ >
+ )}
+
+
+ );
+ }
+
if (!selectedCompanyId) {
return (
Select a company to manage secrets.
@@ -1144,6 +1423,7 @@ export function Secrets() {
}
return (
+
@@ -1226,143 +1506,177 @@ export function Secrets() {
) : filteredRows.length === 0 ? (
) : (
-
-
-
- | Name |
- Provided by |
- Mode |
- Provider |
- Status |
- Coverage / version |
- Last updated |
- Last resolved |
- References |
- Reference |
- |
-
-
-
- {filteredRows.map((row) => (
- {
- setSecretDetailTab("details");
- if (row.kind === "company") {
- setSelectedSecretId(row.secret.id);
- setSelectedDefinitionId(null);
- } else {
- setSelectedDefinitionId(row.definition.id);
- setSelectedSecretId(null);
- }
- }}
+
+
+
-
-
- {row.kind === "company" ? row.secret.name : row.definition.name}
-
-
- {row.kind === "company" ? row.secret.key : row.definition.key}
-
- |
-
- {row.kind === "company" ? (
-
- Company
-
- ) : (
-
- )}
- |
-
- {row.kind === "company" ? modeLabel(row.secret.managedMode) : "Personal value"}
- |
-
-
- {row.kind === "company" ? providerLabel(providers, row.secret.provider) : "—"}
-
- |
-
-
- {row.kind === "company" ? row.secret.status : row.definition.status}
-
- |
-
- {row.kind === "company" ? (
- v{row.secret.latestVersion}
- ) : (
-
- )}
- |
-
- {formatRelative(row.kind === "company" ? row.secret.lastRotatedAt : row.definition.updatedAt)}
- |
-
- {row.kind === "company" ? formatRelative(row.secret.lastResolvedAt) : "—"}
- |
-
- {row.kind === "company" ? (
- |
-
- {row.kind === "user" ? (
- Each member stores their own value
- ) : row.secret.managedMode === "external_reference" ? (
-
-
- {row.secret.externalRef ?? "—"}
-
- ) : (
- Owned
- )}
- |
-
- |
-
- ))}
-
-
+
+
+
+ {row.kind === "company" ? row.secret.name : row.definition.name}
+
+
+ {row.kind === "company" ? row.secret.key : row.definition.key}
+
+
+
event.stopPropagation()}>{renderRowActions(row)}
+
+
+ {row.kind === "company" ? (
+ <>
+
+ Company
+
+
+
+ >
+ ) : (
+ <>
+
+
+
+ >
+ )}
+
+
+
+ {row.kind === "company" ? (
+ <>
+ v{row.secret.latestVersion} ·{" "}
+ {row.secret.managedMode === "external_reference" ? "linked" : "managed"}
+ >
+ ) : (
+ "Member-owned values"
+ )}
+
+ Updated {formatRelative(row.kind === "company" ? row.secret.updatedAt : row.definition.updatedAt)}
+
+
+ );
+ })}
+
+
)}
@@ -1409,82 +1723,91 @@ export function Secrets() {
{selectedSecret ? (
<>
-
-
-
- {selectedSecret.name}
-
- {selectedSecret.status}
-
+
+
+
+ {selectedSecret.name}
+
-
- {providerLabel(providers, selectedSecret.provider)} · v{selectedSecret.latestVersion} · {modeLabel(selectedSecret.managedMode)}
+
+ {providerLabel(providers, selectedSecret.provider)} secret {selectedSecret.key}
+
+
+ {selectedSecret.key}
+
+
+
+
+
+ Company
+
+ {modeLabel(selectedSecret.managedMode)}
+ {providerLabel(providers, selectedSecret.provider)}
+ v{selectedSecret.latestVersion}
+
-
+
- {selectedSecret.status === "active" ? (
-
- ) : (
-
- )}
- {selectedSecret.status === "archived" ? (
-
- ) : (
-
- )}
-
+
+
+
+
+
+
+ statusMutation.mutate({
+ id: selectedSecret.id,
+ status: selectedSecret.status === "active" ? "disabled" : "active",
+ })
+ }
+ >
+ {selectedSecret.status === "active" ? (
+
+ ) : (
+
+ )}
+ {selectedSecret.status === "active" ? "Disable" : "Activate"}
+
+
+ statusMutation.mutate({
+ id: selectedSecret.id,
+ status: selectedSecret.status === "archived" ? "active" : "archived",
+ })
+ }
+ >
+ {selectedSecret.status === "archived" ? (
+
+ ) : (
+
+ )}
+ {selectedSecret.status === "archived" ? "Unarchive" : "Archive"}
+
+
+ setDeleteConfirm(selectedSecret)}>
+ Delete secret
+
+
+
@@ -1501,7 +1824,7 @@ export function Secrets() {
-
+
@@ -1518,22 +1841,39 @@ export function Secrets() {
>
) : selectedDefinition ? (
<>
-
-
-
- {selectedDefinition.name}
-
- {selectedDefinition.status}
-
+
+
+
+ {selectedDefinition.name}
+
-
- Each user · {selectedDefinition.key}
+
+ Each user secret definition {selectedDefinition.key}
+
+
+ {selectedDefinition.key}
+
+
+
+
+
+
+
+
+
-
+
-
- {selectedDefinition.status === "active" ? (
-
- ) : (
-
- )}
- {selectedDefinition.status === "archived" ? (
-
- ) : (
-
- )}
-
+
+
+
+
+
+ openEditDefinition(selectedDefinition)}>
+ Edit definition
+
+
+
+ definitionStatusMutation.mutate({
+ definition: selectedDefinition,
+ status: selectedDefinition.status === "active" ? "disabled" : "active",
+ })
+ }
+ >
+ {selectedDefinition.status === "active" ? (
+
+ ) : (
+
+ )}
+ {selectedDefinition.status === "active" ? "Disable" : "Activate"}
+
+
+ definitionStatusMutation.mutate({
+ definition: selectedDefinition,
+ status: selectedDefinition.status === "archived" ? "active" : "archived",
+ })
+ }
+ >
+ {selectedDefinition.status === "archived" ? (
+
+ ) : (
+
+ )}
+ {selectedDefinition.status === "archived" ? "Unarchive" : "Archive"}
+
+
+ setDefinitionDeleteConfirm(selectedDefinition)}>
+ Delete definition
+
+
+
@@ -1630,7 +1950,7 @@ export function Secrets() {
-
+
+
);
}
@@ -3119,9 +3440,11 @@ function TextField({
function CoverageInline({
companyId,
definitionId,
+ compact = false,
}: {
companyId: string;
definitionId: string;
+ compact?: boolean;
}) {
const coverageQuery = useQuery({
queryKey: queryKeys.secrets.userDefinitionCoverage(companyId, definitionId),
@@ -3132,19 +3455,31 @@ function CoverageInline({
if (coverageQuery.isPending) return Loading…;
if (coverageQuery.isError) return Coverage unavailable;
return (
-
+
- {coverageSummaryLabel(summary)}
+
+ {compact && summary
+ ? `${summary.configuredCount}/${summary.configuredCount + summary.missingCount + summary.inactiveCount} set`
+ : coverageSummaryLabel(summary)}
+
{summary && summary.missingCount > 0 ? (
- · {summary.missingCount} missing
+
+ · {compact ? `${summary.missingCount} miss` : `${summary.missingCount} missing`}
+
) : null}
);
}
-function UserSecretDetailsTab({ definition }: { definition: UserSecretDefinition }) {
+function UserSecretDetailsTab({
+ companyId,
+ definition,
+}: {
+ companyId: string;
+ definition: UserSecretDefinition;
+}) {
return (
-
+
{definition.description ?? —}
@@ -3152,18 +3487,16 @@ function UserSecretDetailsTab({ definition }: { definition: UserSecretDefinition
{definition.key}
- {definition.status}
+
+
+
+
{formatRelative(definition.createdAt)}
{formatRelative(definition.updatedAt)}
-
-
-
- Usage guidance
-
- -
- {definition.usageGuidance ?? —}
-
-
-
+
+ {definition.usageGuidance ?? —}
+
+
No value is stored on this admin row. Each member manages their own value under My secrets.
@@ -3251,35 +3584,40 @@ function UserSecretAccessEventsTab() {
function SecretDetailsTab({
secret,
+ providers,
providerConfigs,
}: {
secret: CompanySecret;
+ providers: SecretProviderDescriptor[];
providerConfigs: CompanySecretProviderConfig[];
}) {
return (
-
+
{secret.description ?? —}
+ Company
{modeLabel(secret.managedMode)}
- {secret.provider.replaceAll("_", " ")}
+ {providerLabel(providers, secret.provider)}
{providerVaultLabel(providerConfigs, secret.providerConfigId)}
+
+ {secret.externalRef ? (
+ {secret.externalRef}
+ ) : (
+ —
+ )}
+
v{secret.latestVersion}
+
+ {(secret.referenceCount ?? 0) === 1
+ ? "1 binding"
+ : `${secret.referenceCount ?? 0} bindings`}
+
{formatRelative(secret.createdAt)}
{formatRelative(secret.updatedAt)}
{formatRelative(secret.lastRotatedAt)}
{formatRelative(secret.lastResolvedAt)}
- {secret.externalRef ? (
-
-
-
- {secret.managedMode === "external_reference" ? "Linked provider reference" : "Provider-managed path"}
-
- -
- {secret.externalRef}
-
-
- ) : null}
-
+
{modeDescription(secret.managedMode)} Paperclip never re-displays stored values.
@@ -3288,9 +3626,9 @@ function SecretDetailsTab({
function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
-
+
- {label}
- - {children}
+ - {children}
);
}