Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useAtomSet } from "@effect/atom-react";
import * as Exit from "effect/Exit";

import { usePendingSources } from "@executor-js/react/api/optimistic";
import { sourceWriteKeys } from "@executor-js/react/api/reactivity-keys";
Expand Down Expand Up @@ -97,22 +98,22 @@ type GoogleDiscoveryTemplate = GoogleDiscoveryPreset & {
const GOOGLE_G_ICON = "https://fonts.gstatic.com/s/i/productlogos/googleg/v6/192px.svg";

function parseGoogleDiscoveryPreset(preset: GoogleDiscoveryPreset): GoogleDiscoveryTemplate {
try {
const url = new URL(preset.url);
const parts = url.pathname.split("/").filter(Boolean);
const apisIndex = parts.indexOf("apis");
const service = apisIndex >= 0 ? parts[apisIndex + 1] : undefined;
const version =
apisIndex >= 0 ? parts[apisIndex + 2] : (url.searchParams.get("version") ?? undefined);
return {
...preset,
discoveryUrl: preset.url,
service: service ?? url.hostname.replace(/\.googleapis\.com$/, ""),
version: version ?? "",
};
} catch {
if (!URL.canParse(preset.url)) {
return { ...preset, discoveryUrl: preset.url, service: preset.id, version: "" };
}

const url = new URL(preset.url);
const parts = url.pathname.split("/").filter(Boolean);
const apisIndex = parts.indexOf("apis");
const service = apisIndex >= 0 ? parts[apisIndex + 1] : undefined;
const version =
apisIndex >= 0 ? parts[apisIndex + 2] : (url.searchParams.get("version") ?? undefined);
return {
...preset,
discoveryUrl: preset.url,
service: service ?? url.hostname.replace(/\.googleapis\.com$/, ""),
version: version ?? "",
};
}

const GOOGLE_DISCOVERY_TEMPLATES = googleDiscoveryPresets.map(parseGoogleDiscoveryPreset);
Expand Down Expand Up @@ -202,8 +203,8 @@ export default function AddGoogleDiscoverySource(props: {
"google";

const scopeId = useScope();
const doProbe = useAtomSet(probeGoogleDiscovery, { mode: "promise" });
const doAdd = useAtomSet(addGoogleDiscoverySource, { mode: "promise" });
const doProbe = useAtomSet(probeGoogleDiscovery, { mode: "promiseExit" });
const doAdd = useAtomSet(addGoogleDiscoverySource, { mode: "promiseExit" });
const { beginAdd } = usePendingSources();
const secretList = useSecretPickerSecrets();
const oauth = useOAuthPopupFlow({
Expand Down Expand Up @@ -235,25 +236,27 @@ export default function AddGoogleDiscoverySource(props: {
setError(null);
setOauthAuth(null);
setShowScopes(false);
try {
const result = await doProbe({
params: { scopeId },
payload: { discoveryUrl: discoveryUrl.trim() },
});
setProbe({
...result,
scopes: [...result.scopes],
operations: [...result.operations],
});
if (result.scopes.length === 0) {
setAuthKind("none");
}
} catch (e) {
const exit = await doProbe({
params: { scopeId },
payload: { discoveryUrl: discoveryUrl.trim() },
});
if (Exit.isFailure(exit)) {
setProbe(null);
setError(e instanceof Error ? e.message : "Failed to inspect discovery document");
} finally {
setError("Failed to inspect discovery document");
setLoadingProbe(false);
return;
}

const result = exit.value;
setProbe({
...result,
scopes: [...result.scopes],
operations: [...result.operations],
});
if (result.scopes.length === 0) {
setAuthKind("none");
}
setLoadingProbe(false);
}, [discoveryUrl, doProbe, scopeId]);

// Keep the latest handleProbe in a ref so the debounced effect can call it
Expand Down Expand Up @@ -331,33 +334,32 @@ export default function AddGoogleDiscoverySource(props: {
name: displayName,
kind: "google-discovery",
});
try {
await doAdd({
params: { scopeId },
payload: {
name: displayName,
discoveryUrl: discoveryUrl.trim(),
namespace,
auth:
authKind === "oauth2" && oauthAuth
? {
kind: "oauth2" as const,
connectionId: oauthAuth.connectionId,
clientIdSecretId: oauthAuth.clientIdSecretId,
clientSecretSecretId: oauthAuth.clientSecretSecretId,
scopes: oauthAuth.scopes,
}
: { kind: "none" as const },
},
reactivityKeys: [...sourceWriteKeys],
});
props.onComplete();
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to add source");
const exit = await doAdd({
params: { scopeId },
payload: {
name: displayName,
discoveryUrl: discoveryUrl.trim(),
namespace,
auth:
authKind === "oauth2" && oauthAuth
? {
kind: "oauth2" as const,
connectionId: oauthAuth.connectionId,
clientIdSecretId: oauthAuth.clientIdSecretId,
clientSecretSecretId: oauthAuth.clientSecretSecretId,
scopes: oauthAuth.scopes,
}
: { kind: "none" as const },
},
reactivityKeys: [...sourceWriteKeys],
});
placeholder.done();
if (Exit.isFailure(exit)) {
setError("Failed to add source");
setAdding(false);
} finally {
placeholder.done();
return;
}
props.onComplete();
}, [
probe,
doAdd,
Expand Down
50 changes: 25 additions & 25 deletions packages/plugins/onepassword/src/react/OnePasswordSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from "react";
import { useAtomSet, useAtomValue } from "@effect/atom-react";
import * as Exit from "effect/Exit";
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
import { ReactivityKey } from "@executor-js/react/api/reactivity-keys";
import { useScope } from "@executor-js/react/api/scope-context";
Expand Down Expand Up @@ -62,15 +63,15 @@ function VaultPicker(props: {
isLoading: true,
error: null,
}),
onError: (error) => ({
onError: () => ({
vaults: [] as { id: string; name: string }[],
isLoading: false,
error: error.message,
error: "Failed to list vaults",
}),
onDefect: (defect) => ({
onDefect: () => ({
vaults: [] as { id: string; name: string }[],
isLoading: false,
error: defect instanceof Error ? defect.message : "Failed to list vaults",
error: "Failed to list vaults",
}),
onSuccess: ({ value }) => {
const v = value.vaults;
Expand Down Expand Up @@ -142,7 +143,7 @@ function ConfigDialog(props: {
const [error, setError] = useState<string | null>(null);

const scopeId = useScope();
const doConfigure = useAtomSet(configureOnePassword, { mode: "promise" });
const doConfigure = useAtomSet(configureOnePassword, { mode: "promiseExit" });

const reset = () => {
if (!isEdit) {
Expand All @@ -159,23 +160,26 @@ function ConfigDialog(props: {
if (!accountName.trim() || !vaultId.trim()) return;
setSaving(true);
setError(null);
try {
const auth =
authKind === "desktop-app"
? { kind: "desktop-app" as const, accountName: accountName.trim() }
: { kind: "service-account" as const, tokenSecretId: accountName.trim() };

await doConfigure({
params: { scopeId },
payload: { auth, vaultId: vaultId.trim(), name: vaultName.trim() || "1Password" },
reactivityKeys: [ReactivityKey.secrets],
});
props.onOpenChange(false);
reset();
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to save configuration");
const auth =
authKind === "desktop-app"
? { kind: "desktop-app" as const, accountName: accountName.trim() }
: { kind: "service-account" as const, tokenSecretId: accountName.trim() };

const exit = await doConfigure({
params: { scopeId },
payload: { auth, vaultId: vaultId.trim(), name: vaultName.trim() || "1Password" },
reactivityKeys: [ReactivityKey.secrets],
});

if (Exit.isFailure(exit)) {
setError("Failed to save configuration");
setSaving(false);
return;
}

props.onOpenChange(false);
reset();
};

return (
Expand Down Expand Up @@ -298,14 +302,10 @@ export default function OnePasswordSettings() {
const [configOpen, setConfigOpen] = useState(false);
const scopeId = useScope();
const configResult = useAtomValue(onepasswordConfigAtom(scopeId));
const doRemove = useAtomSet(removeOnePasswordConfig, { mode: "promise" });
const doRemove = useAtomSet(removeOnePasswordConfig, { mode: "promiseExit" });

const handleRemove = async () => {
try {
await doRemove({ params: { scopeId }, reactivityKeys: [ReactivityKey.secrets] });
} catch {
/* TODO: toast */
}
await doRemove({ params: { scopeId }, reactivityKeys: [ReactivityKey.secrets] });
};

const config: OnePasswordConfig | null = AsyncResult.match(
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/api/scope-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export function ScopeProvider(props: React.PropsWithChildren<{ fallback?: React.
export function useScope(): ScopeId {
const scope = React.useContext(ScopeContext);
if (scope === null) {
// oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: React hook invariant
throw new Error("useScope must be used inside a ScopeProvider");
}
return scope.id;
Expand All @@ -53,6 +54,7 @@ export function useScope(): ScopeId {
export function useScopeInfo(): ScopeInfo {
const scope = React.useContext(ScopeContext);
if (scope === null) {
// oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: React hook invariant
throw new Error("useScopeInfo must be used inside a ScopeProvider");
}
return scope;
Expand All @@ -66,6 +68,7 @@ export function useUserScope(): ScopeId {
const stack = useScopeStack();
const innermost = stack[0];
if (!innermost) {
// oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: React hook invariant
throw new Error("useUserScope requires a non-empty scope stack");
}
return innermost.id;
Expand Down
41 changes: 21 additions & 20 deletions packages/react/src/plugins/secret-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,8 @@ import {
} from "../components/select";
import type { VariantProps } from "class-variance-authority";

import {
getUniqueSecretId,
isSecretIdTaken,
} from "./secret-id";
import { getUniqueSecretId, isSecretIdTaken } from "./secret-id";
import * as Exit from "effect/Exit";

// ---------------------------------------------------------------------------
// Context
Expand Down Expand Up @@ -78,6 +76,7 @@ const SecretFormContext = createContext<SecretFormContextValue | null>(null);
function useSecretForm(): SecretFormContextValue {
const ctx = use(SecretFormContext);
if (!ctx) {
// oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: React context invariant
throw new Error("SecretForm parts must be rendered inside <SecretForm.Provider>");
}
return ctx;
Expand Down Expand Up @@ -110,7 +109,7 @@ function SecretFormProvider(props: SecretFormProviderProps) {

const defaultScope = useScope();
const scopeId = scopeIdProp ?? defaultScope;
const doSet = useAtomSet(setSecret, { mode: "promise" });
const doSet = useAtomSet(setSecret, { mode: "promiseExit" });

const [state, setState] = useState<SecretFormState>(() => ({
name: "",
Expand Down Expand Up @@ -142,27 +141,27 @@ function SecretFormProvider(props: SecretFormProviderProps) {
const submit = async () => {
if (!canSubmit) return;
setState((s) => ({ ...s, status: { kind: "submitting" } }));
try {
await doSet({
params: { scopeId },
payload: {
id: SecretId.make(id.trim()),
name: displayName || id.trim(),
value: state.value.trim(),
provider: state.provider === "auto" ? undefined : state.provider,
},
reactivityKeys: secretWriteKeys,
});
onCreated(id.trim());
} catch (e) {
const exit = await doSet({
params: { scopeId },
payload: {
id: SecretId.make(id.trim()),
name: displayName || id.trim(),
value: state.value.trim(),
provider: state.provider === "auto" ? undefined : state.provider,
},
reactivityKeys: secretWriteKeys,
});
if (Exit.isFailure(exit)) {
setState((s) => ({
...s,
status: {
kind: "error",
message: e instanceof Error ? e.message : "Failed to save secret",
message: "Failed to save secret",
},
}));
return;
}
onCreated(id.trim());
};

const value: SecretFormContextValue = {
Expand Down Expand Up @@ -255,7 +254,9 @@ function ValueField(props: { revealable?: boolean; placeholder?: string }) {
</Button>
)}
</div>
{errored && <FieldError>{state.status.kind === "error" ? state.status.message : ""}</FieldError>}
{errored && (
<FieldError>{state.status.kind === "error" ? state.status.message : ""}</FieldError>
)}
</Field>
);
}
Expand Down
Loading