-
Notifications
You must be signed in to change notification settings - Fork 154
Expand file tree
/
Copy pathsecrets.tsx
More file actions
148 lines (140 loc) · 5.83 KB
/
Copy pathsecrets.tsx
File metadata and controls
148 lines (140 loc) · 5.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import { Suspense } from "react";
import { useAtomRefresh, useAtomValue } from "@effect/atom-react";
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
import type { ProviderKey } from "@executor-js/sdk/shared";
import { useSecretProviderPlugins } from "@executor-js/sdk/client";
import { providersAtom } from "../api/atoms";
import {
CardStack,
CardStackContent,
CardStackEntry,
CardStackEntryActions,
CardStackEntryContent,
CardStackEntryDescription,
CardStackEntryTitle,
CardStackHeader,
} from "../components/card-stack";
import { Badge } from "../components/badge";
import { PageContainer, PageHeader } from "../components/page";
import { useExecutorDocumentTitle } from "../lib/document-title";
import { ErrorState } from "../components/error-state";
import { isAsyncResultLoading } from "../lib/async-result";
// ---------------------------------------------------------------------------
// Providers page (v2) — repurposed from the v1 Secrets page.
//
// v1 stored standalone secrets and bound them per-source. v2 makes a connection
// the credential, and a `CredentialProvider` is where its value lives (the
// default store for pasted values, or an external backend like 1Password /
// keychain). This page surfaces the registered providers plus any provider
// plugin's settings card. The route still exports `SecretsPage` so existing app
// wiring keeps resolving; new callers should treat it as the Providers view.
// ---------------------------------------------------------------------------
const PROVIDER_LABELS: Record<string, string> = {
encrypted: "Encrypted store",
keychain: "Keychain",
file: "Local file",
memory: "Memory",
onepassword: "1Password",
"workos-vault": "WorkOS Vault",
};
const providerLabel = (key: string): string =>
PROVIDER_LABELS[key] ??
key
.split(/[-_]/g)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ");
export function SecretsPage(props: { showProviderInfo?: boolean }) {
useExecutorDocumentTitle("Providers");
const showProviderInfo = props.showProviderInfo ?? true;
const secretProviderPlugins = useSecretProviderPlugins();
const providers = useAtomValue(providersAtom);
const refreshProviders = useAtomRefresh(providersAtom);
return (
<PageContainer>
<PageHeader
title="Providers"
description="Where your connections' credential values live: the default store for pasted values, or an external backend like 1Password or your system keychain."
/>
{/* Provider plugins (settings cards) */}
{showProviderInfo && secretProviderPlugins.length > 0 && (
<div className="mb-10">
<CardStack>
<CardStackHeader>Configure providers</CardStackHeader>
<CardStackContent>
{secretProviderPlugins.map((plugin) => (
<Suspense
key={plugin.key}
fallback={
<div className="px-4 py-3 animate-pulse">
<div className="h-4 w-24 rounded bg-muted" />
</div>
}
>
<plugin.settings />
</Suspense>
))}
</CardStackContent>
</CardStack>
</div>
)}
{/* Registered providers */}
{isAsyncResultLoading(providers) ? (
<div className="flex items-center gap-2 py-8">
<div className="size-1.5 animate-pulse rounded-full bg-muted-foreground/30" />
<p className="text-sm text-muted-foreground">Loading providers…</p>
</div>
) : (
AsyncResult.match(providers, {
onInitial: () => (
<div className="flex items-center gap-2 py-8">
<div className="size-1.5 animate-pulse rounded-full bg-muted-foreground/30" />
<p className="text-sm text-muted-foreground">Loading providers…</p>
</div>
),
onFailure: () => (
<ErrorState message="Failed to load providers" onRetry={refreshProviders} />
),
onSuccess: ({ value }) => (
<CardStack>
<CardStackHeader>Available providers</CardStackHeader>
<CardStackContent>
{value.length === 0 ? (
<CardStackEntry>
<CardStackEntryContent>
<CardStackEntryDescription>
No credential providers are registered.
</CardStackEntryDescription>
</CardStackEntryContent>
</CardStackEntry>
) : (
value.map((key: ProviderKey) => (
<CardStackEntry key={String(key)}>
<CardStackEntryContent>
<CardStackEntryTitle className="flex min-w-0 items-center gap-2">
<span className="min-w-0 shrink truncate">
{providerLabel(String(key))}
</span>
</CardStackEntryTitle>
<CardStackEntryDescription>
{String(key) === "encrypted"
? "Values you paste are encrypted and stored in this instance's database."
: `${providerLabel(String(key))} credential provider.`}
</CardStackEntryDescription>
</CardStackEntryContent>
<CardStackEntryActions>
<Badge variant="secondary">
{String(key) === "encrypted" ? "default" : "provider"}
</Badge>
</CardStackEntryActions>
</CardStackEntry>
))
)}
</CardStackContent>
</CardStack>
),
})
)}
</PageContainer>
);
}