@agent-native/dispatch 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/actions/create-pylon-ticket.d.ts +3 -0
- package/dist/actions/create-pylon-ticket.d.ts.map +1 -0
- package/dist/actions/create-pylon-ticket.js +94 -0
- package/dist/actions/create-pylon-ticket.js.map +1 -0
- package/dist/actions/create-vault-grant.js +1 -1
- package/dist/actions/create-vault-grant.js.map +1 -1
- package/dist/actions/create-vault-secret.d.ts.map +1 -1
- package/dist/actions/create-vault-secret.js +4 -3
- package/dist/actions/create-vault-secret.js.map +1 -1
- package/dist/actions/get-vault-access-settings.d.ts +3 -0
- package/dist/actions/get-vault-access-settings.d.ts.map +1 -0
- package/dist/actions/get-vault-access-settings.js +10 -0
- package/dist/actions/get-vault-access-settings.js.map +1 -0
- package/dist/actions/grant-vault-secrets-to-app.js +1 -1
- package/dist/actions/grant-vault-secrets-to-app.js.map +1 -1
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +8 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-integrations-catalog.js +1 -1
- package/dist/actions/list-integrations-catalog.js.map +1 -1
- package/dist/actions/list-vault-grants.js +1 -1
- package/dist/actions/list-vault-grants.js.map +1 -1
- package/dist/actions/list-workspace-apps.d.ts.map +1 -1
- package/dist/actions/list-workspace-apps.js +5 -1
- package/dist/actions/list-workspace-apps.js.map +1 -1
- package/dist/actions/set-vault-access-settings.d.ts +3 -0
- package/dist/actions/set-vault-access-settings.d.ts.map +1 -0
- package/dist/actions/set-vault-access-settings.js +13 -0
- package/dist/actions/set-vault-access-settings.js.map +1 -0
- package/dist/actions/start-workspace-app-creation.d.ts.map +1 -1
- package/dist/actions/start-workspace-app-creation.js +6 -0
- package/dist/actions/start-workspace-app-creation.js.map +1 -1
- package/dist/actions/sync-vault-to-app.js +1 -1
- package/dist/actions/sync-vault-to-app.js.map +1 -1
- package/dist/actions/update-workspace-app-metadata.d.ts +3 -0
- package/dist/actions/update-workspace-app-metadata.d.ts.map +1 -0
- package/dist/actions/update-workspace-app-metadata.js +30 -0
- package/dist/actions/update-workspace-app-metadata.js.map +1 -0
- package/dist/actions/view-screen.d.ts.map +1 -1
- package/dist/actions/view-screen.js +4 -2
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/app-keys-popover.d.ts.map +1 -1
- package/dist/components/app-keys-popover.js +17 -5
- package/dist/components/app-keys-popover.js.map +1 -1
- package/dist/components/create-app-popover.d.ts.map +1 -1
- package/dist/components/create-app-popover.js +38 -14
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/dispatch-shell.d.ts +4 -4
- package/dist/components/dispatch-shell.d.ts.map +1 -1
- package/dist/components/dispatch-shell.js +6 -6
- package/dist/components/dispatch-shell.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +10 -3
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/messaging-setup-panel.d.ts.map +1 -1
- package/dist/components/messaging-setup-panel.js +2 -2
- package/dist/components/messaging-setup-panel.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +41 -2
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/hooks/use-navigation-state.js +12 -5
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/lib/catch-all-target.d.ts +2 -0
- package/dist/lib/catch-all-target.d.ts.map +1 -0
- package/dist/lib/catch-all-target.js +95 -0
- package/dist/lib/catch-all-target.js.map +1 -0
- package/dist/lib/workspace-apps.d.ts +9 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/pages/$appId.d.ts +2 -24
- package/dist/routes/pages/$appId.d.ts.map +1 -1
- package/dist/routes/pages/$appId.js +42 -8
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/routes/pages/approval.d.ts.map +1 -1
- package/dist/routes/pages/approval.js +2 -1
- package/dist/routes/pages/approval.js.map +1 -1
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +2 -1
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/integrations.d.ts.map +1 -1
- package/dist/routes/pages/integrations.js +20 -15
- package/dist/routes/pages/integrations.js.map +1 -1
- package/dist/routes/pages/new-app.js +1 -1
- package/dist/routes/pages/new-app.js.map +1 -1
- package/dist/routes/pages/overview.d.ts.map +1 -1
- package/dist/routes/pages/overview.js +14 -1
- package/dist/routes/pages/overview.js.map +1 -1
- package/dist/routes/pages/vault.d.ts.map +1 -1
- package/dist/routes/pages/vault.js +25 -6
- package/dist/routes/pages/vault.js.map +1 -1
- package/dist/routes/pages/workspace.d.ts.map +1 -1
- package/dist/routes/pages/workspace.js +5 -3
- package/dist/routes/pages/workspace.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts +13 -0
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +295 -9
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/env-config.d.ts.map +1 -1
- package/dist/server/lib/env-config.js +5 -0
- package/dist/server/lib/env-config.js.map +1 -1
- package/dist/server/lib/onboarding-steps.d.ts +12 -0
- package/dist/server/lib/onboarding-steps.d.ts.map +1 -0
- package/dist/server/lib/onboarding-steps.js +47 -0
- package/dist/server/lib/onboarding-steps.js.map +1 -0
- package/dist/server/lib/vault-store.d.ts +55 -0
- package/dist/server/lib/vault-store.d.ts.map +1 -1
- package/dist/server/lib/vault-store.js +210 -41
- package/dist/server/lib/vault-store.js.map +1 -1
- package/dist/server/plugins/agent-chat.d.ts.map +1 -1
- package/dist/server/plugins/agent-chat.js +2 -1
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/server/plugins/core-routes.d.ts.map +1 -1
- package/dist/server/plugins/core-routes.js +4 -0
- package/dist/server/plugins/core-routes.js.map +1 -1
- package/dist/server/plugins/integrations.js +2 -2
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +13 -11
- package/src/actions/create-pylon-ticket.ts +109 -0
- package/src/actions/create-vault-grant.ts +1 -1
- package/src/actions/create-vault-secret.ts +4 -3
- package/src/actions/get-vault-access-settings.ts +11 -0
- package/src/actions/grant-vault-secrets-to-app.ts +1 -1
- package/src/actions/index.ts +8 -0
- package/src/actions/list-integrations-catalog.ts +1 -1
- package/src/actions/list-vault-grants.ts +1 -1
- package/src/actions/list-workspace-apps.ts +5 -1
- package/src/actions/set-vault-access-settings.ts +16 -0
- package/src/actions/start-workspace-app-creation.ts +8 -0
- package/src/actions/sync-vault-to-app.ts +1 -1
- package/src/actions/update-workspace-app-metadata.ts +32 -0
- package/src/actions/view-screen.ts +4 -1
- package/src/components/app-keys-popover.tsx +38 -8
- package/src/components/create-app-popover.tsx +47 -14
- package/src/components/dispatch-shell.tsx +16 -15
- package/src/components/layout/Layout.tsx +11 -5
- package/src/components/messaging-setup-panel.tsx +54 -39
- package/src/components/workspace-app-card.tsx +102 -0
- package/src/hooks/use-navigation-state.ts +10 -4
- package/src/lib/catch-all-target.spec.ts +218 -0
- package/src/lib/catch-all-target.ts +99 -0
- package/src/lib/workspace-apps.ts +9 -0
- package/src/routes/pages/$appId.tsx +45 -7
- package/src/routes/pages/approval.tsx +33 -3
- package/src/routes/pages/apps.$appId.tsx +6 -1
- package/src/routes/pages/integrations.tsx +57 -18
- package/src/routes/pages/new-app.tsx +1 -1
- package/src/routes/pages/overview.tsx +69 -29
- package/src/routes/pages/vault.tsx +101 -21
- package/src/routes/pages/workspace.tsx +21 -3
- package/src/server/lib/app-creation-store.spec.ts +61 -2
- package/src/server/lib/app-creation-store.ts +386 -11
- package/src/server/lib/env-config.ts +5 -0
- package/src/server/lib/onboarding-steps.ts +49 -0
- package/src/server/lib/vault-store.spec.ts +69 -0
- package/src/server/lib/vault-store.ts +266 -49
- package/src/server/plugins/agent-chat.ts +2 -1
- package/src/server/plugins/core-routes.ts +5 -0
- package/src/server/plugins/integrations.ts +2 -2
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { useEffect, useMemo } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Link,
|
|
4
|
+
Navigate,
|
|
4
5
|
redirect,
|
|
5
6
|
useParams,
|
|
7
|
+
type ClientLoaderFunctionArgs,
|
|
6
8
|
type LoaderFunctionArgs,
|
|
7
9
|
} from "react-router";
|
|
8
10
|
import { useActionQuery, appPath } from "@agent-native/core/client";
|
|
9
|
-
import { loadWorkspaceAppsManifest } from "@agent-native/core/server/agent-discovery";
|
|
10
11
|
import {
|
|
11
12
|
IconArrowLeft,
|
|
12
13
|
IconArrowUpRight,
|
|
@@ -16,6 +17,7 @@ import { DispatchShell } from "@/components/dispatch-shell";
|
|
|
16
17
|
import { Spinner } from "@/components/ui/spinner";
|
|
17
18
|
import { Badge } from "@/components/ui/badge";
|
|
18
19
|
import { Button } from "@/components/ui/button";
|
|
20
|
+
import { resolveCatchAllTarget } from "@/lib/catch-all-target";
|
|
19
21
|
import {
|
|
20
22
|
workspaceAppHref,
|
|
21
23
|
type WorkspaceAppSummary,
|
|
@@ -47,19 +49,49 @@ export function meta() {
|
|
|
47
49
|
* OAuth callbackURL, so Google sign-in completes back at /dispatch/todo
|
|
48
50
|
* and looks broken. This route fixes both the post-creation navigation
|
|
49
51
|
* and the OAuth round-trip from a single place.
|
|
52
|
+
*
|
|
53
|
+
* Built-in template fallback: when no workspace manifest is available
|
|
54
|
+
* (framework dev with each template on its own port, hosted dispatch with
|
|
55
|
+
* no sibling apps), redirect to the matching first-party template's deploy
|
|
56
|
+
* URL — `http://localhost:<devPort>` in dev, `https://<id>.agent-native.com`
|
|
57
|
+
* in production. Without this, a user visiting `/forms` on dispatch is
|
|
58
|
+
* forced to sign in (auth guard) and then lands on this route's "Page not
|
|
59
|
+
* found" pane after the post-login reload.
|
|
60
|
+
*
|
|
61
|
+
* `appId === "dispatch"` short-circuit: when the segment matches Dispatch
|
|
62
|
+
* itself (e.g. `/dispatch/dispatch`), we go straight to the overview rather
|
|
63
|
+
* than chaining through `/dispatch` (which polled `useActionQuery` re-fired
|
|
64
|
+
* `window.location.assign` against and looped forever in production).
|
|
50
65
|
*/
|
|
66
|
+
function dispatchSelfRedirect(appId: string | undefined): string | null {
|
|
67
|
+
if (appId === "dispatch") return appPath("/overview");
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
51
71
|
export function loader({ params }: LoaderFunctionArgs) {
|
|
52
72
|
const appId = params.appId;
|
|
53
73
|
if (!appId) return null;
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
-
const
|
|
57
|
-
const target =
|
|
58
|
-
app?.path && app.path.startsWith("/") ? app.path : app ? `/${appId}` : null;
|
|
74
|
+
const selfTarget = dispatchSelfRedirect(appId);
|
|
75
|
+
if (selfTarget) throw redirect(selfTarget);
|
|
76
|
+
const target = resolveCatchAllTarget(appId);
|
|
59
77
|
if (target) throw redirect(target);
|
|
60
78
|
return null;
|
|
61
79
|
}
|
|
62
80
|
|
|
81
|
+
export async function clientLoader({
|
|
82
|
+
params,
|
|
83
|
+
serverLoader,
|
|
84
|
+
}: ClientLoaderFunctionArgs) {
|
|
85
|
+
const selfTarget = dispatchSelfRedirect(params.appId);
|
|
86
|
+
if (selfTarget) throw redirect(selfTarget);
|
|
87
|
+
// Defer to the server loader so the built-in template fallback runs on
|
|
88
|
+
// SPA navigations too (e.g. clicking a `/<template-id>` link inside
|
|
89
|
+
// dispatch). Without this the client side would only check the workspace
|
|
90
|
+
// apps query, which never lists the static first-party templates and so
|
|
91
|
+
// the user would land on the "Page not found" pane.
|
|
92
|
+
return serverLoader();
|
|
93
|
+
}
|
|
94
|
+
|
|
63
95
|
export default function WorkspaceAppCatchAllRoute() {
|
|
64
96
|
const { appId } = useParams();
|
|
65
97
|
const { data: apps = [], isLoading } = useActionQuery(
|
|
@@ -73,11 +105,17 @@ export default function WorkspaceAppCatchAllRoute() {
|
|
|
73
105
|
[appId, apps],
|
|
74
106
|
);
|
|
75
107
|
const href = app ? workspaceAppHref(app) : null;
|
|
108
|
+
const isSelfReference = appId === "dispatch";
|
|
76
109
|
|
|
77
110
|
useEffect(() => {
|
|
111
|
+
if (isSelfReference) return;
|
|
78
112
|
if (!app || app.status === "pending" || !href) return;
|
|
79
113
|
window.location.assign(href);
|
|
80
|
-
}, [app, href]);
|
|
114
|
+
}, [app, href, isSelfReference]);
|
|
115
|
+
|
|
116
|
+
if (isSelfReference) {
|
|
117
|
+
return <Navigate to={appPath("/overview")} replace />;
|
|
118
|
+
}
|
|
81
119
|
|
|
82
120
|
if ((isLoading && !app) || (app && app.status !== "pending" && href)) {
|
|
83
121
|
return (
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { toast } from "sonner";
|
|
10
10
|
import { Button } from "@/components/ui/button";
|
|
11
11
|
import { Badge } from "@/components/ui/badge";
|
|
12
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
12
13
|
import {
|
|
13
14
|
IconCheck,
|
|
14
15
|
IconX,
|
|
@@ -98,9 +99,38 @@ export default function ApprovalPreviewRoute() {
|
|
|
98
99
|
|
|
99
100
|
if (isLoading) {
|
|
100
101
|
return (
|
|
101
|
-
<div className="flex min-h-screen items-
|
|
102
|
-
<div className="w-full max-w-md
|
|
103
|
-
<
|
|
102
|
+
<div className="flex min-h-screen items-start justify-center bg-background p-6">
|
|
103
|
+
<div className="w-full max-w-md space-y-4">
|
|
104
|
+
<div className="rounded-2xl border bg-card p-5">
|
|
105
|
+
<div className="flex items-start gap-3">
|
|
106
|
+
<Skeleton className="h-9 w-9 shrink-0 rounded-xl" />
|
|
107
|
+
<div className="min-w-0 flex-1 space-y-2">
|
|
108
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
109
|
+
<Skeleton className="h-4 w-40" />
|
|
110
|
+
<Skeleton className="h-5 w-20 rounded-full" />
|
|
111
|
+
</div>
|
|
112
|
+
<Skeleton className="h-3 w-32" />
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<div className="mt-4 space-y-2 rounded-xl border bg-muted/30 px-4 py-3">
|
|
116
|
+
<div className="flex justify-between gap-4">
|
|
117
|
+
<Skeleton className="h-3 w-20" />
|
|
118
|
+
<Skeleton className="h-3 w-24" />
|
|
119
|
+
</div>
|
|
120
|
+
<div className="flex justify-between gap-4">
|
|
121
|
+
<Skeleton className="h-3 w-20" />
|
|
122
|
+
<Skeleton className="h-3 w-28" />
|
|
123
|
+
</div>
|
|
124
|
+
<div className="flex justify-between gap-4">
|
|
125
|
+
<Skeleton className="h-3 w-16" />
|
|
126
|
+
<Skeleton className="h-3 w-32" />
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<div className="mt-4 flex gap-2">
|
|
130
|
+
<Skeleton className="h-8 flex-1 rounded-md" />
|
|
131
|
+
<Skeleton className="h-8 flex-1 rounded-md" />
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
104
134
|
</div>
|
|
105
135
|
</div>
|
|
106
136
|
);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { DispatchShell } from "@/components/dispatch-shell";
|
|
10
10
|
import { Badge } from "@/components/ui/badge";
|
|
11
11
|
import { Button } from "@/components/ui/button";
|
|
12
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
12
13
|
import {
|
|
13
14
|
workspaceAppHref,
|
|
14
15
|
type WorkspaceAppSummary,
|
|
@@ -53,7 +54,11 @@ export default function WorkspaceAppRoute() {
|
|
|
53
54
|
</Button>
|
|
54
55
|
|
|
55
56
|
{isLoading && !app ? (
|
|
56
|
-
<
|
|
57
|
+
<div className="space-y-3">
|
|
58
|
+
<Skeleton className="h-5 w-48" />
|
|
59
|
+
<Skeleton className="h-4 w-full" />
|
|
60
|
+
<Skeleton className="h-4 w-2/3" />
|
|
61
|
+
</div>
|
|
57
62
|
) : !app ? (
|
|
58
63
|
<div className="space-y-3">
|
|
59
64
|
<h2 className="text-base font-semibold text-foreground">
|
|
@@ -28,6 +28,11 @@ import {
|
|
|
28
28
|
} from "@/components/ui/dialog";
|
|
29
29
|
import { Input } from "@/components/ui/input";
|
|
30
30
|
import { Label } from "@/components/ui/label";
|
|
31
|
+
import {
|
|
32
|
+
Tooltip,
|
|
33
|
+
TooltipContent,
|
|
34
|
+
TooltipTrigger,
|
|
35
|
+
} from "@/components/ui/tooltip";
|
|
31
36
|
|
|
32
37
|
export function meta() {
|
|
33
38
|
return [{ title: "Connections — Dispatch" }];
|
|
@@ -90,10 +95,12 @@ function ConnectDialog({
|
|
|
90
95
|
service,
|
|
91
96
|
open,
|
|
92
97
|
onOpenChange,
|
|
98
|
+
accessMode,
|
|
93
99
|
}: {
|
|
94
100
|
service: Service;
|
|
95
101
|
open: boolean;
|
|
96
102
|
onOpenChange: (next: boolean) => void;
|
|
103
|
+
accessMode: "all-apps" | "manual";
|
|
97
104
|
}) {
|
|
98
105
|
const [value, setValue] = useState("");
|
|
99
106
|
const qc = useQueryClient();
|
|
@@ -128,16 +135,18 @@ function ConnectDialog({
|
|
|
128
135
|
throw new Error("Secret created but id missing");
|
|
129
136
|
}
|
|
130
137
|
|
|
131
|
-
// 2.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
// 2. Manual mode needs grants; all-apps mode only needs sync.
|
|
139
|
+
if (accessMode === "manual") {
|
|
140
|
+
const targets = service.apps.filter((a) => !a.vaultGranted);
|
|
141
|
+
for (const app of targets) {
|
|
142
|
+
try {
|
|
143
|
+
await createGrant.mutateAsync({
|
|
144
|
+
secretId,
|
|
145
|
+
appId: app.appId,
|
|
146
|
+
});
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.warn(`grant to ${app.appId} failed`, err);
|
|
149
|
+
}
|
|
141
150
|
}
|
|
142
151
|
}
|
|
143
152
|
for (const app of service.apps) {
|
|
@@ -217,7 +226,13 @@ function ConnectDialog({
|
|
|
217
226
|
);
|
|
218
227
|
}
|
|
219
228
|
|
|
220
|
-
function ConnectorCard({
|
|
229
|
+
function ConnectorCard({
|
|
230
|
+
service,
|
|
231
|
+
accessMode,
|
|
232
|
+
}: {
|
|
233
|
+
service: Service;
|
|
234
|
+
accessMode: "all-apps" | "manual";
|
|
235
|
+
}) {
|
|
221
236
|
const [open, setOpen] = useState(false);
|
|
222
237
|
const isConnected = service.apps.some((a) => a.configured);
|
|
223
238
|
const appCount = service.apps.length;
|
|
@@ -251,10 +266,15 @@ function ConnectorCard({ service }: { service: Service }) {
|
|
|
251
266
|
</Badge>
|
|
252
267
|
)}
|
|
253
268
|
</div>
|
|
254
|
-
<div className="min-w-0">
|
|
255
|
-
<
|
|
256
|
-
|
|
257
|
-
|
|
269
|
+
<div className="w-full min-w-0">
|
|
270
|
+
<Tooltip>
|
|
271
|
+
<TooltipTrigger asChild>
|
|
272
|
+
<div className="text-sm font-semibold text-foreground truncate">
|
|
273
|
+
{service.label}
|
|
274
|
+
</div>
|
|
275
|
+
</TooltipTrigger>
|
|
276
|
+
<TooltipContent>{service.label}</TooltipContent>
|
|
277
|
+
</Tooltip>
|
|
258
278
|
<div className="font-mono text-xs text-muted-foreground/80 truncate">
|
|
259
279
|
{service.key}
|
|
260
280
|
</div>
|
|
@@ -263,7 +283,12 @@ function ConnectorCard({ service }: { service: Service }) {
|
|
|
263
283
|
Used by {appCount} {appCount === 1 ? "app" : "apps"}
|
|
264
284
|
</div>
|
|
265
285
|
</button>
|
|
266
|
-
<ConnectDialog
|
|
286
|
+
<ConnectDialog
|
|
287
|
+
service={service}
|
|
288
|
+
open={open}
|
|
289
|
+
onOpenChange={setOpen}
|
|
290
|
+
accessMode={accessMode}
|
|
291
|
+
/>
|
|
267
292
|
</>
|
|
268
293
|
);
|
|
269
294
|
}
|
|
@@ -297,7 +322,13 @@ export default function ConnectionsRoute() {
|
|
|
297
322
|
"list-integrations-catalog",
|
|
298
323
|
{},
|
|
299
324
|
);
|
|
325
|
+
const { data: accessSettings } = useActionQuery(
|
|
326
|
+
"get-vault-access-settings",
|
|
327
|
+
{},
|
|
328
|
+
);
|
|
300
329
|
const apps = (catalog as CatalogApp[]) || [];
|
|
330
|
+
const accessMode =
|
|
331
|
+
(accessSettings as any)?.mode === "manual" ? "manual" : "all-apps";
|
|
301
332
|
|
|
302
333
|
const services = useMemo<Service[]>(() => {
|
|
303
334
|
const map = new Map<string, Service>();
|
|
@@ -357,7 +388,11 @@ export default function ConnectionsRoute() {
|
|
|
357
388
|
</div>
|
|
358
389
|
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
359
390
|
{available.map((service) => (
|
|
360
|
-
<ConnectorCard
|
|
391
|
+
<ConnectorCard
|
|
392
|
+
key={service.key}
|
|
393
|
+
service={service}
|
|
394
|
+
accessMode={accessMode}
|
|
395
|
+
/>
|
|
361
396
|
))}
|
|
362
397
|
</div>
|
|
363
398
|
</section>
|
|
@@ -373,7 +408,11 @@ export default function ConnectionsRoute() {
|
|
|
373
408
|
</div>
|
|
374
409
|
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
375
410
|
{connected.map((service) => (
|
|
376
|
-
<ConnectorCard
|
|
411
|
+
<ConnectorCard
|
|
412
|
+
key={service.key}
|
|
413
|
+
service={service}
|
|
414
|
+
accessMode={accessMode}
|
|
415
|
+
/>
|
|
377
416
|
))}
|
|
378
417
|
</div>
|
|
379
418
|
</section>
|
|
@@ -9,7 +9,7 @@ export default function NewAppRoute() {
|
|
|
9
9
|
return (
|
|
10
10
|
<DispatchShell
|
|
11
11
|
title="New App"
|
|
12
|
-
description="Create a workspace app from a prompt and
|
|
12
|
+
description="Create a workspace app from a prompt and apply the workspace vault policy."
|
|
13
13
|
>
|
|
14
14
|
<NewWorkspaceAppFlow sourceApp="dispatch" className="px-0 py-0" />
|
|
15
15
|
</DispatchShell>
|
|
@@ -131,6 +131,60 @@ function AppCardSkeleton() {
|
|
|
131
131
|
);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
interface RecentAuditEvent {
|
|
135
|
+
id: string;
|
|
136
|
+
summary: string;
|
|
137
|
+
actor: string;
|
|
138
|
+
createdAt: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function RecentActivityList({
|
|
142
|
+
isLoading,
|
|
143
|
+
events,
|
|
144
|
+
}: {
|
|
145
|
+
isLoading: boolean;
|
|
146
|
+
events: RecentAuditEvent[];
|
|
147
|
+
}) {
|
|
148
|
+
if (isLoading && events.length === 0) {
|
|
149
|
+
return (
|
|
150
|
+
<div className="mt-4 space-y-3">
|
|
151
|
+
{Array.from({ length: 3 }).map((_, index) => (
|
|
152
|
+
<div
|
|
153
|
+
key={index}
|
|
154
|
+
className="rounded-xl border bg-muted/30 px-4 py-3 space-y-2"
|
|
155
|
+
>
|
|
156
|
+
<Skeleton className="h-4 w-3/5" />
|
|
157
|
+
<Skeleton className="h-3 w-2/5" />
|
|
158
|
+
</div>
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (events.length === 0) {
|
|
164
|
+
return (
|
|
165
|
+
<div className="mt-4 space-y-3">
|
|
166
|
+
<div className="rounded-xl border border-dashed px-4 py-6 text-sm text-muted-foreground">
|
|
167
|
+
No activity yet.
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return (
|
|
173
|
+
<div className="mt-4 space-y-3">
|
|
174
|
+
{events.map((event) => (
|
|
175
|
+
<div key={event.id} className="rounded-xl border bg-muted/30 px-4 py-3">
|
|
176
|
+
<div className="text-sm font-medium text-foreground">
|
|
177
|
+
{event.summary}
|
|
178
|
+
</div>
|
|
179
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
180
|
+
{event.actor} · {new Date(event.createdAt).toLocaleString()}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
))}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
134
188
|
function WorkspaceAppsSection({
|
|
135
189
|
apps,
|
|
136
190
|
isLoading,
|
|
@@ -563,7 +617,7 @@ export default function OverviewRoute() {
|
|
|
563
617
|
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
564
618
|
<StatCard
|
|
565
619
|
label="Vault secrets"
|
|
566
|
-
help="Credentials stored in the workspace vault.
|
|
620
|
+
help="Credentials stored in the workspace vault."
|
|
567
621
|
value={data?.vault?.secretCount || 0}
|
|
568
622
|
icon={IconKey}
|
|
569
623
|
cta={
|
|
@@ -575,8 +629,16 @@ export default function OverviewRoute() {
|
|
|
575
629
|
}
|
|
576
630
|
/>
|
|
577
631
|
<StatCard
|
|
578
|
-
label=
|
|
579
|
-
|
|
632
|
+
label={
|
|
633
|
+
data?.vault?.accessMode === "manual"
|
|
634
|
+
? "Active grants"
|
|
635
|
+
: "Accessible keys"
|
|
636
|
+
}
|
|
637
|
+
help={
|
|
638
|
+
data?.vault?.accessMode === "manual"
|
|
639
|
+
? "Secrets currently granted to apps. Sync them to push credentials."
|
|
640
|
+
: "Vault keys available to every workspace app."
|
|
641
|
+
}
|
|
580
642
|
value={data?.vault?.activeGrantCount || 0}
|
|
581
643
|
icon={IconShieldCheck}
|
|
582
644
|
/>
|
|
@@ -625,33 +687,11 @@ export default function OverviewRoute() {
|
|
|
625
687
|
<h2 className="text-lg font-semibold text-foreground">
|
|
626
688
|
Recent activity
|
|
627
689
|
</h2>
|
|
628
|
-
{isLoading && (
|
|
629
|
-
<span className="text-xs text-muted-foreground">
|
|
630
|
-
Loading...
|
|
631
|
-
</span>
|
|
632
|
-
)}
|
|
633
|
-
</div>
|
|
634
|
-
<div className="mt-4 space-y-3">
|
|
635
|
-
{(data?.recentAudit || []).map((event) => (
|
|
636
|
-
<div
|
|
637
|
-
key={event.id}
|
|
638
|
-
className="rounded-xl border bg-muted/30 px-4 py-3"
|
|
639
|
-
>
|
|
640
|
-
<div className="text-sm font-medium text-foreground">
|
|
641
|
-
{event.summary}
|
|
642
|
-
</div>
|
|
643
|
-
<div className="mt-1 text-xs text-muted-foreground">
|
|
644
|
-
{event.actor} ·{" "}
|
|
645
|
-
{new Date(event.createdAt).toLocaleString()}
|
|
646
|
-
</div>
|
|
647
|
-
</div>
|
|
648
|
-
))}
|
|
649
|
-
{!isLoading && (data?.recentAudit?.length || 0) === 0 && (
|
|
650
|
-
<div className="rounded-xl border border-dashed px-4 py-6 text-sm text-muted-foreground">
|
|
651
|
-
No activity yet.
|
|
652
|
-
</div>
|
|
653
|
-
)}
|
|
654
690
|
</div>
|
|
691
|
+
<RecentActivityList
|
|
692
|
+
isLoading={isLoading}
|
|
693
|
+
events={data?.recentAudit ?? []}
|
|
694
|
+
/>
|
|
655
695
|
</section>
|
|
656
696
|
|
|
657
697
|
<section className="rounded-2xl border bg-card p-5">
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
} from "@/components/ui/alert-dialog";
|
|
38
38
|
import { Input } from "@/components/ui/input";
|
|
39
39
|
import { Label } from "@/components/ui/label";
|
|
40
|
+
import { Switch } from "@/components/ui/switch";
|
|
40
41
|
import {
|
|
41
42
|
Select,
|
|
42
43
|
SelectContent,
|
|
@@ -46,6 +47,7 @@ import {
|
|
|
46
47
|
} from "@/components/ui/select";
|
|
47
48
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
48
49
|
import { Textarea } from "@/components/ui/textarea";
|
|
50
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
49
51
|
|
|
50
52
|
const PROVIDERS = [
|
|
51
53
|
"google",
|
|
@@ -60,6 +62,8 @@ const PROVIDERS = [
|
|
|
60
62
|
"other",
|
|
61
63
|
];
|
|
62
64
|
|
|
65
|
+
type VaultAccessMode = "all-apps" | "manual";
|
|
66
|
+
|
|
63
67
|
export function meta() {
|
|
64
68
|
return [{ title: "Vault — Dispatch" }];
|
|
65
69
|
}
|
|
@@ -240,7 +244,53 @@ function GrantDialog({
|
|
|
240
244
|
);
|
|
241
245
|
}
|
|
242
246
|
|
|
243
|
-
function
|
|
247
|
+
function VaultAccessSettingsCard({ mode }: { mode: VaultAccessMode }) {
|
|
248
|
+
const update = useActionMutation("set-vault-access-settings", {
|
|
249
|
+
onSuccess: (next: any) =>
|
|
250
|
+
toast.success(
|
|
251
|
+
next?.mode === "manual"
|
|
252
|
+
? "Manual vault access enabled"
|
|
253
|
+
: "All apps can use vault keys",
|
|
254
|
+
),
|
|
255
|
+
onError: (err) => toast.error(String(err)),
|
|
256
|
+
});
|
|
257
|
+
const allApps = mode !== "manual";
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<div className="rounded-xl border bg-card px-4 py-3">
|
|
261
|
+
<div className="flex items-center justify-between gap-4">
|
|
262
|
+
<div className="min-w-0">
|
|
263
|
+
<Label className="text-sm font-medium">
|
|
264
|
+
All apps can use vault keys
|
|
265
|
+
</Label>
|
|
266
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
267
|
+
{allApps
|
|
268
|
+
? "Every workspace app can receive every saved key."
|
|
269
|
+
: "Only apps with explicit grants can receive saved keys."}
|
|
270
|
+
</p>
|
|
271
|
+
</div>
|
|
272
|
+
<Switch
|
|
273
|
+
checked={allApps}
|
|
274
|
+
disabled={update.isPending}
|
|
275
|
+
onCheckedChange={(checked) =>
|
|
276
|
+
update.mutate({ mode: checked ? "all-apps" : "manual" })
|
|
277
|
+
}
|
|
278
|
+
aria-label="Allow all workspace apps to use vault keys"
|
|
279
|
+
/>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function SecretRow({
|
|
286
|
+
secret,
|
|
287
|
+
grants,
|
|
288
|
+
accessMode,
|
|
289
|
+
}: {
|
|
290
|
+
secret: any;
|
|
291
|
+
grants: any[];
|
|
292
|
+
accessMode: VaultAccessMode;
|
|
293
|
+
}) {
|
|
244
294
|
const [expanded, setExpanded] = useState(false);
|
|
245
295
|
const [showValue, setShowValue] = useState(false);
|
|
246
296
|
|
|
@@ -259,6 +309,7 @@ function SecretRow({ secret, grants }: { secret: any; grants: any[] }) {
|
|
|
259
309
|
});
|
|
260
310
|
|
|
261
311
|
const activeGrants = grants.filter((g) => g.status === "active");
|
|
312
|
+
const allApps = accessMode !== "manual";
|
|
262
313
|
|
|
263
314
|
return (
|
|
264
315
|
<div className="rounded-xl border bg-card">
|
|
@@ -289,7 +340,9 @@ function SecretRow({ secret, grants }: { secret: any; grants: any[] }) {
|
|
|
289
340
|
</div>
|
|
290
341
|
<div className="flex items-center gap-2">
|
|
291
342
|
<Badge variant="outline" className="text-xs">
|
|
292
|
-
{
|
|
343
|
+
{allApps
|
|
344
|
+
? "All apps"
|
|
345
|
+
: `${activeGrants.length} grant${activeGrants.length !== 1 ? "s" : ""}`}
|
|
293
346
|
</Badge>
|
|
294
347
|
</div>
|
|
295
348
|
</button>
|
|
@@ -319,11 +372,17 @@ function SecretRow({ secret, grants }: { secret: any; grants: any[] }) {
|
|
|
319
372
|
<div className="space-y-2">
|
|
320
373
|
<div className="flex items-center justify-between">
|
|
321
374
|
<span className="text-xs font-medium text-foreground">
|
|
322
|
-
Grants
|
|
375
|
+
{allApps ? "Access" : "Grants"}
|
|
323
376
|
</span>
|
|
324
|
-
|
|
377
|
+
{!allApps && (
|
|
378
|
+
<GrantDialog secretId={secret.id} secretName={secret.name} />
|
|
379
|
+
)}
|
|
325
380
|
</div>
|
|
326
|
-
{
|
|
381
|
+
{allApps ? (
|
|
382
|
+
<div className="rounded-lg border border-dashed px-3 py-4 text-center text-xs text-muted-foreground">
|
|
383
|
+
Available to every workspace app.
|
|
384
|
+
</div>
|
|
385
|
+
) : activeGrants.length > 0 ? (
|
|
327
386
|
<div className="space-y-1.5">
|
|
328
387
|
{activeGrants.map((grant: any) => (
|
|
329
388
|
<div
|
|
@@ -365,7 +424,7 @@ function SecretRow({ secret, grants }: { secret: any; grants: any[] }) {
|
|
|
365
424
|
</div>
|
|
366
425
|
) : (
|
|
367
426
|
<div className="rounded-lg border border-dashed px-3 py-4 text-center text-xs text-muted-foreground">
|
|
368
|
-
No grants yet.
|
|
427
|
+
No grants yet.
|
|
369
428
|
</div>
|
|
370
429
|
)}
|
|
371
430
|
</div>
|
|
@@ -387,8 +446,8 @@ function SecretRow({ secret, grants }: { secret: any; grants: any[] }) {
|
|
|
387
446
|
<AlertDialogTitle>Delete this secret?</AlertDialogTitle>
|
|
388
447
|
<AlertDialogDescription>
|
|
389
448
|
Removing “{secret.name}” revokes all of its grants. Apps
|
|
390
|
-
that depended on this credential
|
|
391
|
-
|
|
449
|
+
that depended on this credential can lose access on the next
|
|
450
|
+
sync. This cannot be undone.
|
|
392
451
|
</AlertDialogDescription>
|
|
393
452
|
</AlertDialogHeader>
|
|
394
453
|
<AlertDialogFooter>
|
|
@@ -507,6 +566,12 @@ export default function VaultRoute() {
|
|
|
507
566
|
const { data: grants } = useActionQuery("list-vault-grants", {});
|
|
508
567
|
const { data: requests } = useActionQuery("list-vault-requests", {});
|
|
509
568
|
const { data: audit } = useActionQuery("list-vault-audit", { limit: 20 });
|
|
569
|
+
const { data: accessSettings } = useActionQuery(
|
|
570
|
+
"get-vault-access-settings",
|
|
571
|
+
{},
|
|
572
|
+
);
|
|
573
|
+
const accessMode: VaultAccessMode =
|
|
574
|
+
(accessSettings as any)?.mode === "manual" ? "manual" : "all-apps";
|
|
510
575
|
|
|
511
576
|
const grantsBySecret = (grants || []).reduce(
|
|
512
577
|
(acc: Record<string, any[]>, g: any) => {
|
|
@@ -524,7 +589,7 @@ export default function VaultRoute() {
|
|
|
524
589
|
return (
|
|
525
590
|
<DispatchShell
|
|
526
591
|
title="Vault"
|
|
527
|
-
description="Centralized secret management for your workspace. Store credentials once
|
|
592
|
+
description="Centralized secret management for your workspace. Store credentials once and sync them to apps."
|
|
528
593
|
>
|
|
529
594
|
<Tabs defaultValue="secrets">
|
|
530
595
|
<TabsList>
|
|
@@ -546,25 +611,40 @@ export default function VaultRoute() {
|
|
|
546
611
|
</TabsList>
|
|
547
612
|
|
|
548
613
|
<TabsContent value="secrets" className="mt-4 space-y-3">
|
|
614
|
+
<VaultAccessSettingsCard mode={accessMode} />
|
|
615
|
+
|
|
549
616
|
<div className="flex items-center justify-between">
|
|
550
617
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
551
618
|
<IconKey size={16} />
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
619
|
+
{secretsLoading ? (
|
|
620
|
+
<Skeleton className="h-4 w-20" />
|
|
621
|
+
) : (
|
|
622
|
+
<span>
|
|
623
|
+
{`${secrets?.length || 0} secret${(secrets?.length || 0) !== 1 ? "s" : ""}`}
|
|
624
|
+
</span>
|
|
625
|
+
)}
|
|
557
626
|
</div>
|
|
558
627
|
<AddSecretDialog />
|
|
559
628
|
</div>
|
|
560
629
|
|
|
561
|
-
{(secrets
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
630
|
+
{secretsLoading && (secrets ?? []).length === 0
|
|
631
|
+
? Array.from({ length: 3 }).map((_, index) => (
|
|
632
|
+
<div
|
|
633
|
+
key={index}
|
|
634
|
+
className="rounded-2xl border bg-card px-5 py-4 space-y-2"
|
|
635
|
+
>
|
|
636
|
+
<Skeleton className="h-4 w-1/3" />
|
|
637
|
+
<Skeleton className="h-3 w-2/3" />
|
|
638
|
+
</div>
|
|
639
|
+
))
|
|
640
|
+
: (secrets || []).map((secret: any) => (
|
|
641
|
+
<SecretRow
|
|
642
|
+
key={secret.id}
|
|
643
|
+
secret={secret}
|
|
644
|
+
grants={grantsBySecret[secret.id] || []}
|
|
645
|
+
accessMode={accessMode}
|
|
646
|
+
/>
|
|
647
|
+
))}
|
|
568
648
|
|
|
569
649
|
{!secretsLoading && (secrets?.length || 0) === 0 && (
|
|
570
650
|
<div className="rounded-2xl border border-dashed px-6 py-12 text-center">
|