Skip to content

Commit 7265745

Browse files
authored
Add remove in-use toasts (#530)
1 parent a2e9835 commit 7265745

3 files changed

Lines changed: 68 additions & 23 deletions

File tree

packages/react/src/api/atoms.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,26 @@ export const updatePolicy = ExecutorApiClient.mutation("policies", "update");
153153

154154
export const removePolicy = ExecutorApiClient.mutation("policies", "remove");
155155

156+
// ---------------------------------------------------------------------------
157+
// Secrets — optimistic removals.
158+
// ---------------------------------------------------------------------------
159+
160+
export const secretsOptimisticAtom = Atom.family((scopeId: ScopeId) =>
161+
Atom.optimistic(secretsAtom(scopeId)),
162+
);
163+
164+
export const removeSecretOptimistic = Atom.family((scopeId: ScopeId) =>
165+
secretsOptimisticAtom(scopeId).pipe(
166+
Atom.optimisticFn({
167+
reducer: (current, arg) =>
168+
AsyncResult.map(current, (rows) =>
169+
rows.filter((secret) => secret.id !== arg.params.secretId),
170+
),
171+
fn: removeSecret,
172+
}),
173+
),
174+
);
175+
156176
// ---------------------------------------------------------------------------
157177
// Policies — optimistic surface. Reads go through `policiesOptimisticAtom`
158178
// (which layers in-flight transitions on top of `policiesAtom`), and writes

packages/react/src/pages/connections.tsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Suspense } from "react";
22
import { useAtomValue, useAtomSet } from "@effect/atom-react";
33
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
4-
import { ConnectionId, type ScopeId } from "@executor-js/sdk";
4+
import * as Exit from "effect/Exit";
5+
import * as Option from "effect/Option";
6+
import { ConnectionId, ConnectionInUseError, type ScopeId } from "@executor-js/sdk";
57
import { toast } from "sonner";
68

79
import { connectionUsagesAtom, removeConnection } from "../api/atoms";
@@ -161,18 +163,25 @@ export function ConnectionsPage() {
161163
const scopeStack = useScopeStack();
162164
const connections = useConnectionsWithPendingRemovals(scopeId);
163165
const { beginRemove } = usePendingConnectionRemovals();
164-
const doRemove = useAtomSet(removeConnection, { mode: "promise" });
166+
const doRemove = useAtomSet(removeConnection, { mode: "promiseExit" });
165167

166168
const handleRemove = async (connectionId: string) => {
167169
const pending = beginRemove(connectionId);
168-
try {
169-
await doRemove({
170-
params: { scopeId, connectionId: ConnectionId.make(connectionId) },
171-
reactivityKeys: connectionWriteKeys,
172-
});
173-
} catch (e) {
170+
const exit = await doRemove({
171+
params: { scopeId, connectionId: ConnectionId.make(connectionId) },
172+
reactivityKeys: connectionWriteKeys,
173+
});
174+
if (Exit.isFailure(exit)) {
174175
pending.undo();
175-
toast.error(e instanceof Error ? e.message : "Failed to remove connection");
176+
const error = Exit.findErrorOption(exit);
177+
if (Option.isSome(error) && error.value instanceof ConnectionInUseError) {
178+
const count = error.value.usageCount;
179+
toast.error(
180+
`Connection is used by ${count} ${count === 1 ? "source" : "sources"}. Detach it before removing it.`,
181+
);
182+
} else {
183+
toast.error("Failed to remove connection");
184+
}
176185
}
177186
};
178187

packages/react/src/pages/secrets.tsx

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import { useMemo, useState, Suspense } from "react";
22
import { useAtomValue, useAtomSet } from "@effect/atom-react";
33
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
4-
import { secretsAtom, secretUsagesAtom, removeSecret } from "../api/atoms";
4+
import * as Exit from "effect/Exit";
5+
import * as Option from "effect/Option";
6+
import { toast } from "sonner";
7+
import {
8+
removeSecretOptimistic,
9+
secretsOptimisticAtom,
10+
secretUsagesAtom,
11+
} from "../api/atoms";
512
import { secretWriteKeys } from "../api/reactivity-keys";
613
import { useSecretProviderPlugins } from "@executor-js/sdk/client";
7-
import { SecretId, type ScopeId } from "@executor-js/sdk";
14+
import { SecretId, SecretInUseError, type ScopeId } from "@executor-js/sdk";
815
import { SecretForm } from "../plugins/secret-form";
916
import { useScope } from "../hooks/use-scope";
1017
import {
@@ -234,7 +241,7 @@ export function SecretsPage(props: {
234241
const secretProviderPlugins = useSecretProviderPlugins();
235242
const [addOpen, setAddOpen] = useState(false);
236243
const scopeId = useScope();
237-
const secrets = useAtomValue(secretsAtom(scopeId));
244+
const secrets = useAtomValue(secretsOptimisticAtom(scopeId));
238245
const existingSecretIds = useMemo(
239246
() =>
240247
AsyncResult.match(secrets, {
@@ -244,19 +251,28 @@ export function SecretsPage(props: {
244251
}),
245252
[secrets],
246253
);
247-
const doRemove = useAtomSet(removeSecret, { mode: "promise" });
254+
const doRemove = useAtomSet(removeSecretOptimistic(scopeId), {
255+
mode: "promiseExit",
256+
});
248257

249258
const handleRemove = async (secretId: string) => {
250-
try {
251-
await doRemove({
252-
params: {
253-
scopeId,
254-
secretId: SecretId.make(secretId),
255-
},
256-
reactivityKeys: secretWriteKeys,
257-
});
258-
} catch {
259-
// TODO: toast
259+
const exit = await doRemove({
260+
params: {
261+
scopeId,
262+
secretId: SecretId.make(secretId),
263+
},
264+
reactivityKeys: secretWriteKeys,
265+
});
266+
if (Exit.isFailure(exit)) {
267+
const error = Exit.findErrorOption(exit);
268+
if (Option.isSome(error) && error.value instanceof SecretInUseError) {
269+
const count = error.value.usageCount;
270+
toast.error(
271+
`Secret is used by ${count} ${count === 1 ? "source" : "sources"}. Detach it before removing it.`,
272+
);
273+
} else {
274+
toast.error("Failed to remove secret");
275+
}
260276
}
261277
};
262278

0 commit comments

Comments
 (0)