@agent-native/dispatch 0.16.6 → 0.17.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/dist/actions/provider-api-register.d.ts +13 -13
- package/dist/components/automation-run-history-dialog.d.ts +9 -0
- package/dist/components/automation-run-history-dialog.d.ts.map +1 -0
- package/dist/components/automation-run-history-dialog.js +61 -0
- package/dist/components/automation-run-history-dialog.js.map +1 -0
- package/dist/components/connected-app-card.d.ts +5 -0
- package/dist/components/connected-app-card.d.ts.map +1 -0
- package/dist/components/connected-app-card.js +12 -0
- package/dist/components/connected-app-card.js.map +1 -0
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +12 -4
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/lib/automation-display.d.ts +2 -0
- package/dist/lib/automation-display.d.ts.map +1 -1
- package/dist/lib/automation-display.js +17 -3
- package/dist/lib/automation-display.js.map +1 -1
- package/dist/lib/automations.d.ts +3 -0
- package/dist/lib/automations.d.ts.map +1 -1
- package/dist/lib/automations.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +3 -10
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/automations.d.ts.map +1 -1
- package/dist/routes/pages/automations.js +19 -13
- package/dist/routes/pages/automations.js.map +1 -1
- package/package.json +3 -3
- package/src/components/automation-run-history-dialog.tsx +242 -0
- package/src/components/connected-app-card.tsx +42 -0
- package/src/components/dispatch-control-plane.spec.tsx +79 -2
- package/src/components/dispatch-control-plane.tsx +38 -3
- package/src/lib/automation-display.spec.ts +40 -0
- package/src/lib/automation-display.ts +23 -3
- package/src/lib/automations.ts +3 -0
- package/src/routes/pages/apps.tsx +2 -39
- package/src/routes/pages/automations.tsx +41 -10
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import { withBuilderUtmTrackingParams } from "@agent-native/core/shared/builder-link-tracking";
|
|
3
|
+
import { IconArrowUpRight } from "@tabler/icons-react";
|
|
4
|
+
|
|
5
|
+
import type { ConnectedAppSummary } from "../lib/other-apps";
|
|
6
|
+
|
|
7
|
+
export function ConnectedAppCard({ app }: { app: ConnectedAppSummary }) {
|
|
8
|
+
const t = useT();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<a
|
|
12
|
+
href={withBuilderUtmTrackingParams(app.url, {
|
|
13
|
+
campaign: "product",
|
|
14
|
+
content: "dispatch_app",
|
|
15
|
+
})}
|
|
16
|
+
target="_blank"
|
|
17
|
+
rel="noopener noreferrer"
|
|
18
|
+
className="group flex min-h-[116px] items-start gap-3 rounded-xl bg-card/40 p-4 transition-[background-color] hover:bg-accent/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
19
|
+
>
|
|
20
|
+
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-xs font-semibold text-muted-foreground">
|
|
21
|
+
{app.name.charAt(0).toUpperCase()}
|
|
22
|
+
</span>
|
|
23
|
+
<span className="min-w-0 flex-1">
|
|
24
|
+
<span className="flex items-start justify-between gap-3">
|
|
25
|
+
<span className="truncate text-sm font-semibold text-foreground">
|
|
26
|
+
{app.name}
|
|
27
|
+
</span>
|
|
28
|
+
<IconArrowUpRight
|
|
29
|
+
size={15}
|
|
30
|
+
className="shrink-0 text-muted-foreground transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5"
|
|
31
|
+
/>
|
|
32
|
+
</span>
|
|
33
|
+
<span className="mt-2 line-clamp-2 block text-[13px] leading-5 text-muted-foreground">
|
|
34
|
+
{app.description || app.url}
|
|
35
|
+
</span>
|
|
36
|
+
<span className="mt-3 block text-xs font-medium text-foreground">
|
|
37
|
+
{t("dispatch.pages.openApp")}
|
|
38
|
+
</span>
|
|
39
|
+
</span>
|
|
40
|
+
</a>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -10,6 +10,8 @@ import { TooltipProvider } from "./ui/tooltip";
|
|
|
10
10
|
const clientState = vi.hoisted(() => ({
|
|
11
11
|
navigateWithTransition: vi.fn(),
|
|
12
12
|
promptComposerProps: null as Record<string, unknown> | null,
|
|
13
|
+
workspaceApps: [] as Array<Record<string, unknown>>,
|
|
14
|
+
connectedApps: [] as Array<Record<string, unknown>>,
|
|
13
15
|
useChatModels: vi.fn(() => ({
|
|
14
16
|
availableModels: [],
|
|
15
17
|
defaultModel: "auto",
|
|
@@ -50,12 +52,17 @@ vi.mock("@agent-native/core/client/composer", () => ({
|
|
|
50
52
|
}));
|
|
51
53
|
|
|
52
54
|
vi.mock("@agent-native/core/client/hooks", () => ({
|
|
53
|
-
useActionQuery: () => ({
|
|
54
|
-
data:
|
|
55
|
+
useActionQuery: (name: string) => ({
|
|
56
|
+
data:
|
|
57
|
+
name === "list-connected-agents"
|
|
58
|
+
? clientState.connectedApps
|
|
59
|
+
: clientState.workspaceApps,
|
|
55
60
|
isLoading: false,
|
|
56
61
|
isError: false,
|
|
62
|
+
error: null,
|
|
57
63
|
refetch: vi.fn(),
|
|
58
64
|
}),
|
|
65
|
+
useActionMutation: () => ({ mutate: vi.fn(), isPending: false }),
|
|
59
66
|
}));
|
|
60
67
|
|
|
61
68
|
vi.mock("@agent-native/core/client/host", () => ({
|
|
@@ -65,6 +72,7 @@ vi.mock("@agent-native/core/client/host", () => ({
|
|
|
65
72
|
vi.mock("@agent-native/core/client/i18n", () => ({
|
|
66
73
|
useT: () => (key: string, values?: { defaultValue?: string }) =>
|
|
67
74
|
values?.defaultValue ?? key,
|
|
75
|
+
useFormatters: () => ({ formatDate: (value: string) => value }),
|
|
68
76
|
}));
|
|
69
77
|
|
|
70
78
|
vi.mock("./create-app-popover", () => ({
|
|
@@ -79,6 +87,8 @@ describe("DispatchControlPlane", () => {
|
|
|
79
87
|
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
80
88
|
clientState.navigateWithTransition.mockReset();
|
|
81
89
|
clientState.promptComposerProps = null;
|
|
90
|
+
clientState.workspaceApps = [];
|
|
91
|
+
clientState.connectedApps = [];
|
|
82
92
|
clientState.useChatModels.mockClear();
|
|
83
93
|
container = document.createElement("div");
|
|
84
94
|
document.body.appendChild(container);
|
|
@@ -140,4 +150,71 @@ describe("DispatchControlPlane", () => {
|
|
|
140
150
|
}),
|
|
141
151
|
);
|
|
142
152
|
});
|
|
153
|
+
|
|
154
|
+
it("shows mounted and connected apps together without duplicates", async () => {
|
|
155
|
+
clientState.workspaceApps = [
|
|
156
|
+
{
|
|
157
|
+
id: "onboarding",
|
|
158
|
+
name: "Onboarding",
|
|
159
|
+
path: "/onboarding",
|
|
160
|
+
status: "ready",
|
|
161
|
+
isDispatch: false,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "dispatch",
|
|
165
|
+
name: "Dispatch",
|
|
166
|
+
path: "/dispatch",
|
|
167
|
+
status: "ready",
|
|
168
|
+
isDispatch: true,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "archived-app",
|
|
172
|
+
name: "Archived app",
|
|
173
|
+
path: "/archived-app",
|
|
174
|
+
status: "ready",
|
|
175
|
+
isDispatch: false,
|
|
176
|
+
archived: true,
|
|
177
|
+
},
|
|
178
|
+
];
|
|
179
|
+
clientState.connectedApps = [
|
|
180
|
+
{
|
|
181
|
+
id: "mail",
|
|
182
|
+
name: "Mail",
|
|
183
|
+
description: "Email client",
|
|
184
|
+
url: "https://mail.agent-native.com",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: "clips",
|
|
188
|
+
name: "Clips",
|
|
189
|
+
description: "Record and share",
|
|
190
|
+
url: "https://clips.agent-native.com",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "onboarding",
|
|
194
|
+
name: "Duplicate onboarding",
|
|
195
|
+
url: "https://duplicate.example.com",
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
await act(async () => {
|
|
200
|
+
root.render(
|
|
201
|
+
<MemoryRouter initialEntries={["/overview"]}>
|
|
202
|
+
<TooltipProvider>
|
|
203
|
+
<DispatchControlPlane />
|
|
204
|
+
</TooltipProvider>
|
|
205
|
+
</MemoryRouter>,
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
expect(container.textContent).toContain("Onboarding");
|
|
210
|
+
expect(container.textContent).toContain("Mail");
|
|
211
|
+
expect(container.textContent).toContain("Clips");
|
|
212
|
+
expect(container.textContent).not.toContain("Archived app");
|
|
213
|
+
expect(container.textContent).not.toContain("Duplicate onboarding");
|
|
214
|
+
expect(
|
|
215
|
+
Array.from(container.querySelectorAll("a")).filter((anchor) =>
|
|
216
|
+
anchor.getAttribute("href")?.includes("onboarding"),
|
|
217
|
+
),
|
|
218
|
+
).toHaveLength(1);
|
|
219
|
+
});
|
|
143
220
|
});
|
|
@@ -15,9 +15,11 @@ import type { ReactNode } from "react";
|
|
|
15
15
|
import { useState } from "react";
|
|
16
16
|
import { Link, useNavigate } from "react-router";
|
|
17
17
|
|
|
18
|
+
import { filterOtherApps, type ConnectedAppSummary } from "../lib/other-apps";
|
|
18
19
|
import { submitOverviewPrompt } from "../lib/overview-chat";
|
|
19
20
|
import type { WorkspaceAppSummary } from "../lib/workspace-apps";
|
|
20
21
|
import { ActionQueryError } from "./action-query-error";
|
|
22
|
+
import { ConnectedAppCard } from "./connected-app-card";
|
|
21
23
|
import { CreateAppPopover } from "./create-app-popover";
|
|
22
24
|
import { useSetPageTitle } from "./layout/HeaderActions";
|
|
23
25
|
import { Button } from "./ui/button";
|
|
@@ -141,17 +143,29 @@ function CommandPanel() {
|
|
|
141
143
|
function AppsPanel({
|
|
142
144
|
apps,
|
|
143
145
|
isLoading,
|
|
146
|
+
otherApps,
|
|
147
|
+
otherAppsError,
|
|
148
|
+
otherAppsLoading,
|
|
149
|
+
onRetryOtherApps,
|
|
144
150
|
}: {
|
|
145
151
|
apps: WorkspaceAppSummary[];
|
|
146
152
|
isLoading: boolean;
|
|
153
|
+
otherApps: ConnectedAppSummary[];
|
|
154
|
+
otherAppsError?: Error | null;
|
|
155
|
+
otherAppsLoading: boolean;
|
|
156
|
+
onRetryOtherApps: () => void;
|
|
147
157
|
}) {
|
|
148
158
|
const t = useT();
|
|
149
159
|
const [showPending, setShowPending] = useState(false);
|
|
150
160
|
const visibleApps = apps.filter((app) => !app.isDispatch && !app.archived);
|
|
151
161
|
const activeApps = visibleApps.filter((app) => app.status !== "pending");
|
|
152
162
|
const pendingApps = visibleApps.filter((app) => app.status === "pending");
|
|
163
|
+
const hasActiveApps = activeApps.length > 0 || otherApps.length > 0;
|
|
153
164
|
const showSkeletons =
|
|
154
|
-
isLoading
|
|
165
|
+
(isLoading || otherAppsLoading) &&
|
|
166
|
+
activeApps.length === 0 &&
|
|
167
|
+
otherApps.length === 0 &&
|
|
168
|
+
pendingApps.length === 0;
|
|
155
169
|
|
|
156
170
|
return (
|
|
157
171
|
<section className="flex flex-col gap-3">
|
|
@@ -176,15 +190,21 @@ function AppsPanel({
|
|
|
176
190
|
</div>
|
|
177
191
|
))}
|
|
178
192
|
</div>
|
|
179
|
-
) :
|
|
193
|
+
) : hasActiveApps ? (
|
|
180
194
|
<div className="grid gap-3 md:grid-cols-2">
|
|
181
195
|
{activeApps.map((app) => (
|
|
182
196
|
<WorkspaceAppCard key={app.id} app={app} className="min-h-32" />
|
|
183
197
|
))}
|
|
198
|
+
{otherApps.map((app) => (
|
|
199
|
+
<ConnectedAppCard key={app.id} app={app} />
|
|
200
|
+
))}
|
|
184
201
|
</div>
|
|
185
202
|
) : (
|
|
186
203
|
<CreateAppPopover />
|
|
187
204
|
)}
|
|
205
|
+
{otherAppsError ? (
|
|
206
|
+
<ActionQueryError error={otherAppsError} onRetry={onRetryOtherApps} />
|
|
207
|
+
) : null}
|
|
188
208
|
{pendingApps.length > 0 ? (
|
|
189
209
|
<Collapsible open={showPending} onOpenChange={setShowPending}>
|
|
190
210
|
<div className="space-y-3 border-t pt-3">
|
|
@@ -246,7 +266,15 @@ export function DispatchControlPlane() {
|
|
|
246
266
|
"list-workspace-apps",
|
|
247
267
|
{ includeAgentCards: false, includeArchived: true },
|
|
248
268
|
);
|
|
269
|
+
const connectedAppsQuery = useActionQuery<ConnectedAppSummary[]>(
|
|
270
|
+
"list-connected-agents",
|
|
271
|
+
{},
|
|
272
|
+
);
|
|
249
273
|
const { data: workspaceApps = [], isLoading: appsLoading } = appsQuery;
|
|
274
|
+
const otherApps = filterOtherApps(
|
|
275
|
+
connectedAppsQuery.data ?? [],
|
|
276
|
+
workspaceApps ?? [],
|
|
277
|
+
);
|
|
250
278
|
|
|
251
279
|
return (
|
|
252
280
|
<div className="flex flex-col gap-8">
|
|
@@ -257,7 +285,14 @@ export function DispatchControlPlane() {
|
|
|
257
285
|
onRetry={() => void appsQuery.refetch()}
|
|
258
286
|
/>
|
|
259
287
|
) : (
|
|
260
|
-
<AppsPanel
|
|
288
|
+
<AppsPanel
|
|
289
|
+
apps={workspaceApps ?? []}
|
|
290
|
+
isLoading={appsLoading}
|
|
291
|
+
otherApps={otherApps}
|
|
292
|
+
otherAppsError={connectedAppsQuery.error}
|
|
293
|
+
otherAppsLoading={connectedAppsQuery.isLoading}
|
|
294
|
+
onRetryOtherApps={() => void connectedAppsQuery.refetch()}
|
|
295
|
+
/>
|
|
261
296
|
)}
|
|
262
297
|
</div>
|
|
263
298
|
);
|
|
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
|
|
|
3
3
|
import {
|
|
4
4
|
automationIdentity,
|
|
5
5
|
automationNextRun,
|
|
6
|
+
automationScopeLabel,
|
|
6
7
|
automationStatus,
|
|
7
8
|
automationTarget,
|
|
8
9
|
automationTroubleshootPath,
|
|
@@ -77,6 +78,45 @@ describe("automation-display", () => {
|
|
|
77
78
|
});
|
|
78
79
|
});
|
|
79
80
|
|
|
81
|
+
it("labels blocked scheduler checks separately from execution failures", () => {
|
|
82
|
+
const blocked = item({
|
|
83
|
+
id: "blocked",
|
|
84
|
+
name: "Blocked",
|
|
85
|
+
path: "jobs/blocked.md",
|
|
86
|
+
owner: "user@example.com",
|
|
87
|
+
lastStatus: "skipped",
|
|
88
|
+
lastError: "Condition was false",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(automationStatus(blocked)).toEqual({
|
|
92
|
+
label: "Blocked",
|
|
93
|
+
tone: "warning",
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("identifies personal and organization ownership in the UI", () => {
|
|
98
|
+
expect(
|
|
99
|
+
automationScopeLabel(
|
|
100
|
+
item({
|
|
101
|
+
id: "personal",
|
|
102
|
+
name: "Personal",
|
|
103
|
+
path: "jobs/personal.md",
|
|
104
|
+
owner: "user@example.com",
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
).toBe("Personal");
|
|
108
|
+
expect(
|
|
109
|
+
automationScopeLabel(
|
|
110
|
+
item({
|
|
111
|
+
id: "organization",
|
|
112
|
+
name: "Organization",
|
|
113
|
+
path: "jobs/organization.md",
|
|
114
|
+
owner: "__organization__:org-1",
|
|
115
|
+
}),
|
|
116
|
+
),
|
|
117
|
+
).toBe("Organization");
|
|
118
|
+
});
|
|
119
|
+
|
|
80
120
|
it("sorts enabled errors ahead of healthy runs", () => {
|
|
81
121
|
const healthy = item({
|
|
82
122
|
id: "healthy",
|
|
@@ -27,6 +27,16 @@ export function automationTarget(item: DispatchAutomationItem): string {
|
|
|
27
27
|
return item.triggerType || "schedule";
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function automationScopeLabel(
|
|
31
|
+
item: Pick<DispatchAutomationItem, "owner" | "scope">,
|
|
32
|
+
): string {
|
|
33
|
+
return item.scope === "organization" ||
|
|
34
|
+
item.owner.startsWith("__organization__:") ||
|
|
35
|
+
item.owner === "__shared__"
|
|
36
|
+
? "Organization"
|
|
37
|
+
: "Personal";
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
export function relativeRunTime(value: string | null | undefined): string {
|
|
31
41
|
if (!value) return "never";
|
|
32
42
|
const timestamp = new Date(value).getTime();
|
|
@@ -44,6 +54,10 @@ export function automationLastRun(item: DispatchAutomationItem): string {
|
|
|
44
54
|
return item.lastRun ? relativeRunTime(item.lastRun) : "never";
|
|
45
55
|
}
|
|
46
56
|
|
|
57
|
+
export function automationLastCheck(item: DispatchAutomationItem): string {
|
|
58
|
+
return item.lastCheck ? relativeRunTime(item.lastCheck) : "never";
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
export function automationNextRun(item: DispatchAutomationItem): string {
|
|
48
62
|
if (!item.enabled) return "paused";
|
|
49
63
|
if (item.triggerType === "event") return "on event";
|
|
@@ -59,7 +73,7 @@ export function automationStatus(item: DispatchAutomationItem): {
|
|
|
59
73
|
if (item.lastStatus === "running")
|
|
60
74
|
return { label: "Running", tone: "warning" };
|
|
61
75
|
if (item.lastStatus === "skipped")
|
|
62
|
-
return { label: "
|
|
76
|
+
return { label: "Blocked", tone: "warning" };
|
|
63
77
|
if (item.lastStatus === "success")
|
|
64
78
|
return { label: "Healthy", tone: "success" };
|
|
65
79
|
return { label: "Ready", tone: "default" };
|
|
@@ -69,8 +83,14 @@ export function sortAutomations(
|
|
|
69
83
|
automations: DispatchAutomationItem[],
|
|
70
84
|
): DispatchAutomationItem[] {
|
|
71
85
|
return [...automations].sort((a, b) => {
|
|
72
|
-
const aError =
|
|
73
|
-
|
|
86
|
+
const aError =
|
|
87
|
+
a.enabled && (a.lastStatus === "error" || a.lastStatus === "skipped")
|
|
88
|
+
? 1
|
|
89
|
+
: 0;
|
|
90
|
+
const bError =
|
|
91
|
+
b.enabled && (b.lastStatus === "error" || b.lastStatus === "skipped")
|
|
92
|
+
? 1
|
|
93
|
+
: 0;
|
|
74
94
|
if (aError !== bError) return bError - aError;
|
|
75
95
|
return (b.lastRun || "").localeCompare(a.lastRun || "");
|
|
76
96
|
});
|
package/src/lib/automations.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface DispatchAutomationItem {
|
|
|
5
5
|
name: string;
|
|
6
6
|
path: string;
|
|
7
7
|
owner: string;
|
|
8
|
+
scope?: "personal" | "organization";
|
|
8
9
|
canUpdate?: boolean;
|
|
9
10
|
triggerType?: "schedule" | "event" | string;
|
|
10
11
|
event?: string;
|
|
@@ -14,8 +15,10 @@ export interface DispatchAutomationItem {
|
|
|
14
15
|
mode?: string;
|
|
15
16
|
domain?: string;
|
|
16
17
|
enabled?: boolean;
|
|
18
|
+
timezone?: string;
|
|
17
19
|
lastStatus?: string;
|
|
18
20
|
lastRun?: string;
|
|
21
|
+
lastCheck?: string;
|
|
19
22
|
lastError?: string;
|
|
20
23
|
nextRun?: string;
|
|
21
24
|
createdBy?: string;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { useActionQuery } from "@agent-native/core/client/hooks";
|
|
2
2
|
import { useT } from "@agent-native/core/client/i18n";
|
|
3
|
-
import { withBuilderUtmTrackingParams } from "@agent-native/core/shared/builder-link-tracking";
|
|
4
3
|
import {
|
|
5
4
|
IconApps,
|
|
6
|
-
IconArrowUpRight,
|
|
7
5
|
IconChevronDown,
|
|
8
6
|
IconClockHour4,
|
|
9
7
|
IconEyeOff,
|
|
@@ -13,6 +11,7 @@ import {
|
|
|
13
11
|
import { useState } from "react";
|
|
14
12
|
|
|
15
13
|
import { ActionQueryError } from "../../components/action-query-error";
|
|
14
|
+
import { ConnectedAppCard } from "../../components/connected-app-card";
|
|
16
15
|
import { CreateAppPopover } from "../../components/create-app-popover";
|
|
17
16
|
import { DispatchShell } from "../../components/dispatch-shell";
|
|
18
17
|
import { Button } from "../../components/ui/button";
|
|
@@ -277,7 +276,7 @@ export default function AppsRoute() {
|
|
|
277
276
|
) : (
|
|
278
277
|
<div className="grid auto-rows-fr gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
279
278
|
{otherApps.map((app) => (
|
|
280
|
-
<
|
|
279
|
+
<ConnectedAppCard key={app.id} app={app} />
|
|
281
280
|
))}
|
|
282
281
|
</div>
|
|
283
282
|
)}
|
|
@@ -382,42 +381,6 @@ function OtherAppsSkeletonGrid() {
|
|
|
382
381
|
);
|
|
383
382
|
}
|
|
384
383
|
|
|
385
|
-
function OtherAppCard({ app }: { app: ConnectedAppSummary }) {
|
|
386
|
-
const t = useT();
|
|
387
|
-
return (
|
|
388
|
-
<a
|
|
389
|
-
href={withBuilderUtmTrackingParams(app.url, {
|
|
390
|
-
campaign: "product",
|
|
391
|
-
content: "dispatch_app",
|
|
392
|
-
})}
|
|
393
|
-
target="_blank"
|
|
394
|
-
rel="noopener noreferrer"
|
|
395
|
-
className="group flex min-h-[116px] items-start gap-3 rounded-xl bg-card/40 p-4 transition-[background-color] hover:bg-accent/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
396
|
-
>
|
|
397
|
-
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-xs font-semibold text-muted-foreground">
|
|
398
|
-
{app.name.charAt(0).toUpperCase()}
|
|
399
|
-
</span>
|
|
400
|
-
<span className="min-w-0 flex-1">
|
|
401
|
-
<span className="flex items-start justify-between gap-3">
|
|
402
|
-
<span className="truncate text-sm font-semibold text-foreground">
|
|
403
|
-
{app.name}
|
|
404
|
-
</span>
|
|
405
|
-
<IconArrowUpRight
|
|
406
|
-
size={15}
|
|
407
|
-
className="shrink-0 text-muted-foreground transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5"
|
|
408
|
-
/>
|
|
409
|
-
</span>
|
|
410
|
-
<span className="mt-2 line-clamp-2 block text-[13px] leading-5 text-muted-foreground">
|
|
411
|
-
{app.description || app.url}
|
|
412
|
-
</span>
|
|
413
|
-
<span className="mt-3 block text-xs font-medium text-foreground">
|
|
414
|
-
{t("dispatch.pages.openApp")}
|
|
415
|
-
</span>
|
|
416
|
-
</span>
|
|
417
|
-
</a>
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
384
|
function AppsSkeletonGrid() {
|
|
422
385
|
return (
|
|
423
386
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
@@ -3,6 +3,7 @@ import { PromptComposer } from "@agent-native/core/client/composer";
|
|
|
3
3
|
import { useChangeVersions } from "@agent-native/core/client/hooks";
|
|
4
4
|
import {
|
|
5
5
|
IconFileSearch,
|
|
6
|
+
IconListDetails,
|
|
6
7
|
IconPlus,
|
|
7
8
|
IconSettingsAutomation,
|
|
8
9
|
} from "@tabler/icons-react";
|
|
@@ -11,6 +12,7 @@ import { useMemo, useState } from "react";
|
|
|
11
12
|
import { Link } from "react-router";
|
|
12
13
|
import { toast } from "sonner";
|
|
13
14
|
|
|
15
|
+
import { AutomationRunHistoryDialog } from "../../components/automation-run-history-dialog";
|
|
14
16
|
import { DispatchShell } from "../../components/dispatch-shell";
|
|
15
17
|
import { Badge } from "../../components/ui/badge";
|
|
16
18
|
import { Button } from "../../components/ui/button";
|
|
@@ -23,8 +25,10 @@ import { Skeleton } from "../../components/ui/skeleton";
|
|
|
23
25
|
import { Switch } from "../../components/ui/switch";
|
|
24
26
|
import {
|
|
25
27
|
automationIdentity,
|
|
28
|
+
automationLastCheck,
|
|
26
29
|
automationLastRun,
|
|
27
30
|
automationNextRun,
|
|
31
|
+
automationScopeLabel,
|
|
28
32
|
automationStatus,
|
|
29
33
|
automationTarget,
|
|
30
34
|
automationTroubleshootPath,
|
|
@@ -175,13 +179,17 @@ function CreateAutomationButton() {
|
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
export default function AutomationsRoute() {
|
|
182
|
+
const [detailsTarget, setDetailsTarget] =
|
|
183
|
+
useState<DispatchAutomationItem | null>(null);
|
|
178
184
|
const automationsQuery = useAutomations();
|
|
179
185
|
const toggleAutomation = useToggleAutomation();
|
|
180
186
|
const automations = automationsQuery.data ?? [];
|
|
181
187
|
const ordered = useMemo(() => sortAutomations(automations), [automations]);
|
|
182
188
|
const enabledCount = automations.filter((item) => item.enabled).length;
|
|
183
189
|
const errorCount = automations.filter(
|
|
184
|
-
(item) =>
|
|
190
|
+
(item) =>
|
|
191
|
+
item.enabled &&
|
|
192
|
+
(item.lastStatus === "error" || item.lastStatus === "skipped"),
|
|
185
193
|
).length;
|
|
186
194
|
const pendingToggleIdentity = toggleAutomation.isPending
|
|
187
195
|
? toggleAutomation.variables
|
|
@@ -192,7 +200,7 @@ export default function AutomationsRoute() {
|
|
|
192
200
|
return (
|
|
193
201
|
<DispatchShell
|
|
194
202
|
title="Automations"
|
|
195
|
-
description="See scheduled and event-triggered jobs, pause them, or ask the agent to create one."
|
|
203
|
+
description="See scheduled and event-triggered jobs, inspect their checks and past runs, pause them, or ask the agent to create one."
|
|
196
204
|
>
|
|
197
205
|
<section className="flex flex-col gap-4">
|
|
198
206
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
@@ -238,6 +246,10 @@ export default function AutomationsRoute() {
|
|
|
238
246
|
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-muted-foreground">
|
|
239
247
|
<span>Last {automationLastRun(item)}</span>
|
|
240
248
|
<span>Next {automationNextRun(item)}</span>
|
|
249
|
+
<span>{automationScopeLabel(item)}</span>
|
|
250
|
+
{item.lastCheck ? (
|
|
251
|
+
<span>Checked {automationLastCheck(item)}</span>
|
|
252
|
+
) : null}
|
|
241
253
|
</div>
|
|
242
254
|
{item.lastError ? (
|
|
243
255
|
<div className="mt-1 flex flex-wrap items-baseline gap-x-2 gap-y-1 text-xs text-destructive">
|
|
@@ -256,14 +268,26 @@ export default function AutomationsRoute() {
|
|
|
256
268
|
) : null}
|
|
257
269
|
</div>
|
|
258
270
|
<div className="flex flex-col items-end gap-2">
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
271
|
+
<div className="flex items-center gap-2">
|
|
272
|
+
<Badge
|
|
273
|
+
variant={
|
|
274
|
+
status.tone === "danger" ? "destructive" : "outline"
|
|
275
|
+
}
|
|
276
|
+
className="h-5"
|
|
277
|
+
>
|
|
278
|
+
{status.label}
|
|
279
|
+
</Badge>
|
|
280
|
+
<Button
|
|
281
|
+
type="button"
|
|
282
|
+
variant="ghost"
|
|
283
|
+
size="sm"
|
|
284
|
+
className="h-7 px-2 text-xs"
|
|
285
|
+
onClick={() => setDetailsTarget(item)}
|
|
286
|
+
>
|
|
287
|
+
<IconListDetails size={14} />
|
|
288
|
+
Details
|
|
289
|
+
</Button>
|
|
290
|
+
</div>
|
|
267
291
|
<Switch
|
|
268
292
|
checked={!!item.enabled}
|
|
269
293
|
disabled={!canUpdate || isToggling}
|
|
@@ -288,6 +312,13 @@ export default function AutomationsRoute() {
|
|
|
288
312
|
)}
|
|
289
313
|
</div>
|
|
290
314
|
</section>
|
|
315
|
+
<AutomationRunHistoryDialog
|
|
316
|
+
automation={detailsTarget}
|
|
317
|
+
open={detailsTarget !== null}
|
|
318
|
+
onOpenChange={(open) => {
|
|
319
|
+
if (!open) setDetailsTarget(null);
|
|
320
|
+
}}
|
|
321
|
+
/>
|
|
291
322
|
</DispatchShell>
|
|
292
323
|
);
|
|
293
324
|
}
|