148 lines · 5.8 KB · TypeScript
RawDownload
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>  );}