Preselect vault when importing secrets from a specific vault

Adds an Import secrets action to each provider vault row that opens the
ImportFromVaultDialog pre-scoped to that vault via initialProviderConfigId,
and resets the preselection when the dialog closes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 4.8 committed 2026-06-25 00:19:38 +00:00
1 parent bac15ebd09
commit df532d85f0
3 files changed
+117 -4

No files matched your search

+69
View File
@@ -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> = {},
): 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(
<MemoryRouter>
<QueryClientProvider client={queryClient}>
<Secrets />
</QueryClientProvider>
</MemoryRouter>,
);
});
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);
+38 -2
View File
@@ -406,6 +406,7 @@ export function Secrets() {
const [usageDialogSecretId, setUsageDialogSecretId] = useState<string | null>(null);
const [createOpen, setCreateOpen] = useState(false);
const [importOpen, setImportOpen] = useState(false);
const [importInitialVaultId, setImportInitialVaultId] = useState<string | null>(null);
const [createMode, setCreateMode] = useState<CreateMode>("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<string, unknown>;
@@ -911,7 +917,7 @@ export function Secrets() {
/>
<ImportFromVaultButton
providerConfigs={providerConfigs}
onClick={() => 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 && (
<ImportFromVaultDialog
open={importOpen}
onOpenChange={setImportOpen}
onOpenChange={(open) => {
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)}
/>
))}
</div>
@@ -2023,6 +2038,7 @@ function ProviderVaultCard({
onRemove,
onSetDefault,
onHealthCheck,
onImportSecrets,
}: {
config: CompanySecretProviderConfig;
pending: boolean;
@@ -2031,9 +2047,12 @@ function ProviderVaultCard({
onRemove: () => void;
onSetDefault: () => void;
onHealthCheck: () => void;
onImportSecrets: () => void;
}) {
const blockReason = getProviderConfigBlockReason(config);
const details = config.healthDetails;
const canRefreshRemoteSecrets =
config.provider === "aws_secrets_manager" && !blockReason;
return (
<div className="rounded-md border border-border bg-background p-4">
<div className="flex items-start gap-3">
@@ -2081,6 +2100,23 @@ function ProviderVaultCard({
{pending ? <Loader2 className="h-3.5 w-3.5 animate-spin mr-1" /> : <RefreshCw className="h-3.5 w-3.5 mr-1" />}
Check health
</Button>
{config.provider === "aws_secrets_manager" ? (
<Button
variant="outline"
size="sm"
onClick={onImportSecrets}
disabled={pending || !canRefreshRemoteSecrets}
title={
canRefreshRemoteSecrets
? "Refresh AWS metadata and import existing secrets"
: blockReason ?? "This vault cannot refresh secrets right now."
}
data-testid={`provider-vault-refresh-secrets-${config.id}`}
>
<Cloud className="h-3.5 w-3.5 mr-1" />
Refresh secrets
</Button>
) : null}
<Button
variant="outline"
size="sm"
+10 -2
View File
@@ -60,6 +60,7 @@ interface ImportFromVaultDialogProps {
companyId: string;
providerConfigs: CompanySecretProviderConfig[];
existingSecrets: CompanySecret[];
initialProviderConfigId?: string | null;
onImportComplete?: (result: RemoteSecretImportResult) => void;
onManageVaults?: () => void;
}
@@ -83,9 +84,15 @@ function eligibleVaults(configs: CompanySecretProviderConfig[]): CompanySecretPr
return configs.filter(isAwsSelectable);
}
function pickDefaultVault(configs: CompanySecretProviderConfig[]): string | null {
function pickDefaultVault(
configs: CompanySecretProviderConfig[],
preferredId?: string | null,
): string | null {
const eligible = eligibleVaults(configs);
if (eligible.length === 0) return null;
if (preferredId && eligible.some((vault) => vault.id === preferredId)) {
return preferredId;
}
return (eligible.find((vault) => vault.isDefault) ?? eligible[0]).id;
}
@@ -327,6 +334,7 @@ export function ImportFromVaultDialog({
companyId,
providerConfigs,
existingSecrets,
initialProviderConfigId,
onImportComplete,
onManageVaults,
}: ImportFromVaultDialogProps) {
@@ -360,7 +368,7 @@ export function ImportFromVaultDialog({
setSelection(new Map());
setImportResult(null);
setShowOnlySelected(false);
const next = pickDefaultVault(providerConfigs);
const next = pickDefaultVault(providerConfigs, initialProviderConfigId);
setVaultId(next);
// We deliberately depend only on open so that re-opens reset the dialog;
// providerConfigs changes during a session are handled by next preview fetch.