diff --git a/ui/src/pages/Secrets.render.test.tsx b/ui/src/pages/Secrets.render.test.tsx index fa0d4cdc98..35ffc85731 100644 --- a/ui/src/pages/Secrets.render.test.tsx +++ b/ui/src/pages/Secrets.render.test.tsx @@ -6,6 +6,7 @@ import { MemoryRouter } from "react-router-dom"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { CompanySecretProviderConfig, + RemoteSecretImportPreviewResult, SecretProviderConfigDiscoveryPreviewResult, SecretProviderDescriptor, } from "@paperclipai/shared"; @@ -25,6 +26,8 @@ const mockSecretsApi = vi.hoisted(() => ({ removeProviderConfig: vi.fn(), setDefaultProviderConfig: vi.fn(), checkProviderConfigHealth: vi.fn(), + remoteImportPreview: vi.fn(), + remoteImport: vi.fn(), create: vi.fn(), update: vi.fn(), rotate: vi.fn(), @@ -195,6 +198,18 @@ function makeDiscoveryPreview( }; } +function makeRemoteImportPreview( + overrides: Partial = {}, +): RemoteSecretImportPreviewResult { + return { + providerConfigId: "vault-aws", + provider: "aws_secrets_manager", + nextToken: null, + candidates: [], + ...overrides, + }; +} + function setInputValue(input: HTMLInputElement, value: string) { const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set; setter?.call(input, value); @@ -242,6 +257,7 @@ describe("Secrets page layout", () => { }); mockSecretsApi.providerConfigs.mockResolvedValue(providerConfigs); mockSecretsApi.providerConfigDiscoveryPreview.mockResolvedValue(makeDiscoveryPreview()); + mockSecretsApi.remoteImportPreview.mockResolvedValue(makeRemoteImportPreview()); }); afterEach(() => { @@ -292,6 +308,7 @@ describe("Secrets page layout", () => { onRemove={vi.fn()} onSetDefault={vi.fn()} onHealthCheck={vi.fn()} + onImportSecrets={vi.fn()} pendingActionId={null} />, ); @@ -308,6 +325,58 @@ describe("Secrets page layout", () => { }); }); + it("refreshes existing AWS secrets from a provider vault card", async () => { + const root = createRoot(container); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + + await act(async () => { + root.render( + + + + + , + ); + }); + await flushReact(); + await flushReact(); + + const vaultTabButton = [...document.querySelectorAll("button")].find( + (button) => button.textContent?.includes("Provider vaults"), + ) as HTMLButtonElement | undefined; + await act(async () => { + vaultTabButton?.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true })); + vaultTabButton?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" })); + vaultTabButton?.click(); + }); + await flushReact(); + + const refreshButton = document.querySelector( + '[data-testid="provider-vault-refresh-secrets-vault-aws"]', + ) as HTMLButtonElement | null; + expect(refreshButton).not.toBeNull(); + + await act(async () => { + refreshButton?.click(); + }); + await flushReact(); + await flushReact(); + + expect(document.body.textContent).toContain("Import from AWS Secrets Manager"); + expect(mockSecretsApi.remoteImportPreview).toHaveBeenCalledWith("company-1", { + providerConfigId: "vault-aws", + query: null, + nextToken: null, + pageSize: 50, + }); + + await act(async () => { + root.unmount(); + }); + }); + it("warns that removing a provider vault only removes Paperclip config", async () => { mockSecretsApi.removeProviderConfig.mockResolvedValueOnce(providerConfigs[1]); const root = createRoot(container); diff --git a/ui/src/pages/Secrets.tsx b/ui/src/pages/Secrets.tsx index 9ca27931f3..7623ca79f1 100644 --- a/ui/src/pages/Secrets.tsx +++ b/ui/src/pages/Secrets.tsx @@ -406,6 +406,7 @@ export function Secrets() { const [usageDialogSecretId, setUsageDialogSecretId] = useState(null); const [createOpen, setCreateOpen] = useState(false); const [importOpen, setImportOpen] = useState(false); + const [importInitialVaultId, setImportInitialVaultId] = useState(null); const [createMode, setCreateMode] = useState("managed"); const [createForm, setCreateForm] = useState({ name: "", @@ -844,6 +845,11 @@ export function Secrets() { setVaultDialogOpen(true); } + function openImportFromVault(config?: CompanySecretProviderConfig | null) { + setImportInitialVaultId(config?.id ?? null); + setImportOpen(true); + } + function applyVaultDiscoveryCandidate(candidate: SecretProviderConfigDiscoveryCandidate) { if (candidate.provider !== "aws_secrets_manager") return; const config = candidate.config as Record; @@ -911,7 +917,7 @@ export function Secrets() { /> setImportOpen(true)} + onClick={() => openImportFromVault()} onManageVaults={() => setActiveTab("vaults")} className="ml-auto" /> @@ -1040,6 +1046,7 @@ export function Secrets() { onRemove={(config) => setRemoveVaultConfirm(config)} onSetDefault={(config) => defaultVaultMutation.mutate(config.id)} onHealthCheck={(config) => healthVaultMutation.mutate(config.id)} + onImportSecrets={openImportFromVault} pendingActionId={ disableVaultMutation.variables ?? removeVaultMutation.variables ?? @@ -1187,12 +1194,17 @@ export function Secrets() { {selectedCompanyId && ( { + setImportOpen(open); + if (!open) setImportInitialVaultId(null); + }} companyId={selectedCompanyId} providerConfigs={providerConfigs} existingSecrets={secrets} + initialProviderConfigId={importInitialVaultId} onManageVaults={() => { setImportOpen(false); + setImportInitialVaultId(null); setActiveTab("vaults"); }} onImportComplete={() => { @@ -1910,6 +1922,7 @@ export function ProviderVaultsTab({ onRemove, onSetDefault, onHealthCheck, + onImportSecrets, pendingActionId, }: { providers: SecretProviderDescriptor[]; @@ -1923,6 +1936,7 @@ export function ProviderVaultsTab({ onRemove: (config: CompanySecretProviderConfig) => void; onSetDefault: (config: CompanySecretProviderConfig) => void; onHealthCheck: (config: CompanySecretProviderConfig) => void; + onImportSecrets: (config: CompanySecretProviderConfig) => void; pendingActionId: string | null; }) { if (loading) { @@ -2004,6 +2018,7 @@ export function ProviderVaultsTab({ onRemove={() => onRemove(config)} onSetDefault={() => onSetDefault(config)} onHealthCheck={() => onHealthCheck(config)} + onImportSecrets={() => onImportSecrets(config)} /> ))} @@ -2023,6 +2038,7 @@ function ProviderVaultCard({ onRemove, onSetDefault, onHealthCheck, + onImportSecrets, }: { config: CompanySecretProviderConfig; pending: boolean; @@ -2031,6 +2047,7 @@ function ProviderVaultCard({ onRemove: () => void; onSetDefault: () => void; onHealthCheck: () => void; + onImportSecrets: () => void; }) { const blockReason = getProviderConfigBlockReason(config); const details = config.healthDetails; @@ -2081,6 +2098,23 @@ function ProviderVaultCard({ {pending ? : } Check health + {config.provider === "aws_secrets_manager" ? ( + + ) : null}