@agent-native/dispatch 0.24.0 → 0.24.2
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 +12 -12
- package/dist/components/admin-navigation.d.ts.map +1 -1
- package/dist/components/admin-navigation.js +21 -5
- package/dist/components/admin-navigation.js.map +1 -1
- package/dist/components/app-list-row.d.ts +1 -1
- package/dist/components/app-list-row.d.ts.map +1 -1
- package/dist/components/app-list-row.js +1 -1
- package/dist/components/app-list-row.js.map +1 -1
- package/dist/components/app-open-actions.d.ts +1 -17
- package/dist/components/app-open-actions.d.ts.map +1 -1
- package/dist/components/app-open-actions.js +1 -20
- package/dist/components/app-open-actions.js.map +1 -1
- package/dist/components/app-row-settings.d.ts +6 -0
- package/dist/components/app-row-settings.d.ts.map +1 -0
- package/dist/components/app-row-settings.js +11 -0
- package/dist/components/app-row-settings.js.map +1 -0
- package/dist/components/automation-run-history-dialog.d.ts.map +1 -1
- package/dist/components/automation-run-history-dialog.js +3 -2
- package/dist/components/automation-run-history-dialog.js.map +1 -1
- package/dist/components/connected-app-card.d.ts.map +1 -1
- package/dist/components/connected-app-card.js +1 -1
- package/dist/components/connected-app-card.js.map +1 -1
- package/dist/components/create-app-popover.js +2 -2
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +23 -7
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/dispatch-shell.d.ts.map +1 -1
- package/dist/components/dispatch-shell.js +5 -1
- package/dist/components/dispatch-shell.js.map +1 -1
- package/dist/components/docs-link.d.ts +16 -0
- package/dist/components/docs-link.d.ts.map +1 -0
- package/dist/components/docs-link.js +34 -0
- package/dist/components/docs-link.js.map +1 -0
- package/dist/components/layout/Layout.d.ts +1 -0
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +37 -28
- 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 +3 -2
- package/dist/components/messaging-setup-panel.js.map +1 -1
- package/dist/components/usage-alerts-panel.d.ts +11 -0
- package/dist/components/usage-alerts-panel.d.ts.map +1 -0
- package/dist/components/usage-alerts-panel.js +125 -0
- package/dist/components/usage-alerts-panel.js.map +1 -0
- package/dist/components/workspace-app-card.d.ts +3 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +70 -29
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-app-search.d.ts +10 -0
- package/dist/components/workspace-app-search.d.ts.map +1 -0
- package/dist/components/workspace-app-search.js +27 -0
- package/dist/components/workspace-app-search.js.map +1 -0
- package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -1
- package/dist/components/workspace-resource-effective-stack.js +3 -2
- package/dist/components/workspace-resource-effective-stack.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +2 -1
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/lib/workspace-app-layout.d.ts +19 -0
- package/dist/lib/workspace-app-layout.d.ts.map +1 -0
- package/dist/lib/workspace-app-layout.js +75 -0
- package/dist/lib/workspace-app-layout.js.map +1 -0
- package/dist/lib/workspace-apps.d.ts +10 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +59 -0
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/lib/workspace-apps.test.js +18 -1
- package/dist/lib/workspace-apps.test.js.map +1 -1
- package/dist/routes/pages/$appId.d.ts.map +1 -1
- package/dist/routes/pages/$appId.js +4 -2
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/routes/pages/agents.d.ts.map +1 -1
- package/dist/routes/pages/agents.js +2 -3
- package/dist/routes/pages/agents.js.map +1 -1
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +8 -3
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +31 -16
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/audit.d.ts.map +1 -1
- package/dist/routes/pages/audit.js +2 -1
- package/dist/routes/pages/audit.js.map +1 -1
- package/dist/routes/pages/dreams.d.ts.map +1 -1
- package/dist/routes/pages/dreams.js +14 -18
- package/dist/routes/pages/dreams.js.map +1 -1
- package/dist/routes/pages/integrations.d.ts.map +1 -1
- package/dist/routes/pages/integrations.js +51 -14
- package/dist/routes/pages/integrations.js.map +1 -1
- package/dist/routes/pages/metrics.d.ts.map +1 -1
- package/dist/routes/pages/metrics.js +4 -1
- package/dist/routes/pages/metrics.js.map +1 -1
- package/dist/routes/pages/operations.js +1 -1
- package/dist/routes/pages/operations.js.map +1 -1
- package/dist/routes/pages/settings.d.ts.map +1 -1
- package/dist/routes/pages/settings.js +7 -2
- package/dist/routes/pages/settings.js.map +1 -1
- package/dist/routes/pages/thread-debug.d.ts.map +1 -1
- package/dist/routes/pages/thread-debug.js +5 -8
- package/dist/routes/pages/thread-debug.js.map +1 -1
- package/dist/routes/pages/vault.d.ts.map +1 -1
- package/dist/routes/pages/vault.js +1 -1
- 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 +13 -10
- package/dist/routes/pages/workspace.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +12 -52
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/shared/app-roles.d.ts +9 -0
- package/dist/shared/app-roles.d.ts.map +1 -0
- package/dist/shared/app-roles.js +7 -0
- package/dist/shared/app-roles.js.map +1 -0
- package/package.json +4 -3
- package/src/components/admin-navigation.tsx +57 -11
- package/src/components/app-list-row.tsx +1 -2
- package/src/components/app-open-actions.tsx +5 -114
- package/src/components/app-row-settings.tsx +54 -0
- package/src/components/automation-run-history-dialog.tsx +8 -4
- package/src/components/connected-app-card.tsx +0 -1
- package/src/components/create-app-popover.spec.tsx +1 -1
- package/src/components/create-app-popover.tsx +2 -2
- package/src/components/dispatch-control-plane.spec.tsx +36 -2
- package/src/components/dispatch-control-plane.tsx +83 -27
- package/src/components/dispatch-shell.tsx +7 -1
- package/src/components/docs-link.tsx +52 -0
- package/src/components/layout/Layout.spec.tsx +10 -0
- package/src/components/layout/Layout.tsx +50 -23
- package/src/components/messaging-setup-panel.tsx +10 -3
- package/src/components/usage-alerts-panel.tsx +445 -0
- package/src/components/workspace-app-card.spec.tsx +130 -9
- package/src/components/workspace-app-card.tsx +128 -71
- package/src/components/workspace-app-search.tsx +96 -0
- package/src/components/workspace-resource-effective-stack.tsx +10 -5
- package/src/components/workspace-template-card.spec.tsx +52 -23
- package/src/components/workspace-template-card.tsx +7 -10
- package/src/lib/workspace-app-layout.spec.ts +47 -0
- package/src/lib/workspace-app-layout.ts +121 -0
- package/src/lib/workspace-apps.test.ts +45 -1
- package/src/lib/workspace-apps.ts +72 -0
- package/src/routes/pages/$appId.tsx +3 -6
- package/src/routes/pages/agents.tsx +6 -3
- package/src/routes/pages/apps.$appId.tsx +6 -6
- package/src/routes/pages/apps.tsx +116 -62
- package/src/routes/pages/audit.tsx +28 -8
- package/src/routes/pages/dreams.tsx +92 -142
- package/src/routes/pages/integrations.tsx +147 -68
- package/src/routes/pages/metrics.tsx +8 -0
- package/src/routes/pages/operations.tsx +2 -2
- package/src/routes/pages/settings.tsx +14 -9
- package/src/routes/pages/thread-debug.tsx +18 -36
- package/src/routes/pages/vault.tsx +7 -2
- package/src/routes/pages/workspace.tsx +92 -56
- package/src/server/index.ts +4 -0
- package/src/server/lib/app-creation-store.spec.ts +90 -43
- package/src/server/lib/app-creation-store.ts +15 -61
- package/src/shared/app-roles.ts +8 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import type { UsageAlertRule } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
useActionMutation,
|
|
4
|
+
useActionQuery,
|
|
5
|
+
} from "@agent-native/core/client/hooks";
|
|
6
|
+
import { IconBell, IconMail, IconPencil, IconX } from "@tabler/icons-react";
|
|
7
|
+
import { useMemo, useState } from "react";
|
|
8
|
+
import { toast } from "sonner";
|
|
9
|
+
|
|
10
|
+
import { Badge } from "./ui/badge";
|
|
11
|
+
import { Button } from "./ui/button";
|
|
12
|
+
import { Checkbox } from "./ui/checkbox";
|
|
13
|
+
import { Input } from "./ui/input";
|
|
14
|
+
import {
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from "./ui/select";
|
|
21
|
+
import { Skeleton } from "./ui/skeleton";
|
|
22
|
+
import { Switch } from "./ui/switch";
|
|
23
|
+
|
|
24
|
+
export interface UsageAlertAppOption {
|
|
25
|
+
id: string;
|
|
26
|
+
label: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface UsageAlertsPanelProps {
|
|
30
|
+
scope: "user" | "workspace";
|
|
31
|
+
appOptions?: UsageAlertAppOption[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type AlertUnit = "usd" | "builder-credits" | "tokens";
|
|
35
|
+
type AlertChannel = "in-app" | "email";
|
|
36
|
+
|
|
37
|
+
interface AlertDraft {
|
|
38
|
+
ruleId?: string;
|
|
39
|
+
appId: string;
|
|
40
|
+
unit: AlertUnit;
|
|
41
|
+
period: "day" | "month";
|
|
42
|
+
limit: string;
|
|
43
|
+
inApp: boolean;
|
|
44
|
+
email: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function asRules(value: unknown): UsageAlertRule[] {
|
|
48
|
+
if (Array.isArray(value)) return value as UsageAlertRule[];
|
|
49
|
+
if (value && typeof value === "object") {
|
|
50
|
+
const rules = (value as { rules?: unknown }).rules;
|
|
51
|
+
if (Array.isArray(rules)) return rules as UsageAlertRule[];
|
|
52
|
+
}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatAlertValue(rule: UsageAlertRule, value: number): string {
|
|
57
|
+
if (rule.unit === "usd") {
|
|
58
|
+
return value.toLocaleString(undefined, {
|
|
59
|
+
style: "currency",
|
|
60
|
+
currency: "USD",
|
|
61
|
+
maximumFractionDigits: 2,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return (
|
|
65
|
+
value.toLocaleString(undefined, {
|
|
66
|
+
maximumFractionDigits: rule.unit === "builder-credits" ? 2 : 0,
|
|
67
|
+
}) +
|
|
68
|
+
" " +
|
|
69
|
+
(rule.unit === "builder-credits" ? "credits" : "tokens")
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function draftFromRule(rule: UsageAlertRule): AlertDraft {
|
|
74
|
+
return {
|
|
75
|
+
ruleId: rule.id,
|
|
76
|
+
appId: rule.appId ?? "",
|
|
77
|
+
unit: rule.unit,
|
|
78
|
+
period: rule.period,
|
|
79
|
+
limit: String(rule.limit),
|
|
80
|
+
inApp: rule.channels.includes("in-app"),
|
|
81
|
+
email: rule.channels.includes("email"),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function defaultDraft(): AlertDraft {
|
|
86
|
+
return {
|
|
87
|
+
appId: "",
|
|
88
|
+
unit: "usd",
|
|
89
|
+
period: "day",
|
|
90
|
+
limit: "100",
|
|
91
|
+
inApp: true,
|
|
92
|
+
email: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function AlertEditor({
|
|
97
|
+
draft,
|
|
98
|
+
appOptions,
|
|
99
|
+
isExisting,
|
|
100
|
+
onChange,
|
|
101
|
+
onSave,
|
|
102
|
+
onCancel,
|
|
103
|
+
isPending,
|
|
104
|
+
}: {
|
|
105
|
+
draft: AlertDraft;
|
|
106
|
+
appOptions: UsageAlertAppOption[];
|
|
107
|
+
isExisting: boolean;
|
|
108
|
+
onChange: (patch: Partial<AlertDraft>) => void;
|
|
109
|
+
onSave: () => void;
|
|
110
|
+
onCancel: () => void;
|
|
111
|
+
isPending: boolean;
|
|
112
|
+
}) {
|
|
113
|
+
return (
|
|
114
|
+
<div className="grid gap-3 rounded-md border bg-background/60 p-3 sm:grid-cols-2 lg:grid-cols-5">
|
|
115
|
+
<label className="space-y-1.5 text-xs text-muted-foreground">
|
|
116
|
+
<span>App</span>
|
|
117
|
+
<Select
|
|
118
|
+
value={draft.appId || "all"}
|
|
119
|
+
onValueChange={(value) =>
|
|
120
|
+
onChange({ appId: value === "all" ? "" : value })
|
|
121
|
+
}
|
|
122
|
+
disabled={isExisting}
|
|
123
|
+
>
|
|
124
|
+
<SelectTrigger className="h-9 text-sm">
|
|
125
|
+
<SelectValue />
|
|
126
|
+
</SelectTrigger>
|
|
127
|
+
<SelectContent>
|
|
128
|
+
<SelectItem value="all">All apps</SelectItem>
|
|
129
|
+
{appOptions.map((app) => (
|
|
130
|
+
<SelectItem key={app.id} value={app.id}>
|
|
131
|
+
{app.label}
|
|
132
|
+
</SelectItem>
|
|
133
|
+
))}
|
|
134
|
+
</SelectContent>
|
|
135
|
+
</Select>
|
|
136
|
+
</label>
|
|
137
|
+
<label className="space-y-1.5 text-xs text-muted-foreground">
|
|
138
|
+
<span>Unit</span>
|
|
139
|
+
<Select
|
|
140
|
+
value={draft.unit}
|
|
141
|
+
onValueChange={(value) => onChange({ unit: value as AlertUnit })}
|
|
142
|
+
>
|
|
143
|
+
<SelectTrigger className="h-9 text-sm">
|
|
144
|
+
<SelectValue />
|
|
145
|
+
</SelectTrigger>
|
|
146
|
+
<SelectContent>
|
|
147
|
+
<SelectItem value="usd">Dollars</SelectItem>
|
|
148
|
+
<SelectItem value="builder-credits">Credits</SelectItem>
|
|
149
|
+
<SelectItem value="tokens">Tokens</SelectItem>
|
|
150
|
+
</SelectContent>
|
|
151
|
+
</Select>
|
|
152
|
+
</label>
|
|
153
|
+
<label className="space-y-1.5 text-xs text-muted-foreground">
|
|
154
|
+
<span>Limit</span>
|
|
155
|
+
<Input
|
|
156
|
+
type="number"
|
|
157
|
+
min="0.01"
|
|
158
|
+
step={draft.unit === "tokens" ? "1" : "0.01"}
|
|
159
|
+
value={draft.limit}
|
|
160
|
+
onChange={(event) => onChange({ limit: event.target.value })}
|
|
161
|
+
className="h-9 text-sm"
|
|
162
|
+
/>
|
|
163
|
+
</label>
|
|
164
|
+
<label className="space-y-1.5 text-xs text-muted-foreground">
|
|
165
|
+
<span>Period</span>
|
|
166
|
+
<Select
|
|
167
|
+
value={draft.period}
|
|
168
|
+
onValueChange={(value) =>
|
|
169
|
+
onChange({ period: value as "day" | "month" })
|
|
170
|
+
}
|
|
171
|
+
>
|
|
172
|
+
<SelectTrigger className="h-9 text-sm">
|
|
173
|
+
<SelectValue />
|
|
174
|
+
</SelectTrigger>
|
|
175
|
+
<SelectContent>
|
|
176
|
+
<SelectItem value="day">Per day</SelectItem>
|
|
177
|
+
<SelectItem value="month">Per month</SelectItem>
|
|
178
|
+
</SelectContent>
|
|
179
|
+
</Select>
|
|
180
|
+
</label>
|
|
181
|
+
<div className="flex flex-wrap items-end gap-3 pb-1 text-xs text-muted-foreground">
|
|
182
|
+
<label className="inline-flex items-center gap-2">
|
|
183
|
+
<Checkbox
|
|
184
|
+
checked={draft.inApp}
|
|
185
|
+
onCheckedChange={(checked) => onChange({ inApp: checked === true })}
|
|
186
|
+
/>
|
|
187
|
+
In app
|
|
188
|
+
</label>
|
|
189
|
+
<label className="inline-flex items-center gap-2">
|
|
190
|
+
<Checkbox
|
|
191
|
+
checked={draft.email}
|
|
192
|
+
onCheckedChange={(checked) => onChange({ email: checked === true })}
|
|
193
|
+
/>
|
|
194
|
+
Email
|
|
195
|
+
</label>
|
|
196
|
+
<Button
|
|
197
|
+
type="button"
|
|
198
|
+
size="sm"
|
|
199
|
+
className="ms-auto h-8"
|
|
200
|
+
onClick={onSave}
|
|
201
|
+
disabled={isPending}
|
|
202
|
+
>
|
|
203
|
+
Save
|
|
204
|
+
</Button>
|
|
205
|
+
<Button
|
|
206
|
+
type="button"
|
|
207
|
+
variant="ghost"
|
|
208
|
+
size="sm"
|
|
209
|
+
className="h-8"
|
|
210
|
+
onClick={onCancel}
|
|
211
|
+
>
|
|
212
|
+
Cancel
|
|
213
|
+
</Button>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function UsageAlertsPanel({
|
|
220
|
+
scope,
|
|
221
|
+
appOptions = [],
|
|
222
|
+
}: UsageAlertsPanelProps) {
|
|
223
|
+
const query = useActionQuery("get-usage-alerts", { scope });
|
|
224
|
+
const mutation = useActionMutation("manage-usage-alert", {
|
|
225
|
+
onSuccess: () => {
|
|
226
|
+
setEditing(null);
|
|
227
|
+
toast.success("Usage alert updated");
|
|
228
|
+
},
|
|
229
|
+
onError: (error) => toast.error(String(error)),
|
|
230
|
+
});
|
|
231
|
+
const [editing, setEditing] = useState<AlertDraft | null>(null);
|
|
232
|
+
const rules = asRules(query.data);
|
|
233
|
+
const appLabels = useMemo(
|
|
234
|
+
() => new Map(appOptions.map((app) => [app.id, app.label])),
|
|
235
|
+
[appOptions],
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
function saveDraft() {
|
|
239
|
+
if (!editing) return;
|
|
240
|
+
const limit = Number(editing.limit);
|
|
241
|
+
const channels: AlertChannel[] = [];
|
|
242
|
+
if (editing.inApp) channels.push("in-app");
|
|
243
|
+
if (editing.email) channels.push("email");
|
|
244
|
+
if (!Number.isFinite(limit) || limit <= 0) {
|
|
245
|
+
toast.error("Enter a positive threshold.");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (channels.length === 0) {
|
|
249
|
+
toast.error("Choose at least one delivery channel.");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
mutation.mutate({
|
|
253
|
+
operation: "save",
|
|
254
|
+
scope,
|
|
255
|
+
ruleId: editing.ruleId,
|
|
256
|
+
appId: editing.appId || null,
|
|
257
|
+
unit: editing.unit,
|
|
258
|
+
period: editing.period,
|
|
259
|
+
limit,
|
|
260
|
+
channels,
|
|
261
|
+
enabled: true,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<section className="rounded-lg bg-card">
|
|
267
|
+
<div className="flex items-start justify-between gap-3 border-b px-4 py-3">
|
|
268
|
+
<div>
|
|
269
|
+
<h2 className="text-sm font-semibold text-foreground">
|
|
270
|
+
Usage alerts
|
|
271
|
+
</h2>
|
|
272
|
+
<p className="mt-1 max-w-2xl text-xs leading-5 text-muted-foreground">
|
|
273
|
+
Get a quiet in-app and email notice when usage crosses a daily or
|
|
274
|
+
monthly limit.
|
|
275
|
+
</p>
|
|
276
|
+
</div>
|
|
277
|
+
<Button
|
|
278
|
+
type="button"
|
|
279
|
+
variant="outline"
|
|
280
|
+
size="sm"
|
|
281
|
+
onClick={() => setEditing(defaultDraft())}
|
|
282
|
+
disabled={mutation.isPending}
|
|
283
|
+
>
|
|
284
|
+
Add alert
|
|
285
|
+
</Button>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<div className="divide-y">
|
|
289
|
+
{query.isLoading && rules.length === 0 ? (
|
|
290
|
+
<div className="space-y-3 px-4 py-5">
|
|
291
|
+
{Array.from({ length: 3 }).map((_, index) => (
|
|
292
|
+
<div key={index} className="space-y-2">
|
|
293
|
+
<Skeleton className="h-4 w-2/3 max-w-sm" />
|
|
294
|
+
<Skeleton className="h-3 w-1/2 max-w-xs" />
|
|
295
|
+
</div>
|
|
296
|
+
))}
|
|
297
|
+
</div>
|
|
298
|
+
) : null}
|
|
299
|
+
{query.isError ? (
|
|
300
|
+
<div className="px-4 py-6 text-sm text-destructive">
|
|
301
|
+
Usage alerts are unavailable for this scope.{" "}
|
|
302
|
+
{query.error instanceof Error ? query.error.message : "Try again."}
|
|
303
|
+
</div>
|
|
304
|
+
) : null}
|
|
305
|
+
{!query.isLoading && !query.isError && rules.length === 0 ? (
|
|
306
|
+
<div className="px-4 py-6 text-sm text-muted-foreground">
|
|
307
|
+
No alerts yet. The recommended default is $100 per day across all
|
|
308
|
+
apps.
|
|
309
|
+
</div>
|
|
310
|
+
) : null}
|
|
311
|
+
{rules.map((rule) => {
|
|
312
|
+
const isEditing = editing?.ruleId === rule.id;
|
|
313
|
+
const target = rule.appId
|
|
314
|
+
? appLabels.get(rule.appId) || rule.appId
|
|
315
|
+
: "All apps";
|
|
316
|
+
return (
|
|
317
|
+
<div key={rule.id} className="px-4 py-4">
|
|
318
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
319
|
+
<div className="min-w-0">
|
|
320
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
321
|
+
<span className="text-sm font-medium text-foreground">
|
|
322
|
+
{target}
|
|
323
|
+
</span>
|
|
324
|
+
{rule.isDefault ? (
|
|
325
|
+
<Badge variant="secondary">Default</Badge>
|
|
326
|
+
) : null}
|
|
327
|
+
<Badge
|
|
328
|
+
variant={
|
|
329
|
+
rule.status === "triggered" ? "destructive" : "outline"
|
|
330
|
+
}
|
|
331
|
+
>
|
|
332
|
+
{rule.status === "triggered"
|
|
333
|
+
? "Over limit"
|
|
334
|
+
: rule.status === "dismissed"
|
|
335
|
+
? "Dismissed"
|
|
336
|
+
: "On track"}
|
|
337
|
+
</Badge>
|
|
338
|
+
</div>
|
|
339
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
340
|
+
{formatAlertValue(rule, rule.current)} of{" "}
|
|
341
|
+
{formatAlertValue(rule, rule.limit)} · per {rule.period}
|
|
342
|
+
{rule.percent > 0 ? " · " + rule.percent + "%" : ""}
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
<div className="flex items-center gap-2">
|
|
346
|
+
<Switch
|
|
347
|
+
checked={rule.enabled}
|
|
348
|
+
onCheckedChange={(checked) =>
|
|
349
|
+
mutation.mutate({
|
|
350
|
+
operation: "set-enabled",
|
|
351
|
+
scope,
|
|
352
|
+
ruleId: rule.id,
|
|
353
|
+
enabled: checked,
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
aria-label={
|
|
357
|
+
(rule.enabled ? "Disable " : "Enable ") +
|
|
358
|
+
target +
|
|
359
|
+
" usage alert"
|
|
360
|
+
}
|
|
361
|
+
disabled={mutation.isPending}
|
|
362
|
+
/>
|
|
363
|
+
<Button
|
|
364
|
+
type="button"
|
|
365
|
+
variant="ghost"
|
|
366
|
+
size="icon"
|
|
367
|
+
onClick={() => setEditing(draftFromRule(rule))}
|
|
368
|
+
aria-label={"Edit " + target + " usage alert"}
|
|
369
|
+
disabled={mutation.isPending}
|
|
370
|
+
>
|
|
371
|
+
<IconPencil className="size-4" />
|
|
372
|
+
</Button>
|
|
373
|
+
{rule.status === "triggered" ? (
|
|
374
|
+
<Button
|
|
375
|
+
type="button"
|
|
376
|
+
variant="ghost"
|
|
377
|
+
size="icon"
|
|
378
|
+
onClick={() =>
|
|
379
|
+
mutation.mutate({
|
|
380
|
+
operation: "dismiss",
|
|
381
|
+
scope,
|
|
382
|
+
ruleId: rule.id,
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
aria-label={"Dismiss " + target + " usage alert"}
|
|
386
|
+
disabled={mutation.isPending}
|
|
387
|
+
>
|
|
388
|
+
<IconX className="size-4" />
|
|
389
|
+
</Button>
|
|
390
|
+
) : null}
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
<div className="mt-2 flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
|
394
|
+
{rule.channels.includes("in-app") ? (
|
|
395
|
+
<span className="inline-flex items-center gap-1">
|
|
396
|
+
<IconBell className="size-3.5" /> In app
|
|
397
|
+
</span>
|
|
398
|
+
) : null}
|
|
399
|
+
{rule.channels.includes("email") ? (
|
|
400
|
+
<span className="inline-flex items-center gap-1">
|
|
401
|
+
<IconMail className="size-3.5" /> Email
|
|
402
|
+
</span>
|
|
403
|
+
) : null}
|
|
404
|
+
</div>
|
|
405
|
+
{isEditing && editing ? (
|
|
406
|
+
<div className="mt-4">
|
|
407
|
+
<AlertEditor
|
|
408
|
+
draft={editing}
|
|
409
|
+
appOptions={appOptions}
|
|
410
|
+
isExisting
|
|
411
|
+
onChange={(patch) =>
|
|
412
|
+
setEditing((current) =>
|
|
413
|
+
current ? { ...current, ...patch } : current,
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
onSave={saveDraft}
|
|
417
|
+
onCancel={() => setEditing(null)}
|
|
418
|
+
isPending={mutation.isPending}
|
|
419
|
+
/>
|
|
420
|
+
</div>
|
|
421
|
+
) : null}
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
})}
|
|
425
|
+
{editing && !editing.ruleId ? (
|
|
426
|
+
<div className="px-4 py-4">
|
|
427
|
+
<AlertEditor
|
|
428
|
+
draft={editing}
|
|
429
|
+
appOptions={appOptions}
|
|
430
|
+
isExisting={false}
|
|
431
|
+
onChange={(patch) =>
|
|
432
|
+
setEditing((current) =>
|
|
433
|
+
current ? { ...current, ...patch } : current,
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
onSave={saveDraft}
|
|
437
|
+
onCancel={() => setEditing(null)}
|
|
438
|
+
isPending={mutation.isPending}
|
|
439
|
+
/>
|
|
440
|
+
</div>
|
|
441
|
+
) : null}
|
|
442
|
+
</div>
|
|
443
|
+
</section>
|
|
444
|
+
);
|
|
445
|
+
}
|
|
@@ -22,7 +22,8 @@ vi.mock("@agent-native/core/client/i18n", () => ({
|
|
|
22
22
|
useFormatters: () => ({
|
|
23
23
|
formatDate: (value: string) => value,
|
|
24
24
|
}),
|
|
25
|
-
useT: () => (key: string) =>
|
|
25
|
+
useT: () => (key: string, values?: Record<string, unknown>) =>
|
|
26
|
+
String(values?.defaultValue ?? key),
|
|
26
27
|
}));
|
|
27
28
|
|
|
28
29
|
describe("WorkspaceAppCard", () => {
|
|
@@ -42,7 +43,7 @@ describe("WorkspaceAppCard", () => {
|
|
|
42
43
|
vi.unstubAllGlobals();
|
|
43
44
|
});
|
|
44
45
|
|
|
45
|
-
it("
|
|
46
|
+
it("opens ready apps inline and keeps new-tab opening in the chevron menu", async () => {
|
|
46
47
|
await act(async () => {
|
|
47
48
|
root.render(
|
|
48
49
|
<MemoryRouter>
|
|
@@ -61,14 +62,12 @@ describe("WorkspaceAppCard", () => {
|
|
|
61
62
|
);
|
|
62
63
|
});
|
|
63
64
|
|
|
64
|
-
const
|
|
65
|
-
|
|
65
|
+
const openButton = container.querySelector<HTMLButtonElement>(
|
|
66
|
+
".app-open-actions__primary",
|
|
66
67
|
);
|
|
67
|
-
expect(
|
|
68
|
-
expect(
|
|
69
|
-
expect(
|
|
70
|
-
expect(appLink?.getAttribute("rel")).toBe("noreferrer");
|
|
71
|
-
expect(appLink?.querySelector("svg")).toBeNull();
|
|
68
|
+
expect(openButton).not.toBeNull();
|
|
69
|
+
expect(openButton?.textContent).toContain("Open app");
|
|
70
|
+
expect(container.querySelector('a[href="/analytics"]')).toBeNull();
|
|
72
71
|
|
|
73
72
|
expect(
|
|
74
73
|
container.querySelector(
|
|
@@ -78,6 +77,27 @@ describe("WorkspaceAppCard", () => {
|
|
|
78
77
|
expect(document.body.textContent).not.toContain("Open in new tab");
|
|
79
78
|
expect(document.body.textContent).not.toContain("Add app");
|
|
80
79
|
|
|
80
|
+
await act(async () => {
|
|
81
|
+
container
|
|
82
|
+
.querySelector<HTMLButtonElement>(
|
|
83
|
+
'button[aria-label="Open options for Analytics"]',
|
|
84
|
+
)
|
|
85
|
+
?.dispatchEvent(
|
|
86
|
+
new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
const newTabItem = Array.from(
|
|
90
|
+
document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
|
|
91
|
+
).find((item) => item.textContent?.includes("Open in new tab"));
|
|
92
|
+
expect(newTabItem).not.toBeUndefined();
|
|
93
|
+
expect(newTabItem?.getAttribute("href")).toBe("/analytics");
|
|
94
|
+
expect(newTabItem?.getAttribute("target")).toBe("_blank");
|
|
95
|
+
const openMenu = document.querySelector<HTMLElement>('[role="menu"]');
|
|
96
|
+
expect(openMenu?.className).toContain("w-48");
|
|
97
|
+
expect(openMenu?.className).toContain("bg-popover");
|
|
98
|
+
expect(openMenu?.className).toContain("shadow-md");
|
|
99
|
+
expect(newTabItem?.querySelector("svg")).not.toBeNull();
|
|
100
|
+
|
|
81
101
|
const settingsButton = container.querySelector<HTMLButtonElement>(
|
|
82
102
|
'button[aria-label="Settings for Analytics"]',
|
|
83
103
|
);
|
|
@@ -95,6 +115,47 @@ describe("WorkspaceAppCard", () => {
|
|
|
95
115
|
).toBeNull();
|
|
96
116
|
});
|
|
97
117
|
|
|
118
|
+
it("keeps pinning in the app open menu", async () => {
|
|
119
|
+
const onTogglePinned = vi.fn();
|
|
120
|
+
await act(async () => {
|
|
121
|
+
root.render(
|
|
122
|
+
<MemoryRouter>
|
|
123
|
+
<TooltipProvider>
|
|
124
|
+
<WorkspaceAppCard
|
|
125
|
+
app={{
|
|
126
|
+
id: "analytics",
|
|
127
|
+
name: "Analytics",
|
|
128
|
+
path: "/analytics",
|
|
129
|
+
status: "ready",
|
|
130
|
+
}}
|
|
131
|
+
isPinned
|
|
132
|
+
onTogglePinned={onTogglePinned}
|
|
133
|
+
/>
|
|
134
|
+
</TooltipProvider>
|
|
135
|
+
</MemoryRouter>,
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const optionsButton = container.querySelector<HTMLButtonElement>(
|
|
140
|
+
'button[aria-label="Open options for Analytics"]',
|
|
141
|
+
);
|
|
142
|
+
expect(optionsButton).not.toBeNull();
|
|
143
|
+
|
|
144
|
+
await act(async () => {
|
|
145
|
+
optionsButton?.dispatchEvent(
|
|
146
|
+
new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const pinItem = Array.from(
|
|
151
|
+
document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
|
|
152
|
+
).find((item) => item.textContent?.includes("Unpin Analytics"));
|
|
153
|
+
expect(pinItem).not.toBeUndefined();
|
|
154
|
+
|
|
155
|
+
await act(async () => pinItem?.click());
|
|
156
|
+
expect(onTogglePinned).toHaveBeenCalledOnce();
|
|
157
|
+
});
|
|
158
|
+
|
|
98
159
|
it("labels per-app context as agent resources while preserving workspace scope", async () => {
|
|
99
160
|
await act(async () => {
|
|
100
161
|
root.render(
|
|
@@ -127,6 +188,11 @@ describe("WorkspaceAppCard", () => {
|
|
|
127
188
|
document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
|
|
128
189
|
).find((item) => item.textContent?.includes("Agent resources"));
|
|
129
190
|
expect(resourcesItem).not.toBeUndefined();
|
|
191
|
+
const settingsMenu = document.querySelector<HTMLElement>('[role="menu"]');
|
|
192
|
+
expect(settingsMenu?.className).toContain("w-48");
|
|
193
|
+
expect(settingsMenu?.className).toContain("bg-popover");
|
|
194
|
+
expect(settingsMenu?.className).toContain("shadow-md");
|
|
195
|
+
expect(document.querySelectorAll('[role="menuitem"] svg').length).toBe(4);
|
|
130
196
|
|
|
131
197
|
await act(async () => resourcesItem?.click());
|
|
132
198
|
|
|
@@ -150,6 +216,7 @@ describe("WorkspaceAppCard", () => {
|
|
|
150
216
|
name: "New app",
|
|
151
217
|
path: "/new-app",
|
|
152
218
|
builderUrl: "https://builder.example.com/projects/new-app",
|
|
219
|
+
branchName: "pending-branch-id",
|
|
153
220
|
status: "pending",
|
|
154
221
|
}}
|
|
155
222
|
/>
|
|
@@ -167,6 +234,12 @@ describe("WorkspaceAppCard", () => {
|
|
|
167
234
|
);
|
|
168
235
|
expect(appLink?.getAttribute("target")).toBe("_blank");
|
|
169
236
|
expect(appLink?.getAttribute("rel")).toBe("noreferrer");
|
|
237
|
+
expect(container.textContent).toContain("Open in Builder");
|
|
238
|
+
expect(container.textContent).not.toContain("pending-branch-id");
|
|
239
|
+
expect(container.querySelector(".app-open-actions")).toBeNull();
|
|
240
|
+
expect(
|
|
241
|
+
container.querySelector('button[aria-label="Open options for New app"]'),
|
|
242
|
+
).toBeNull();
|
|
170
243
|
});
|
|
171
244
|
|
|
172
245
|
it("shows ownership metadata in the settings menu", async () => {
|
|
@@ -205,5 +278,53 @@ describe("WorkspaceAppCard", () => {
|
|
|
205
278
|
expect(document.body.textContent).toContain("creator@example.com");
|
|
206
279
|
expect(document.body.textContent).toContain("owner@example.com");
|
|
207
280
|
expect(document.body.textContent).toContain("Growth, Operations");
|
|
281
|
+
const renderedLabels = Array.from(
|
|
282
|
+
document.querySelectorAll("span"),
|
|
283
|
+
(span) => span.textContent,
|
|
284
|
+
);
|
|
285
|
+
expect(renderedLabels).not.toContain("agents.dashboardMetadataCreated");
|
|
286
|
+
expect(renderedLabels).not.toContain("agents.notTracked");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("hides metadata rows without tracked values", async () => {
|
|
290
|
+
await act(async () => {
|
|
291
|
+
root.render(
|
|
292
|
+
<MemoryRouter>
|
|
293
|
+
<TooltipProvider>
|
|
294
|
+
<WorkspaceAppCard
|
|
295
|
+
app={{
|
|
296
|
+
id: "analytics",
|
|
297
|
+
name: "Analytics",
|
|
298
|
+
path: "/analytics",
|
|
299
|
+
createdAt: "2026-07-28T12:00:00.000Z",
|
|
300
|
+
createdBy: "Not tracked",
|
|
301
|
+
teams: ["Not tracked"],
|
|
302
|
+
status: "ready",
|
|
303
|
+
}}
|
|
304
|
+
/>
|
|
305
|
+
</TooltipProvider>
|
|
306
|
+
</MemoryRouter>,
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const settingsButton = container.querySelector<HTMLButtonElement>(
|
|
311
|
+
'button[aria-label="Settings for Analytics"]',
|
|
312
|
+
);
|
|
313
|
+
expect(settingsButton).not.toBeNull();
|
|
314
|
+
|
|
315
|
+
await act(async () => {
|
|
316
|
+
settingsButton?.dispatchEvent(
|
|
317
|
+
new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const renderedLabels = Array.from(
|
|
322
|
+
document.querySelectorAll("span"),
|
|
323
|
+
(span) => span.textContent,
|
|
324
|
+
);
|
|
325
|
+
expect(renderedLabels).not.toContain("agents.notTracked");
|
|
326
|
+
expect(renderedLabels).not.toContain("agents.dashboardMetadataCreated");
|
|
327
|
+
expect(renderedLabels).not.toContain("dispatch.pages.appMetadataOwner");
|
|
328
|
+
expect(renderedLabels).not.toContain("dispatch.pages.appMetadataTeams");
|
|
208
329
|
});
|
|
209
330
|
});
|