From 34076590d7631a7e5ab1e40c11e16a797e0d932b Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 1 Jul 2026 18:20:59 +0000 Subject: [PATCH] Add missing-user-secret warning path and access-event owner metadata - MissingUserSecretsBanner: reusable warning listing the current user's unset user secrets with inline "Set value" (shared SetMyUserSecretDialog). Renders nothing when there is nothing missing, so it is safe to embed. Mounted on the New Issue dialog (task creation surface). - Secret access events now show the User-secret scope badge, responsible user, and credential owner (resolved via the company user directory), satisfying the user-secret owner metadata deliverable. Co-Authored-By: Paperclip --- ui/src/components/NewIssueDialog.tsx | 7 ++ ui/src/pages/Secrets.tsx | 59 +++++++++- .../secrets/MissingUserSecretsBanner.tsx | 106 ++++++++++++++++++ 3 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 ui/src/pages/secrets/MissingUserSecretsBanner.tsx diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index bcbda2d457d..bd88484b8b1 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -7,6 +7,7 @@ import { useCompany } from "../context/CompanyContext"; import { useAdapterCapabilities } from "../adapters/use-adapter-capabilities"; import { executionWorkspacesApi } from "../api/execution-workspaces"; import { issuesApi } from "../api/issues"; +import { MissingUserSecretsBanner } from "../pages/secrets/MissingUserSecretsBanner"; import { instanceSettingsApi } from "../api/instanceSettings"; import { projectsApi } from "../api/projects"; import { agentsApi } from "../api/agents"; @@ -1354,6 +1355,12 @@ export function NewIssueDialog() { /> + {effectiveCompanyId ? ( +
+ +
+ ) : null} +
diff --git a/ui/src/pages/Secrets.tsx b/ui/src/pages/Secrets.tsx index c1a79b79ba1..034bc7fc6d5 100644 --- a/ui/src/pages/Secrets.tsx +++ b/ui/src/pages/Secrets.tsx @@ -49,6 +49,7 @@ import { type UpdateSecretProviderConfigInput, } from "../api/secrets"; import { ApiError } from "../api/client"; +import { accessApi, type CompanyUserDirectoryEntry } from "../api/access"; import { queryKeys } from "../lib/queryKeys"; import { EmptyState } from "../components/EmptyState"; import { Button } from "@/components/ui/button"; @@ -1176,7 +1177,11 @@ export function Secrets() { - +
@@ -2585,7 +2590,34 @@ function SecretUsageTab({ loading, bindings }: { loading: boolean; bindings: Com ); } -function SecretEventsTab({ loading, events }: { loading: boolean; events: SecretAccessEvent[] }) { +function SecretEventsTab({ + loading, + events, + companyId, +}: { + loading: boolean; + events: SecretAccessEvent[]; + companyId: string; +}) { + // Resolve responsible/owner user ids to human names for user-scoped events. + const anyUserScoped = events.some( + (event) => + event.secretScope === "user" || event.responsibleUserId || event.credentialOwnerUserId, + ); + const { data: directory } = useQuery({ + queryKey: queryKeys.access.companyUserDirectory(companyId), + queryFn: () => accessApi.listUserDirectory(companyId), + enabled: anyUserScoped, + staleTime: 60_000, + }); + const userLabel = (userId: string | null): string => { + if (!userId) return "—"; + const entry: CompanyUserDirectoryEntry | undefined = directory?.users.find( + (u) => u.principalId === userId, + ); + return entry?.user?.name?.trim() || entry?.user?.email?.trim() || `${userId.slice(0, 8)}…`; + }; + if (loading) { return
Loading…
; } @@ -2600,15 +2632,34 @@ function SecretEventsTab({ loading, events }: { loading: boolean; events: Secret
{events.map((event) => (
-
- +
+ {event.consumerType} · {event.outcome} + {event.secretScope === "user" ? ( + + User secret + + ) : null} {formatRelative(event.createdAt)}
{event.consumerId}
+ {event.responsibleUserId ? ( +
+ Responsible user: {userLabel(event.responsibleUserId)} +
+ ) : null} + {event.credentialOwnerUserId && + event.credentialOwnerUserId !== event.responsibleUserId ? ( +
+ Credential owner: {userLabel(event.credentialOwnerUserId)} +
+ ) : null} {event.errorCode ? (
{event.errorCode}
) : null} diff --git a/ui/src/pages/secrets/MissingUserSecretsBanner.tsx b/ui/src/pages/secrets/MissingUserSecretsBanner.tsx new file mode 100644 index 00000000000..68ebfe8521f --- /dev/null +++ b/ui/src/pages/secrets/MissingUserSecretsBanner.tsx @@ -0,0 +1,106 @@ +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { AlertTriangle } from "lucide-react"; +import { Link } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { secretsApi, type MyUserSecretEntry } from "../../api/secrets"; +import { queryKeys } from "../../lib/queryKeys"; +import { SetMyUserSecretDialog } from "./SetMyUserSecretDialog"; + +/** + * Warning surface for user secrets the current user has not yet set. Renders + * nothing when there is nothing missing, so it is safe to embed on task + * creation / run and issue-failure surfaces. Lets the user satisfy a missing + * required secret inline via the shared value dialog. + * + * Pass `definitionKeys` to scope the warning to a specific set (e.g. the user + * secrets a blocked run reported as missing); omit it to warn about every + * active definition the user has not set. + */ +export function MissingUserSecretsBanner({ + companyId, + definitionKeys, + title = "Set your user secrets", + secretsPath, + className, +}: { + companyId: string; + definitionKeys?: string[]; + title?: string; + /** Optional route to the Secrets → My secrets tab for the "Manage all" link. */ + secretsPath?: string; + className?: string; +}) { + const [dialogFor, setDialogFor] = useState(null); + + const mySecretsQuery = useQuery({ + queryKey: queryKeys.secrets.myUserSecrets(companyId), + queryFn: () => secretsApi.listMyUserSecrets(companyId), + retry: false, + }); + + const keyFilter = definitionKeys ? new Set(definitionKeys) : null; + const missing = (mySecretsQuery.data ?? []).filter( + (entry) => + entry.definition.status === "active" && + !entry.secret && + (!keyFilter || keyFilter.has(entry.definition.key)), + ); + + if (missing.length === 0) return null; + + return ( +
+
+ +
+

{title}

+

+ {missing.length} user secret{missing.length === 1 ? "" : "s"} you are responsible for + {missing.length === 1 ? " has" : " have"} no value yet. Runs that require + {missing.length === 1 ? " it" : " them"} will fail until you set your value. +

+
    + {missing.map((entry) => ( +
  • + + {entry.definition.name}{" "} + {entry.definition.key} + + +
  • + ))} +
+ {secretsPath ? ( + + Manage all my secrets + + ) : null} +
+
+ + { + if (!open) setDialogFor(null); + }} + /> +
+ ); +}