@checkstack/automation-frontend 0.5.0 → 0.6.1
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/CHANGELOG.md +71 -0
- package/package.json +10 -9
- package/src/editor/action-leaf-cards.tsx +2 -2
- package/src/editor/useActionOptionResolvers.ts +51 -0
- package/src/index.tsx +11 -1
- package/src/pages/AutomationEditPage.tsx +31 -1
- package/src/pages/AutomationTemplatePickerPage.tsx +152 -0
- package/tsconfig.json +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# @checkstack/automation-frontend
|
|
2
2
|
|
|
3
|
+
## 0.6.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @checkstack/script-packages-frontend@0.3.11
|
|
8
|
+
|
|
9
|
+
## 0.6.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 6005271: Add AI "skills" - reusable prompt templates for the chat assistant and the
|
|
14
|
+
`ai_analyze` automation action. A skill bundles a system-prompt fragment, an
|
|
15
|
+
optional starter prompt, and (for analyze) suggested output fields, tagged with
|
|
16
|
+
the surfaces it targets.
|
|
17
|
+
|
|
18
|
+
Skills come from two sources merged into one catalogue: builtin skills
|
|
19
|
+
contributed by core/plugins via the new `aiSkillExtensionPoint`, and GLOBAL
|
|
20
|
+
user skills authored by operators (new `ai_skill` table) and visible to everyone
|
|
21
|
+
who can read skills. New access rules `ai.skill.read`, `ai.skill-create.manage`
|
|
22
|
+
(a dedicated create permission), and `ai.skill.manage` (edit/delete, author-only
|
|
23
|
+
with admin moderation) gate the feature - all default-on, admin-revocable.
|
|
24
|
+
|
|
25
|
+
The chat composer gains a skill picker (its system prompt seeds the turn, its
|
|
26
|
+
starter prompt seeds the message box); the `ai_analyze` action gains an optional
|
|
27
|
+
`skillId` that seeds the system prompt, prompt (when blank), and output fields
|
|
28
|
+
(when none) - explicit config always wins. A new "AI skills" settings page lets
|
|
29
|
+
operators browse, view full details (prompts + output fields), publish, edit,
|
|
30
|
+
and delete their global skills. Ships six builtin skills across chat and analyze.
|
|
31
|
+
|
|
32
|
+
To support rich pickers, `@checkstack/ui`'s `DynamicForm` gains a `catalog`
|
|
33
|
+
options style (`x-options-style: "catalog"`, with resolver options carrying an
|
|
34
|
+
optional `description`) that renders a browsable modal of cards instead of a
|
|
35
|
+
plain Select, and `@checkstack/backend-api` propagates the new annotation. The
|
|
36
|
+
shared `PageHeader` now wraps a long subtitle beside its actions instead of
|
|
37
|
+
letting them overlap.
|
|
38
|
+
|
|
39
|
+
- 748268c: Add an example-automation template catalogue. Creating a new automation now
|
|
40
|
+
opens a picker (`/automation/new`) with curated, ready-to-use starting points
|
|
41
|
+
grouped by category, plus a "Blank automation" option. Selecting a template
|
|
42
|
+
seeds the editor (the operator still chooses a service account and saves).
|
|
43
|
+
|
|
44
|
+
Templates are an extensible registry: external plugins contribute their own via
|
|
45
|
+
the new `automationTemplateExtensionPoint`, exactly like actions / triggers /
|
|
46
|
+
artifact types. Every registered template is validated against the LIVE
|
|
47
|
+
trigger/action/artifact registries at server startup - a template that
|
|
48
|
+
references a capability that is not installed is withheld with a console
|
|
49
|
+
warning, and one whose definition no longer validates (interface drift) is
|
|
50
|
+
withheld with a console error - so a template can never silently drift when an
|
|
51
|
+
action, trigger, condition, or artifact interface changes.
|
|
52
|
+
|
|
53
|
+
Ships five built-in templates spanning incident response and alerting
|
|
54
|
+
(AI-triage-and-file-Jira-bug, close-Jira-on-recovery, AI-summarize-incident,
|
|
55
|
+
page-on-call-on-sustained-degradation, AI-severity-escalation).
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- Updated dependencies [4134ed9]
|
|
60
|
+
- Updated dependencies [6005271]
|
|
61
|
+
- Updated dependencies [748268c]
|
|
62
|
+
- Updated dependencies [4134ed9]
|
|
63
|
+
- Updated dependencies [079369a]
|
|
64
|
+
- @checkstack/ai-common@0.4.0
|
|
65
|
+
- @checkstack/ui@1.16.0
|
|
66
|
+
- @checkstack/automation-common@0.6.0
|
|
67
|
+
- @checkstack/auth-common@0.9.1
|
|
68
|
+
- @checkstack/template-engine@0.4.4
|
|
69
|
+
- @checkstack/gitops-frontend@0.5.7
|
|
70
|
+
- @checkstack/script-packages-frontend@0.3.10
|
|
71
|
+
- @checkstack/secrets-frontend@0.2.6
|
|
72
|
+
- @checkstack/catalog-common@2.3.6
|
|
73
|
+
|
|
3
74
|
## 0.5.0
|
|
4
75
|
|
|
5
76
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/automation-frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.tsx",
|
|
@@ -13,18 +13,19 @@
|
|
|
13
13
|
"lint:code": "eslint . --max-warnings 0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@checkstack/auth-common": "0.9.
|
|
17
|
-
"@checkstack/automation-common": "0.
|
|
18
|
-
"@checkstack/
|
|
16
|
+
"@checkstack/auth-common": "0.9.1",
|
|
17
|
+
"@checkstack/automation-common": "0.6.0",
|
|
18
|
+
"@checkstack/ai-common": "0.4.0",
|
|
19
|
+
"@checkstack/catalog-common": "2.3.6",
|
|
19
20
|
"@checkstack/common": "0.15.0",
|
|
20
21
|
"@checkstack/frontend-api": "0.9.0",
|
|
21
|
-
"@checkstack/gitops-frontend": "0.5.
|
|
22
|
+
"@checkstack/gitops-frontend": "0.5.7",
|
|
22
23
|
"@checkstack/integration-common": "0.9.0",
|
|
23
|
-
"@checkstack/script-packages-frontend": "0.3.
|
|
24
|
-
"@checkstack/secrets-frontend": "0.2.
|
|
24
|
+
"@checkstack/script-packages-frontend": "0.3.11",
|
|
25
|
+
"@checkstack/secrets-frontend": "0.2.6",
|
|
25
26
|
"@checkstack/signal-frontend": "0.2.4",
|
|
26
|
-
"@checkstack/template-engine": "0.4.
|
|
27
|
-
"@checkstack/ui": "1.
|
|
27
|
+
"@checkstack/template-engine": "0.4.4",
|
|
28
|
+
"@checkstack/ui": "1.16.0",
|
|
28
29
|
"@dnd-kit/core": "^6.3.1",
|
|
29
30
|
"@dnd-kit/sortable": "^8.0.0",
|
|
30
31
|
"@dnd-kit/utilities": "^3.2.2",
|
|
@@ -32,7 +32,7 @@ import type {
|
|
|
32
32
|
import { useAutomationRegistry } from "./registry-context";
|
|
33
33
|
import { ItemPicker } from "./ItemPicker";
|
|
34
34
|
import { ConditionEditor } from "./ConditionEditor";
|
|
35
|
-
import {
|
|
35
|
+
import { useActionOptionResolvers } from "./useActionOptionResolvers";
|
|
36
36
|
import { automationScriptTestRenderer } from "./ScriptTestRenderer";
|
|
37
37
|
import {
|
|
38
38
|
useScriptPackageTypeAcquisition,
|
|
@@ -127,7 +127,7 @@ export const ProviderActionBody: React.FC<{
|
|
|
127
127
|
// `connectionProviderId`; the bridge turns their `x-options-resolver`
|
|
128
128
|
// fields into a live connection picker + cascading provider dropdowns.
|
|
129
129
|
// Non-connection actions get an empty map (no-op) since the id is absent.
|
|
130
|
-
const optionsResolvers =
|
|
130
|
+
const optionsResolvers = useActionOptionResolvers(
|
|
131
131
|
selected?.connectionProviderId,
|
|
132
132
|
);
|
|
133
133
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
3
|
+
import { AiApi } from "@checkstack/ai-common";
|
|
4
|
+
import type { OptionsResolver } from "@checkstack/ui";
|
|
5
|
+
import { useConnectionOptionResolvers } from "./useConnectionOptionResolvers";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The `x-options-resolver` name the `ai_analyze` action declares for its
|
|
9
|
+
* `skillId` field. Resolved here (NOT through the connection bridge) because
|
|
10
|
+
* skills are not connection-scoped: the connection bridge returns `[]` whenever
|
|
11
|
+
* no `connectionId` is selected, which would wrongly empty the skill dropdown.
|
|
12
|
+
*/
|
|
13
|
+
const AI_SKILL_RESOLVER_NAME = "aiSkillOptions";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolver map for an action editor's `x-options-resolver` config fields.
|
|
17
|
+
*
|
|
18
|
+
* Composes the connection-backed resolvers (connection picker + cascading
|
|
19
|
+
* provider dropdowns) with non-connection resolvers that need their own data
|
|
20
|
+
* source. Today the only non-connection resolver is `aiSkillOptions`, which
|
|
21
|
+
* lists the AI skills targeting the `ai_analyze` action via `listSkills` -
|
|
22
|
+
* independent of any selected connection.
|
|
23
|
+
*/
|
|
24
|
+
export function useActionOptionResolvers(
|
|
25
|
+
connectionProviderId?: string,
|
|
26
|
+
): Record<string, OptionsResolver> {
|
|
27
|
+
const connectionResolvers = useConnectionOptionResolvers(connectionProviderId);
|
|
28
|
+
const aiClient = usePluginClient(AiApi);
|
|
29
|
+
|
|
30
|
+
return React.useMemo(() => {
|
|
31
|
+
const skillResolver: OptionsResolver = async () => {
|
|
32
|
+
const { skills } = await aiClient.listSkills.call({ target: "ai_analyze" });
|
|
33
|
+
return skills.map((skill) => ({
|
|
34
|
+
value: skill.id,
|
|
35
|
+
label: skill.name,
|
|
36
|
+
description: skill.description,
|
|
37
|
+
}));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return new Proxy<Record<string, OptionsResolver>>(
|
|
41
|
+
{},
|
|
42
|
+
{
|
|
43
|
+
get: (_target, prop) => {
|
|
44
|
+
if (prop === AI_SKILL_RESOLVER_NAME) return skillResolver;
|
|
45
|
+
if (typeof prop !== "string") return;
|
|
46
|
+
return connectionResolvers[prop];
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
}, [connectionResolvers, aiClient]);
|
|
51
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -20,7 +20,8 @@ export {
|
|
|
20
20
|
* Routes:
|
|
21
21
|
*
|
|
22
22
|
* - `/automation/` → list view
|
|
23
|
-
* - `/automation/new` →
|
|
23
|
+
* - `/automation/new` → template picker (create)
|
|
24
|
+
* - `/automation/new/blank` → blank / template-seeded editor
|
|
24
25
|
* - `/automation/:automationId` → edit page
|
|
25
26
|
* - `/automation/:automationId/runs` → run history
|
|
26
27
|
* - `/automation/:automationId/runs/:runId` → single run drill-down
|
|
@@ -53,6 +54,15 @@ export default createFrontendPlugin({
|
|
|
53
54
|
},
|
|
54
55
|
{
|
|
55
56
|
route: automationRoutes.routes.create,
|
|
57
|
+
load: () =>
|
|
58
|
+
import("./pages/AutomationTemplatePickerPage").then((m) => ({
|
|
59
|
+
default: m.AutomationTemplatePickerPage,
|
|
60
|
+
})),
|
|
61
|
+
title: "New automation",
|
|
62
|
+
accessRule: automationAccess.manage,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
route: automationRoutes.routes.createBlank,
|
|
56
66
|
load: () =>
|
|
57
67
|
import("./pages/AutomationEditPage").then((m) => ({
|
|
58
68
|
default: m.AutomationEditPage,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Link, useNavigate, useParams } from "react-router-dom";
|
|
2
|
+
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
|
3
3
|
import {
|
|
4
4
|
ChevronLeft,
|
|
5
5
|
History as HistoryIcon,
|
|
@@ -99,6 +99,7 @@ type EditTab = "visual" | "yaml";
|
|
|
99
99
|
*/
|
|
100
100
|
const AutomationEditContent: React.FC = () => {
|
|
101
101
|
const { automationId } = useParams<{ automationId: string }>();
|
|
102
|
+
const [searchParams] = useSearchParams();
|
|
102
103
|
const isNew = !automationId || automationId === "new";
|
|
103
104
|
const client = usePluginClient(AutomationApi);
|
|
104
105
|
const accessApi = useApi(accessApiRef);
|
|
@@ -183,6 +184,35 @@ const AutomationEditContent: React.FC = () => {
|
|
|
183
184
|
},
|
|
184
185
|
);
|
|
185
186
|
|
|
187
|
+
// Template seeding (new automations only): `/automation/new/blank?template=<id>`
|
|
188
|
+
// pre-fills the editor from a curated example template. The template's
|
|
189
|
+
// `definition` seeds the canonical state once; the operator still picks a
|
|
190
|
+
// `runAs` service account and saves. `enabled: false` on the query keeps it
|
|
191
|
+
// off the wire for blank (no-template) and existing-automation editors.
|
|
192
|
+
const templateId = isNew ? searchParams.get("template") : null;
|
|
193
|
+
const templatesQuery = client.listAutomationTemplates.useQuery(undefined, {
|
|
194
|
+
enabled: Boolean(templateId),
|
|
195
|
+
gcTime: 0,
|
|
196
|
+
});
|
|
197
|
+
const selectedTemplate = templateId
|
|
198
|
+
? templatesQuery.data?.items.find((t) => t.id === templateId)
|
|
199
|
+
: undefined;
|
|
200
|
+
useInitOnceForKey(
|
|
201
|
+
templatesQuery.isFetchedAfterMount ? selectedTemplate : undefined,
|
|
202
|
+
selectedTemplate?.id,
|
|
203
|
+
(template) => {
|
|
204
|
+
const normalized: AutomationDefinition = {
|
|
205
|
+
...template.definition,
|
|
206
|
+
triggers: assignDefaultTriggerIds(template.definition.triggers),
|
|
207
|
+
actions: assignDefaultIds(template.definition.actions, new Set()),
|
|
208
|
+
};
|
|
209
|
+
setName(template.definition.name);
|
|
210
|
+
setDescription(template.definition.description ?? "");
|
|
211
|
+
setDefinition(normalized);
|
|
212
|
+
setYamlText(stringifyYaml(normalized));
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
186
216
|
// Keep the YAML mirror in sync with definition while the visual editor
|
|
187
217
|
// is active — the YAML tab needs a non-stale starting point when the
|
|
188
218
|
// operator switches over.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import { Workflow, FilePlus2, ArrowRight } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
usePluginClient,
|
|
6
|
+
accessApiRef,
|
|
7
|
+
useApi,
|
|
8
|
+
wrapInSuspense,
|
|
9
|
+
} from "@checkstack/frontend-api";
|
|
10
|
+
import {
|
|
11
|
+
AutomationApi,
|
|
12
|
+
automationAccess,
|
|
13
|
+
automationRoutes,
|
|
14
|
+
type AutomationTemplate,
|
|
15
|
+
} from "@checkstack/automation-common";
|
|
16
|
+
import {
|
|
17
|
+
PageLayout,
|
|
18
|
+
Card,
|
|
19
|
+
CardHeader,
|
|
20
|
+
CardTitle,
|
|
21
|
+
CardContent,
|
|
22
|
+
Button,
|
|
23
|
+
Badge,
|
|
24
|
+
DynamicIcon,
|
|
25
|
+
LoadingSpinner,
|
|
26
|
+
QueryErrorState,
|
|
27
|
+
} from "@checkstack/ui";
|
|
28
|
+
import { resolveRoute } from "@checkstack/common";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Landing page for "create a new automation". Presents a "Blank automation"
|
|
32
|
+
* option plus a gallery of curated example templates (grouped by category)
|
|
33
|
+
* contributed by core + plugins. Picking a template opens the blank editor
|
|
34
|
+
* pre-seeded from it (`/automation/new/blank?template=<id>`); the operator
|
|
35
|
+
* still chooses a service account and saves.
|
|
36
|
+
*/
|
|
37
|
+
const AutomationTemplatePickerContent: React.FC = () => {
|
|
38
|
+
const client = usePluginClient(AutomationApi);
|
|
39
|
+
const accessApi = useApi(accessApiRef);
|
|
40
|
+
const navigate = useNavigate();
|
|
41
|
+
|
|
42
|
+
const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
|
|
43
|
+
automationAccess.manage,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const query = client.listAutomationTemplates.useQuery();
|
|
47
|
+
|
|
48
|
+
const startBlank = () =>
|
|
49
|
+
navigate(resolveRoute(automationRoutes.routes.createBlank));
|
|
50
|
+
|
|
51
|
+
const openTemplate = (template: AutomationTemplate) =>
|
|
52
|
+
navigate(
|
|
53
|
+
`${resolveRoute(automationRoutes.routes.createBlank)}?template=${encodeURIComponent(
|
|
54
|
+
template.id,
|
|
55
|
+
)}`,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Group templates by category, preserving first-seen category order.
|
|
59
|
+
const grouped = React.useMemo(() => {
|
|
60
|
+
const byCategory = new Map<string, AutomationTemplate[]>();
|
|
61
|
+
for (const template of query.data?.items ?? []) {
|
|
62
|
+
const list = byCategory.get(template.category) ?? [];
|
|
63
|
+
list.push(template);
|
|
64
|
+
byCategory.set(template.category, list);
|
|
65
|
+
}
|
|
66
|
+
return [...byCategory.entries()];
|
|
67
|
+
}, [query.data]);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<PageLayout
|
|
71
|
+
title="New automation"
|
|
72
|
+
subtitle="Start from a curated example or build your own from scratch"
|
|
73
|
+
icon={Workflow}
|
|
74
|
+
loading={accessLoading}
|
|
75
|
+
allowed={canManage}
|
|
76
|
+
>
|
|
77
|
+
<div className="space-y-6">
|
|
78
|
+
<Card>
|
|
79
|
+
<CardContent className="flex flex-col gap-3 p-4 sm:flex-row sm:items-center sm:justify-between">
|
|
80
|
+
<div className="flex items-center gap-3">
|
|
81
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-muted">
|
|
82
|
+
<FilePlus2 className="h-5 w-5 text-muted-foreground" />
|
|
83
|
+
</div>
|
|
84
|
+
<div>
|
|
85
|
+
<div className="font-medium">Blank automation</div>
|
|
86
|
+
<div className="text-sm text-muted-foreground">
|
|
87
|
+
Start with an empty editor and add triggers and actions yourself.
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<Button onClick={startBlank}>
|
|
92
|
+
Start blank
|
|
93
|
+
<ArrowRight className="ml-1 h-4 w-4" />
|
|
94
|
+
</Button>
|
|
95
|
+
</CardContent>
|
|
96
|
+
</Card>
|
|
97
|
+
|
|
98
|
+
{query.isLoading ? (
|
|
99
|
+
<div className="p-6">
|
|
100
|
+
<LoadingSpinner />
|
|
101
|
+
</div>
|
|
102
|
+
) : query.isError ? (
|
|
103
|
+
<QueryErrorState error={query.error} onRetry={() => query.refetch()} />
|
|
104
|
+
) : (
|
|
105
|
+
grouped.map(([category, items]) => (
|
|
106
|
+
<section key={category} className="space-y-3">
|
|
107
|
+
<h2 className="text-sm font-semibold text-muted-foreground">
|
|
108
|
+
{category}
|
|
109
|
+
</h2>
|
|
110
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
111
|
+
{items.map((template) => (
|
|
112
|
+
<Card
|
|
113
|
+
key={template.id}
|
|
114
|
+
className="flex h-full cursor-pointer flex-col transition-colors hover:border-primary"
|
|
115
|
+
onClick={() => openTemplate(template)}
|
|
116
|
+
>
|
|
117
|
+
<CardHeader className="flex-row items-start gap-3 space-y-0">
|
|
118
|
+
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
|
|
119
|
+
<DynamicIcon name={template.icon} className="h-5 w-5" />
|
|
120
|
+
</div>
|
|
121
|
+
<CardTitle className="text-base leading-snug">
|
|
122
|
+
{template.name}
|
|
123
|
+
</CardTitle>
|
|
124
|
+
</CardHeader>
|
|
125
|
+
<CardContent className="flex flex-1 flex-col justify-between gap-3">
|
|
126
|
+
<p className="text-sm text-muted-foreground">
|
|
127
|
+
{template.description}
|
|
128
|
+
</p>
|
|
129
|
+
<div className="flex items-center justify-between">
|
|
130
|
+
<Badge variant="secondary" className="font-normal">
|
|
131
|
+
{template.ownerPluginId}
|
|
132
|
+
</Badge>
|
|
133
|
+
<span className="flex items-center text-sm font-medium text-primary">
|
|
134
|
+
Use template
|
|
135
|
+
<ArrowRight className="ml-1 h-4 w-4" />
|
|
136
|
+
</span>
|
|
137
|
+
</div>
|
|
138
|
+
</CardContent>
|
|
139
|
+
</Card>
|
|
140
|
+
))}
|
|
141
|
+
</div>
|
|
142
|
+
</section>
|
|
143
|
+
))
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
</PageLayout>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const AutomationTemplatePickerPage = wrapInSuspense(
|
|
151
|
+
AutomationTemplatePickerContent,
|
|
152
|
+
);
|