@backstage/plugin-scaffolder 1.12.0-next.2 → 1.13.0-next.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/alpha/package.json +1 -1
  3. package/dist/alpha.d.ts +32 -21
  4. package/dist/alpha.esm.js +13 -21
  5. package/dist/alpha.esm.js.map +1 -1
  6. package/dist/esm/alpha/{ListTasksPage-2e8f4176.esm.js → ListTasksPage-a9fab591.esm.js} +2 -2
  7. package/dist/esm/alpha/{ListTasksPage-2e8f4176.esm.js.map → ListTasksPage-a9fab591.esm.js.map} +1 -1
  8. package/dist/esm/alpha/{Router-2826a2b8.esm.js → Router-ea3122d2.esm.js} +24 -9
  9. package/dist/esm/alpha/Router-ea3122d2.esm.js.map +1 -0
  10. package/dist/esm/alpha/{alpha-714dad1b.esm.js → alpha-0764fae7.esm.js} +243 -255
  11. package/dist/esm/alpha/alpha-0764fae7.esm.js.map +1 -0
  12. package/dist/esm/alpha/{index-2131f4a0.esm.js → index-d45f106a.esm.js} +79 -142
  13. package/dist/esm/alpha/index-d45f106a.esm.js.map +1 -0
  14. package/dist/esm/index/ListTasksPage-64779a2d.esm.js +1310 -0
  15. package/dist/esm/index/ListTasksPage-64779a2d.esm.js.map +1 -0
  16. package/dist/esm/index/{Router-6fd61bff.esm.js → Router-f32000f9.esm.js} +42 -33
  17. package/dist/esm/index/Router-f32000f9.esm.js.map +1 -0
  18. package/dist/esm/index/index-8321766a.esm.js +1741 -0
  19. package/dist/esm/index/index-8321766a.esm.js.map +1 -0
  20. package/dist/esm/index/index-e3edaa49.esm.js +991 -0
  21. package/dist/esm/index/index-e3edaa49.esm.js.map +1 -0
  22. package/dist/index.d.ts +25 -5
  23. package/dist/index.esm.js +9 -59
  24. package/dist/index.esm.js.map +1 -1
  25. package/package.json +23 -23
  26. package/dist/esm/alpha/Router-2826a2b8.esm.js.map +0 -1
  27. package/dist/esm/alpha/alpha-714dad1b.esm.js.map +0 -1
  28. package/dist/esm/alpha/index-2131f4a0.esm.js.map +0 -1
  29. package/dist/esm/index/ListTasksPage-fa403ee3.esm.js +0 -192
  30. package/dist/esm/index/ListTasksPage-fa403ee3.esm.js.map +0 -1
  31. package/dist/esm/index/Router-6fd61bff.esm.js.map +0 -1
  32. package/dist/esm/index/index-0b6cdf44.esm.js +0 -3475
  33. package/dist/esm/index/index-0b6cdf44.esm.js.map +0 -1
  34. package/dist/esm/index/index-f404fb0b.esm.js +0 -449
  35. package/dist/esm/index/index-f404fb0b.esm.js.map +0 -1
@@ -0,0 +1,991 @@
1
+ import React, { useCallback, useState, useMemo, useEffect, Component } from 'react';
2
+ import { Link, useNavigate, Navigate, useParams, useOutlet, Routes, Route } from 'react-router-dom';
3
+ import { useRouteRef, useApp, useApi, useRouteRefParams, AnalyticsContext, useApiHolder, alertApiRef } from '@backstage/core-plugin-api';
4
+ import { DocsIcon, Page, Header, Content, ContentHeader, SupportButton, ErrorPanel, ErrorPage } from '@backstage/core-components';
5
+ import { EntityListProvider, CatalogFilterLayout, EntitySearchBar, EntityKindPicker, UserListPicker, EntityTagPicker, catalogApiRef, humanizeEntityRef } from '@backstage/plugin-catalog-react';
6
+ import { ScaffolderPageContextMenu, TemplateCategoryPicker, TemplateGroups, Workflow, DefaultTemplateOutputs, TaskSteps, TaskLogStream, Stepper, Form } from '@backstage/plugin-scaffolder-react/alpha';
7
+ import Button from '@material-ui/core/Button';
8
+ import IconButton from '@material-ui/core/IconButton';
9
+ import useMediaQuery from '@material-ui/core/useMediaQuery';
10
+ import AddCircleOutline from '@material-ui/icons/AddCircleOutline';
11
+ import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
12
+ import { usePermission } from '@backstage/plugin-permission-react';
13
+ import { r as registerComponentRouteRef, e as editRouteRef, a as actionsRouteRef, b as scaffolderListTaskRouteRef, v as viewTechDocRouteRef, s as selectedTemplateRouteRef, d as rootRouteRef, c as scaffolderTaskRouteRef } from './index-8321766a.esm.js';
14
+ import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model';
15
+ import { useTemplateSecrets, scaffolderApiRef, useTaskEventStream, useCustomFieldExtensions, useCustomLayouts, SecretsContextProvider } from '@backstage/plugin-scaffolder-react';
16
+ import { u as useDryRun, a as useDirectoryEditor, D as DirectoryEditorProvider, b as DryRunProvider, T as TemplateEditorBrowser, c as TemplateEditorTextArea, d as DryRunResults, e as TemplateEditorIntro, W as WebFileSystemAccess, f as DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS, A as ActionsPage, L as ListTasksPage } from './ListTasksPage-64779a2d.esm.js';
17
+ import { makeStyles, IconButton as IconButton$1, Popover, MenuList, MenuItem, ListItemIcon, ListItemText, Box, Paper, FormControl, InputLabel, Select, Card, CardHeader, CardContent, Button as Button$1, LinearProgress } from '@material-ui/core';
18
+ import qs from 'qs';
19
+ import { useAsync } from '@react-hookz/web';
20
+ import Cancel from '@material-ui/icons/Cancel';
21
+ import Retry from '@material-ui/icons/Repeat';
22
+ import Toc from '@material-ui/icons/Toc';
23
+ import MoreVert from '@material-ui/icons/MoreVert';
24
+ import { StreamLanguage } from '@codemirror/language';
25
+ import { yaml as yaml$1 } from '@codemirror/legacy-modes/mode/yaml';
26
+ import CloseIcon from '@material-ui/icons/Close';
27
+ import CodeMirror from '@uiw/react-codemirror';
28
+ import yaml from 'yaml';
29
+ import { makeStyles as makeStyles$1 } from '@material-ui/core/styles';
30
+ import useDebounce from 'react-use/lib/useDebounce';
31
+ import validator from '@rjsf/validator-ajv8';
32
+ import useAsync$1 from 'react-use/lib/useAsync';
33
+ import '@backstage/errors';
34
+ import 'zen-observable';
35
+ import '@backstage/integration-react';
36
+ import '@backstage/catalog-client';
37
+ import '@material-ui/core/FormControl';
38
+ import '@material-ui/lab/Autocomplete';
39
+ import 'zod';
40
+ import 'zod-to-json-schema';
41
+ import '@material-ui/core/FormHelperText';
42
+ import '@material-ui/core/Input';
43
+ import '@material-ui/core/InputLabel';
44
+ import 'react-use/lib/useEffectOnce';
45
+ import '@material-ui/lab';
46
+ import 'lodash/capitalize';
47
+ import '@material-ui/icons/CheckBox';
48
+ import '@material-ui/icons/CheckBoxOutlineBlank';
49
+ import '@material-ui/icons/ExpandMore';
50
+ import '@material-ui/core/Grid';
51
+ import '@material-ui/core/Step';
52
+ import '@material-ui/core/StepLabel';
53
+ import '@material-ui/core/Stepper';
54
+ import '@material-ui/core/Typography';
55
+ import '@material-ui/icons/Check';
56
+ import '@material-ui/icons/FiberManualRecord';
57
+ import 'classnames';
58
+ import 'luxon';
59
+ import 'react-use/lib/useInterval';
60
+ import '@material-ui/icons/Language';
61
+ import '@material-ui/core/Card';
62
+ import '@material-ui/core/CardActionArea';
63
+ import '@material-ui/core/CardContent';
64
+ import '@material-ui/core/Tooltip';
65
+ import '@material-ui/icons/InfoOutlined';
66
+ import '@material-ui/core/Accordion';
67
+ import '@material-ui/core/AccordionDetails';
68
+ import '@material-ui/core/AccordionSummary';
69
+ import '@material-ui/core/Divider';
70
+ import '@material-ui/icons/ExpandLess';
71
+ import '@material-ui/core/List';
72
+ import '@material-ui/core/ListItem';
73
+ import '@material-ui/core/ListItemIcon';
74
+ import '@material-ui/core/ListItemSecondaryAction';
75
+ import '@material-ui/core/ListItemText';
76
+ import '@material-ui/icons/Delete';
77
+ import '@material-ui/core/Box';
78
+ import '@material-ui/core/Tab';
79
+ import '@material-ui/core/Tabs';
80
+ import '@material-ui/icons/Refresh';
81
+ import '@material-ui/icons/Save';
82
+ import '@material-ui/lab/TreeView';
83
+ import '@material-ui/icons/ChevronRight';
84
+ import '@material-ui/lab/TreeItem';
85
+ import '@codemirror/view';
86
+ import '@material-ui/icons/Settings';
87
+ import '@material-ui/icons/FontDownload';
88
+ import 'humanize-duration';
89
+
90
+ const RegisterExistingButton = (props) => {
91
+ const { title, to } = props;
92
+ const { allowed } = usePermission({
93
+ permission: catalogEntityCreatePermission
94
+ });
95
+ const isXSScreen = useMediaQuery(
96
+ (theme) => theme.breakpoints.down("xs")
97
+ );
98
+ if (!to || !allowed) {
99
+ return null;
100
+ }
101
+ return isXSScreen ? /* @__PURE__ */ React.createElement(
102
+ IconButton,
103
+ {
104
+ component: Link,
105
+ color: "primary",
106
+ title,
107
+ size: "small",
108
+ to
109
+ },
110
+ /* @__PURE__ */ React.createElement(AddCircleOutline, null)
111
+ ) : /* @__PURE__ */ React.createElement(Button, { component: Link, variant: "contained", color: "primary", to }, title);
112
+ };
113
+
114
+ const defaultGroup = {
115
+ title: "Templates",
116
+ filter: () => true
117
+ };
118
+ const createGroupsWithOther = (groups) => [
119
+ ...groups,
120
+ {
121
+ title: "Other Templates",
122
+ filter: (e) => ![...groups].some(({ filter }) => filter(e))
123
+ }
124
+ ];
125
+ const TemplateListPage = (props) => {
126
+ var _a, _b, _c;
127
+ const registerComponentLink = useRouteRef(registerComponentRouteRef);
128
+ const {
129
+ TemplateCardComponent,
130
+ groups: givenGroups = [],
131
+ templateFilter
132
+ } = props;
133
+ const navigate = useNavigate();
134
+ const editorLink = useRouteRef(editRouteRef);
135
+ const actionsLink = useRouteRef(actionsRouteRef);
136
+ const tasksLink = useRouteRef(scaffolderListTaskRouteRef);
137
+ const viewTechDocsLink = useRouteRef(viewTechDocRouteRef);
138
+ const templateRoute = useRouteRef(selectedTemplateRouteRef);
139
+ const app = useApp();
140
+ const groups = givenGroups.length ? createGroupsWithOther(givenGroups) : [defaultGroup];
141
+ const scaffolderPageContextMenuProps = {
142
+ onEditorClicked: ((_a = props == null ? void 0 : props.contextMenu) == null ? void 0 : _a.editor) !== false ? () => navigate(editorLink()) : void 0,
143
+ onActionsClicked: ((_b = props == null ? void 0 : props.contextMenu) == null ? void 0 : _b.actions) !== false ? () => navigate(actionsLink()) : void 0,
144
+ onTasksClicked: ((_c = props == null ? void 0 : props.contextMenu) == null ? void 0 : _c.tasks) !== false ? () => navigate(tasksLink()) : void 0
145
+ };
146
+ const additionalLinksForEntity = useCallback(
147
+ (template) => {
148
+ var _a2, _b2;
149
+ const { kind, namespace, name } = parseEntityRef(
150
+ stringifyEntityRef(template)
151
+ );
152
+ return ((_a2 = template.metadata.annotations) == null ? void 0 : _a2["backstage.io/techdocs-ref"]) && viewTechDocsLink ? [
153
+ {
154
+ icon: (_b2 = app.getSystemIcon("docs")) != null ? _b2 : DocsIcon,
155
+ text: "View TechDocs",
156
+ url: viewTechDocsLink({ kind, namespace, name })
157
+ }
158
+ ] : [];
159
+ },
160
+ [app, viewTechDocsLink]
161
+ );
162
+ const onTemplateSelected = useCallback(
163
+ (template) => {
164
+ const { namespace, name } = parseEntityRef(stringifyEntityRef(template));
165
+ navigate(templateRoute({ namespace, templateName: name }));
166
+ },
167
+ [navigate, templateRoute]
168
+ );
169
+ return /* @__PURE__ */ React.createElement(EntityListProvider, null, /* @__PURE__ */ React.createElement(Page, { themeId: "website" }, /* @__PURE__ */ React.createElement(
170
+ Header,
171
+ {
172
+ pageTitleOverride: "Create a new component",
173
+ title: "Create a new component",
174
+ subtitle: "Create new software components using standard templates in your organization"
175
+ },
176
+ /* @__PURE__ */ React.createElement(ScaffolderPageContextMenu, { ...scaffolderPageContextMenuProps })
177
+ ), /* @__PURE__ */ React.createElement(Content, null, /* @__PURE__ */ React.createElement(ContentHeader, { title: "Available Templates" }, /* @__PURE__ */ React.createElement(
178
+ RegisterExistingButton,
179
+ {
180
+ title: "Register Existing Component",
181
+ to: registerComponentLink && registerComponentLink()
182
+ }
183
+ ), /* @__PURE__ */ React.createElement(SupportButton, null, "Create new software components using standard templates. Different templates create different kinds of components (services, websites, documentation, ...).")), /* @__PURE__ */ React.createElement(CatalogFilterLayout, null, /* @__PURE__ */ React.createElement(CatalogFilterLayout.Filters, null, /* @__PURE__ */ React.createElement(EntitySearchBar, null), /* @__PURE__ */ React.createElement(EntityKindPicker, { initialFilter: "template", hidden: true }), /* @__PURE__ */ React.createElement(
184
+ UserListPicker,
185
+ {
186
+ initialFilter: "all",
187
+ availableFilters: ["all", "starred"]
188
+ }
189
+ ), /* @__PURE__ */ React.createElement(TemplateCategoryPicker, null), /* @__PURE__ */ React.createElement(EntityTagPicker, null)), /* @__PURE__ */ React.createElement(CatalogFilterLayout.Content, null, /* @__PURE__ */ React.createElement(
190
+ TemplateGroups,
191
+ {
192
+ groups,
193
+ templateFilter,
194
+ TemplateCardComponent,
195
+ onTemplateSelected,
196
+ additionalLinksForEntity
197
+ }
198
+ ))))));
199
+ };
200
+
201
+ const TemplateWizardPage = (props) => {
202
+ const rootRef = useRouteRef(rootRouteRef);
203
+ const taskRoute = useRouteRef(scaffolderTaskRouteRef);
204
+ const { secrets } = useTemplateSecrets();
205
+ const scaffolderApi = useApi(scaffolderApiRef);
206
+ const navigate = useNavigate();
207
+ const { templateName, namespace } = useRouteRefParams(
208
+ selectedTemplateRouteRef
209
+ );
210
+ const templateRef = stringifyEntityRef({
211
+ kind: "Template",
212
+ namespace,
213
+ name: templateName
214
+ });
215
+ const onCreate = async (values) => {
216
+ const { taskId } = await scaffolderApi.scaffold({
217
+ templateRef,
218
+ values,
219
+ secrets
220
+ });
221
+ navigate(taskRoute({ taskId }));
222
+ };
223
+ const onError = () => /* @__PURE__ */ React.createElement(Navigate, { to: rootRef() });
224
+ return /* @__PURE__ */ React.createElement(AnalyticsContext, { attributes: { entityRef: templateRef } }, /* @__PURE__ */ React.createElement(Page, { themeId: "website" }, /* @__PURE__ */ React.createElement(
225
+ Header,
226
+ {
227
+ pageTitleOverride: "Create a new component",
228
+ title: "Create a new component",
229
+ subtitle: "Create new software components using standard templates in your organization"
230
+ }
231
+ ), /* @__PURE__ */ React.createElement(
232
+ Workflow,
233
+ {
234
+ namespace,
235
+ templateName,
236
+ onCreate,
237
+ onError,
238
+ extensions: props.customFieldExtensions,
239
+ FormProps: props.FormProps,
240
+ layouts: props.layouts
241
+ }
242
+ )));
243
+ };
244
+
245
+ const useStyles$5 = makeStyles((theme) => ({
246
+ button: {
247
+ color: theme.palette.common.white
248
+ }
249
+ }));
250
+ const ContextMenu = (props) => {
251
+ const { cancelEnabled, logsVisible, onStartOver, onToggleLogs, taskId } = props;
252
+ const classes = useStyles$5();
253
+ const scaffolderApi = useApi(scaffolderApiRef);
254
+ const [anchorEl, setAnchorEl] = useState();
255
+ const [{ status: cancelStatus }, { execute: cancel }] = useAsync(async () => {
256
+ if (taskId) {
257
+ await scaffolderApi.cancelTask(taskId);
258
+ }
259
+ });
260
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
261
+ IconButton$1,
262
+ {
263
+ "aria-label": "more",
264
+ "aria-controls": "long-menu",
265
+ "aria-haspopup": "true",
266
+ onClick: (event) => {
267
+ setAnchorEl(event.currentTarget);
268
+ },
269
+ "data-testid": "menu-button",
270
+ color: "inherit",
271
+ className: classes.button
272
+ },
273
+ /* @__PURE__ */ React.createElement(MoreVert, null)
274
+ ), /* @__PURE__ */ React.createElement(
275
+ Popover,
276
+ {
277
+ open: Boolean(anchorEl),
278
+ onClose: () => setAnchorEl(void 0),
279
+ anchorEl,
280
+ anchorOrigin: { vertical: "bottom", horizontal: "right" },
281
+ transformOrigin: { vertical: "top", horizontal: "right" }
282
+ },
283
+ /* @__PURE__ */ React.createElement(MenuList, null, /* @__PURE__ */ React.createElement(MenuItem, { onClick: () => onToggleLogs == null ? void 0 : onToggleLogs(!logsVisible) }, /* @__PURE__ */ React.createElement(ListItemIcon, null, /* @__PURE__ */ React.createElement(Toc, { fontSize: "small" })), /* @__PURE__ */ React.createElement(ListItemText, { primary: logsVisible ? "Hide Logs" : "Show Logs" })), /* @__PURE__ */ React.createElement(MenuItem, { onClick: onStartOver }, /* @__PURE__ */ React.createElement(ListItemIcon, null, /* @__PURE__ */ React.createElement(Retry, { fontSize: "small" })), /* @__PURE__ */ React.createElement(ListItemText, { primary: "Start Over" })), /* @__PURE__ */ React.createElement(
284
+ MenuItem,
285
+ {
286
+ onClick: cancel,
287
+ disabled: !cancelEnabled || cancelStatus !== "not-executed",
288
+ "data-testid": "cancel-task"
289
+ },
290
+ /* @__PURE__ */ React.createElement(ListItemIcon, null, /* @__PURE__ */ React.createElement(Cancel, { fontSize: "small" })),
291
+ /* @__PURE__ */ React.createElement(ListItemText, { primary: "Cancel" })
292
+ ))
293
+ ));
294
+ };
295
+
296
+ const useStyles$4 = makeStyles({
297
+ contentWrapper: {
298
+ display: "flex",
299
+ flexDirection: "column"
300
+ }
301
+ });
302
+ const OngoingTask = (props) => {
303
+ var _a, _b, _c, _d, _e, _f, _g, _h;
304
+ const { taskId } = useParams();
305
+ const templateRouteRef = useRouteRef(selectedTemplateRouteRef);
306
+ const navigate = useNavigate();
307
+ const taskStream = useTaskEventStream(taskId);
308
+ const classes = useStyles$4();
309
+ const steps = useMemo(
310
+ () => {
311
+ var _a2, _b2;
312
+ return (_b2 = (_a2 = taskStream.task) == null ? void 0 : _a2.spec.steps.map((step) => {
313
+ var _a3;
314
+ return {
315
+ ...step,
316
+ ...(_a3 = taskStream == null ? void 0 : taskStream.steps) == null ? void 0 : _a3[step.id]
317
+ };
318
+ })) != null ? _b2 : [];
319
+ },
320
+ [taskStream]
321
+ );
322
+ const [logsVisible, setLogVisibleState] = useState(false);
323
+ useEffect(() => {
324
+ if (taskStream.error) {
325
+ setLogVisibleState(true);
326
+ }
327
+ }, [taskStream.error]);
328
+ const activeStep = useMemo(() => {
329
+ for (let i = steps.length - 1; i >= 0; i--) {
330
+ if (steps[i].status !== "open") {
331
+ return i;
332
+ }
333
+ }
334
+ return 0;
335
+ }, [steps]);
336
+ const startOver = useCallback(() => {
337
+ var _a2, _b2, _c2, _d2, _e2, _f2;
338
+ const { namespace, name } = (_d2 = (_c2 = (_b2 = (_a2 = taskStream.task) == null ? void 0 : _a2.spec.templateInfo) == null ? void 0 : _b2.entity) == null ? void 0 : _c2.metadata) != null ? _d2 : {};
339
+ const formData = (_f2 = (_e2 = taskStream.task) == null ? void 0 : _e2.spec.parameters) != null ? _f2 : {};
340
+ if (!namespace || !name) {
341
+ return;
342
+ }
343
+ navigate({
344
+ pathname: templateRouteRef({
345
+ namespace,
346
+ templateName: name
347
+ }),
348
+ search: `?${qs.stringify({ formData: JSON.stringify(formData) })}`
349
+ });
350
+ }, [
351
+ navigate,
352
+ (_a = taskStream.task) == null ? void 0 : _a.spec.parameters,
353
+ (_d = (_c = (_b = taskStream.task) == null ? void 0 : _b.spec.templateInfo) == null ? void 0 : _c.entity) == null ? void 0 : _d.metadata,
354
+ templateRouteRef
355
+ ]);
356
+ const Outputs = (_e = props.TemplateOutputsComponent) != null ? _e : DefaultTemplateOutputs;
357
+ const templateName = (_h = (_g = (_f = taskStream.task) == null ? void 0 : _f.spec.templateInfo) == null ? void 0 : _g.entity) == null ? void 0 : _h.metadata.name;
358
+ const cancelEnabled = !(taskStream.cancelled || taskStream.completed);
359
+ return /* @__PURE__ */ React.createElement(Page, { themeId: "website" }, /* @__PURE__ */ React.createElement(
360
+ Header,
361
+ {
362
+ pageTitleOverride: `Run of ${templateName}`,
363
+ title: /* @__PURE__ */ React.createElement("div", null, "Run of ", /* @__PURE__ */ React.createElement("code", null, templateName)),
364
+ subtitle: `Task ${taskId}`
365
+ },
366
+ /* @__PURE__ */ React.createElement(
367
+ ContextMenu,
368
+ {
369
+ cancelEnabled,
370
+ logsVisible,
371
+ onStartOver: startOver,
372
+ onToggleLogs: setLogVisibleState,
373
+ taskId
374
+ }
375
+ )
376
+ ), /* @__PURE__ */ React.createElement(Content, { className: classes.contentWrapper }, taskStream.error ? /* @__PURE__ */ React.createElement(Box, { paddingBottom: 2 }, /* @__PURE__ */ React.createElement(
377
+ ErrorPanel,
378
+ {
379
+ error: taskStream.error,
380
+ title: taskStream.error.message
381
+ }
382
+ )) : null, /* @__PURE__ */ React.createElement(Box, { paddingBottom: 2 }, /* @__PURE__ */ React.createElement(
383
+ TaskSteps,
384
+ {
385
+ steps,
386
+ activeStep,
387
+ isComplete: taskStream.completed,
388
+ isError: Boolean(taskStream.error)
389
+ }
390
+ )), /* @__PURE__ */ React.createElement(Outputs, { output: taskStream.output }), logsVisible ? /* @__PURE__ */ React.createElement(Box, { paddingBottom: 2, height: "100%" }, /* @__PURE__ */ React.createElement(Paper, { style: { height: "100%" } }, /* @__PURE__ */ React.createElement(Box, { padding: 2, height: "100%" }, /* @__PURE__ */ React.createElement(TaskLogStream, { logs: taskStream.stepLogs })))) : null));
391
+ };
392
+
393
+ const useStyles$3 = makeStyles$1({
394
+ containerWrapper: {
395
+ position: "relative",
396
+ width: "100%",
397
+ height: "100%"
398
+ },
399
+ container: {
400
+ position: "absolute",
401
+ top: 0,
402
+ bottom: 0,
403
+ left: 0,
404
+ right: 0,
405
+ overflow: "auto"
406
+ }
407
+ });
408
+ class ErrorBoundary extends Component {
409
+ constructor() {
410
+ super(...arguments);
411
+ this.state = {
412
+ shouldRender: true
413
+ };
414
+ }
415
+ componentDidUpdate(prevProps) {
416
+ if (prevProps.invalidator !== this.props.invalidator) {
417
+ this.setState({ shouldRender: true });
418
+ }
419
+ }
420
+ componentDidCatch(error) {
421
+ this.props.setErrorText(error.message);
422
+ this.setState({ shouldRender: false });
423
+ }
424
+ render() {
425
+ return this.state.shouldRender ? this.props.children : null;
426
+ }
427
+ }
428
+ function isJsonObject(value) {
429
+ return typeof value === "object" && value !== null && !Array.isArray(value);
430
+ }
431
+ function TemplateEditorForm(props) {
432
+ const {
433
+ content,
434
+ contentIsSpec,
435
+ onDryRun,
436
+ setErrorText,
437
+ fieldExtensions = [],
438
+ layouts = []
439
+ } = props;
440
+ const classes = useStyles$3();
441
+ const apiHolder = useApiHolder();
442
+ const [steps, setSteps] = useState();
443
+ useDebounce(
444
+ () => {
445
+ try {
446
+ if (!content) {
447
+ setSteps(void 0);
448
+ return;
449
+ }
450
+ const parsed = yaml.parse(content);
451
+ if (!isJsonObject(parsed)) {
452
+ setSteps(void 0);
453
+ return;
454
+ }
455
+ let rootObj = parsed;
456
+ if (!contentIsSpec) {
457
+ const isTemplate = String(parsed.kind).toLocaleLowerCase("en-US") === "template";
458
+ if (!isTemplate) {
459
+ setSteps(void 0);
460
+ return;
461
+ }
462
+ rootObj = isJsonObject(parsed.spec) ? parsed.spec : {};
463
+ }
464
+ const { parameters } = rootObj;
465
+ if (!Array.isArray(parameters)) {
466
+ setErrorText("Template parameters must be an array");
467
+ setSteps(void 0);
468
+ return;
469
+ }
470
+ setErrorText();
471
+ setSteps(
472
+ parameters.flatMap(
473
+ (param) => isJsonObject(param) ? [
474
+ {
475
+ title: String(param.title),
476
+ schema: param
477
+ }
478
+ ] : []
479
+ )
480
+ );
481
+ } catch (e) {
482
+ setErrorText(e.message);
483
+ }
484
+ },
485
+ 250,
486
+ [contentIsSpec, content, apiHolder]
487
+ );
488
+ if (!steps) {
489
+ return null;
490
+ }
491
+ return /* @__PURE__ */ React.createElement("div", { className: classes.containerWrapper }, /* @__PURE__ */ React.createElement("div", { className: classes.container }, /* @__PURE__ */ React.createElement(ErrorBoundary, { invalidator: steps, setErrorText }, /* @__PURE__ */ React.createElement(
492
+ Stepper,
493
+ {
494
+ manifest: { steps, title: "Template Editor" },
495
+ extensions: fieldExtensions,
496
+ onCreate: async (data) => {
497
+ await (onDryRun == null ? void 0 : onDryRun(data));
498
+ },
499
+ layouts,
500
+ components: { createButtonText: onDryRun && "Try It" }
501
+ }
502
+ ))));
503
+ }
504
+ function TemplateEditorFormDirectoryEditorDryRun(props) {
505
+ const { setErrorText, fieldExtensions = [], layouts } = props;
506
+ const dryRun = useDryRun();
507
+ const directoryEditor = useDirectoryEditor();
508
+ const { selectedFile } = directoryEditor;
509
+ const handleDryRun = async (values) => {
510
+ if (!selectedFile) {
511
+ return;
512
+ }
513
+ try {
514
+ await dryRun.execute({
515
+ templateContent: selectedFile.content,
516
+ values,
517
+ files: directoryEditor.files
518
+ });
519
+ setErrorText();
520
+ } catch (e) {
521
+ setErrorText(String(e.cause || e));
522
+ throw e;
523
+ }
524
+ };
525
+ const content = selectedFile && selectedFile.path.match(/\.ya?ml$/) ? selectedFile.content : void 0;
526
+ return /* @__PURE__ */ React.createElement(
527
+ TemplateEditorForm,
528
+ {
529
+ onDryRun: handleDryRun,
530
+ fieldExtensions,
531
+ setErrorText,
532
+ content,
533
+ layouts
534
+ }
535
+ );
536
+ }
537
+ TemplateEditorForm.DirectoryEditorDryRun = TemplateEditorFormDirectoryEditorDryRun;
538
+
539
+ const useStyles$2 = makeStyles((theme) => ({
540
+ root: {
541
+ gridArea: "pageContent",
542
+ display: "grid",
543
+ gridTemplateAreas: `
544
+ "controls controls"
545
+ "fieldForm preview"
546
+ `,
547
+ gridTemplateRows: "auto 1fr",
548
+ gridTemplateColumns: "1fr 1fr"
549
+ },
550
+ controls: {
551
+ gridArea: "controls",
552
+ display: "flex",
553
+ flexFlow: "row nowrap",
554
+ alignItems: "center",
555
+ margin: theme.spacing(1)
556
+ },
557
+ fieldForm: {
558
+ gridArea: "fieldForm"
559
+ },
560
+ preview: {
561
+ gridArea: "preview"
562
+ }
563
+ }));
564
+ const CustomFieldExplorer = ({
565
+ customFieldExtensions = [],
566
+ onClose
567
+ }) => {
568
+ var _a, _b;
569
+ const classes = useStyles$2();
570
+ const fieldOptions = customFieldExtensions.filter((field) => !!field.schema);
571
+ const [selectedField, setSelectedField] = useState(fieldOptions[0]);
572
+ const [fieldFormState, setFieldFormState] = useState({});
573
+ const [refreshKey, setRefreshKey] = useState(Date.now());
574
+ const sampleFieldTemplate = useMemo(
575
+ () => {
576
+ var _a2, _b2;
577
+ return yaml.stringify({
578
+ parameters: [
579
+ {
580
+ title: `${selectedField.name} Example`,
581
+ properties: {
582
+ [selectedField.name]: {
583
+ type: (_b2 = (_a2 = selectedField.schema) == null ? void 0 : _a2.returnValue) == null ? void 0 : _b2.type,
584
+ "ui:field": selectedField.name,
585
+ "ui:options": fieldFormState
586
+ }
587
+ }
588
+ }
589
+ ]
590
+ });
591
+ },
592
+ [fieldFormState, selectedField]
593
+ );
594
+ const fieldComponents = useMemo(() => {
595
+ return Object.fromEntries(
596
+ customFieldExtensions.map(({ name, component }) => [name, component])
597
+ );
598
+ }, [customFieldExtensions]);
599
+ const handleSelectionChange = useCallback(
600
+ (selection) => {
601
+ setSelectedField(selection);
602
+ setFieldFormState({});
603
+ },
604
+ [setFieldFormState, setSelectedField]
605
+ );
606
+ const handleFieldConfigChange = useCallback(
607
+ (state) => {
608
+ setFieldFormState(state);
609
+ setRefreshKey(Date.now());
610
+ },
611
+ [setFieldFormState, setRefreshKey]
612
+ );
613
+ return /* @__PURE__ */ React.createElement("main", { className: classes.root }, /* @__PURE__ */ React.createElement("div", { className: classes.controls }, /* @__PURE__ */ React.createElement(FormControl, { variant: "outlined", size: "small", fullWidth: true }, /* @__PURE__ */ React.createElement(InputLabel, { id: "select-field-label" }, "Choose Custom Field Extension"), /* @__PURE__ */ React.createElement(
614
+ Select,
615
+ {
616
+ value: selectedField,
617
+ label: "Choose Custom Field Extension",
618
+ labelId: "select-field-label",
619
+ onChange: (e) => handleSelectionChange(e.target.value)
620
+ },
621
+ fieldOptions.map((option, idx) => /* @__PURE__ */ React.createElement(MenuItem, { key: idx, value: option }, option.name))
622
+ )), /* @__PURE__ */ React.createElement(IconButton$1, { size: "medium", onClick: onClose }, /* @__PURE__ */ React.createElement(CloseIcon, null))), /* @__PURE__ */ React.createElement("div", { className: classes.fieldForm }, /* @__PURE__ */ React.createElement(Card, null, /* @__PURE__ */ React.createElement(CardHeader, { title: "Field Options" }), /* @__PURE__ */ React.createElement(CardContent, null, /* @__PURE__ */ React.createElement(
623
+ Form,
624
+ {
625
+ showErrorList: false,
626
+ fields: { ...fieldComponents },
627
+ noHtml5Validate: true,
628
+ formData: fieldFormState,
629
+ formContext: { fieldFormState },
630
+ onSubmit: (e) => handleFieldConfigChange(e.formData),
631
+ validator,
632
+ schema: ((_a = selectedField.schema) == null ? void 0 : _a.uiOptions) || {}
633
+ },
634
+ /* @__PURE__ */ React.createElement(
635
+ Button$1,
636
+ {
637
+ variant: "contained",
638
+ color: "primary",
639
+ type: "submit",
640
+ disabled: !((_b = selectedField.schema) == null ? void 0 : _b.uiOptions)
641
+ },
642
+ "Apply"
643
+ )
644
+ )))), /* @__PURE__ */ React.createElement("div", { className: classes.preview }, /* @__PURE__ */ React.createElement(Card, null, /* @__PURE__ */ React.createElement(CardHeader, { title: "Example Template Spec" }), /* @__PURE__ */ React.createElement(CardContent, null, /* @__PURE__ */ React.createElement(
645
+ CodeMirror,
646
+ {
647
+ readOnly: true,
648
+ theme: "dark",
649
+ height: "100%",
650
+ extensions: [StreamLanguage.define(yaml$1)],
651
+ value: sampleFieldTemplate
652
+ }
653
+ ))), /* @__PURE__ */ React.createElement(
654
+ TemplateEditorForm,
655
+ {
656
+ key: refreshKey,
657
+ content: sampleFieldTemplate,
658
+ contentIsSpec: true,
659
+ fieldExtensions: customFieldExtensions,
660
+ setErrorText: () => null
661
+ }
662
+ )));
663
+ };
664
+
665
+ const useStyles$1 = makeStyles({
666
+ // Reset and fix sizing to make sure scrolling behaves correctly
667
+ root: {
668
+ gridArea: "pageContent",
669
+ display: "grid",
670
+ gridTemplateAreas: `
671
+ "browser editor preview"
672
+ "results results results"
673
+ `,
674
+ gridTemplateColumns: "1fr 3fr 2fr",
675
+ gridTemplateRows: "1fr auto"
676
+ },
677
+ browser: {
678
+ gridArea: "browser",
679
+ overflow: "auto"
680
+ },
681
+ editor: {
682
+ gridArea: "editor",
683
+ overflow: "auto"
684
+ },
685
+ preview: {
686
+ gridArea: "preview",
687
+ overflow: "auto"
688
+ },
689
+ results: {
690
+ gridArea: "results"
691
+ }
692
+ });
693
+ const TemplateEditor = (props) => {
694
+ const classes = useStyles$1();
695
+ const [errorText, setErrorText] = useState();
696
+ return /* @__PURE__ */ React.createElement(DirectoryEditorProvider, { directory: props.directory }, /* @__PURE__ */ React.createElement(DryRunProvider, null, /* @__PURE__ */ React.createElement("main", { className: classes.root }, /* @__PURE__ */ React.createElement("section", { className: classes.browser }, /* @__PURE__ */ React.createElement(TemplateEditorBrowser, { onClose: props.onClose })), /* @__PURE__ */ React.createElement("section", { className: classes.editor }, /* @__PURE__ */ React.createElement(TemplateEditorTextArea.DirectoryEditor, { errorText })), /* @__PURE__ */ React.createElement("section", { className: classes.preview }, /* @__PURE__ */ React.createElement(
697
+ TemplateEditorForm.DirectoryEditorDryRun,
698
+ {
699
+ setErrorText,
700
+ fieldExtensions: props.fieldExtensions,
701
+ layouts: props.layouts
702
+ }
703
+ )), /* @__PURE__ */ React.createElement("section", { className: classes.results }, /* @__PURE__ */ React.createElement(DryRunResults, null)))));
704
+ };
705
+
706
+ const EXAMPLE_TEMPLATE_PARAMS_YAML = `# Edit the template parameters below to see how they will render in the scaffolder form UI
707
+ parameters:
708
+ - title: Fill in some steps
709
+ required:
710
+ - name
711
+ properties:
712
+ name:
713
+ title: Name
714
+ type: string
715
+ description: Unique name of the component
716
+ owner:
717
+ title: Owner
718
+ type: string
719
+ description: Owner of the component
720
+ ui:field: OwnerPicker
721
+ ui:options:
722
+ catalogFilter:
723
+ kind: Group
724
+ - title: Choose a location
725
+ required:
726
+ - repoUrl
727
+ properties:
728
+ repoUrl:
729
+ title: Repository Location
730
+ type: string
731
+ ui:field: RepoUrlPicker
732
+ ui:options:
733
+ allowedHosts:
734
+ - github.com
735
+ steps:
736
+ - id: fetch-base
737
+ name: Fetch Base
738
+ action: fetch:template
739
+ input:
740
+ url: ./template
741
+ values:
742
+ name: \${{parameters.name}}
743
+ `;
744
+ const useStyles = makeStyles((theme) => ({
745
+ root: {
746
+ gridArea: "pageContent",
747
+ display: "grid",
748
+ gridTemplateAreas: `
749
+ "controls controls"
750
+ "textArea preview"
751
+ `,
752
+ gridTemplateRows: "auto 1fr",
753
+ gridTemplateColumns: "1fr 1fr"
754
+ },
755
+ controls: {
756
+ gridArea: "controls",
757
+ display: "flex",
758
+ flexFlow: "row nowrap",
759
+ alignItems: "center",
760
+ margin: theme.spacing(1)
761
+ },
762
+ textArea: {
763
+ gridArea: "textArea"
764
+ },
765
+ preview: {
766
+ gridArea: "preview"
767
+ }
768
+ }));
769
+ const TemplateFormPreviewer = ({
770
+ defaultPreviewTemplate = EXAMPLE_TEMPLATE_PARAMS_YAML,
771
+ customFieldExtensions = [],
772
+ onClose,
773
+ layouts = []
774
+ }) => {
775
+ const classes = useStyles();
776
+ const alertApi = useApi(alertApiRef);
777
+ const catalogApi = useApi(catalogApiRef);
778
+ const [selectedTemplate, setSelectedTemplate] = useState("");
779
+ const [errorText, setErrorText] = useState();
780
+ const [templateOptions, setTemplateOptions] = useState([]);
781
+ const [templateYaml, setTemplateYaml] = useState(defaultPreviewTemplate);
782
+ const { loading } = useAsync$1(
783
+ () => catalogApi.getEntities({
784
+ filter: { kind: "template" },
785
+ fields: [
786
+ "kind",
787
+ "metadata.namespace",
788
+ "metadata.name",
789
+ "metadata.title",
790
+ "spec.parameters",
791
+ "spec.steps",
792
+ "spec.output"
793
+ ]
794
+ }).then(
795
+ ({ items }) => setTemplateOptions(
796
+ items.map((template) => {
797
+ var _a;
798
+ return {
799
+ label: (_a = template.metadata.title) != null ? _a : humanizeEntityRef(template, { defaultKind: "template" }),
800
+ value: template
801
+ };
802
+ })
803
+ )
804
+ ).catch(
805
+ (e) => alertApi.post({
806
+ message: `Error loading exisiting templates: ${e.message}`,
807
+ severity: "error"
808
+ })
809
+ ),
810
+ [catalogApi]
811
+ );
812
+ const handleSelectChange = useCallback(
813
+ (selected) => {
814
+ setSelectedTemplate(selected);
815
+ setTemplateYaml(yaml.stringify(selected.spec));
816
+ },
817
+ [setTemplateYaml]
818
+ );
819
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, loading && /* @__PURE__ */ React.createElement(LinearProgress, null), /* @__PURE__ */ React.createElement("main", { className: classes.root }, /* @__PURE__ */ React.createElement("div", { className: classes.controls }, /* @__PURE__ */ React.createElement(FormControl, { variant: "outlined", size: "small", fullWidth: true }, /* @__PURE__ */ React.createElement(InputLabel, { id: "select-template-label" }, "Load Existing Template"), /* @__PURE__ */ React.createElement(
820
+ Select,
821
+ {
822
+ value: selectedTemplate,
823
+ label: "Load Existing Template",
824
+ labelId: "select-template-label",
825
+ onChange: (e) => handleSelectChange(e.target.value)
826
+ },
827
+ templateOptions.map((option, idx) => /* @__PURE__ */ React.createElement(MenuItem, { key: idx, value: option.value }, option.label))
828
+ )), /* @__PURE__ */ React.createElement(IconButton$1, { size: "medium", onClick: onClose }, /* @__PURE__ */ React.createElement(CloseIcon, null))), /* @__PURE__ */ React.createElement("div", { className: classes.textArea }, /* @__PURE__ */ React.createElement(
829
+ TemplateEditorTextArea,
830
+ {
831
+ content: templateYaml,
832
+ onUpdate: setTemplateYaml,
833
+ errorText
834
+ }
835
+ )), /* @__PURE__ */ React.createElement("div", { className: classes.preview }, /* @__PURE__ */ React.createElement(
836
+ TemplateEditorForm,
837
+ {
838
+ content: templateYaml,
839
+ contentIsSpec: true,
840
+ fieldExtensions: customFieldExtensions,
841
+ setErrorText,
842
+ layouts
843
+ }
844
+ ))));
845
+ };
846
+
847
+ function TemplateEditorPage(props) {
848
+ const [selection, setSelection] = useState();
849
+ let content = null;
850
+ if ((selection == null ? void 0 : selection.type) === "local") {
851
+ content = /* @__PURE__ */ React.createElement(
852
+ TemplateEditor,
853
+ {
854
+ directory: selection.directory,
855
+ fieldExtensions: props.customFieldExtensions,
856
+ onClose: () => setSelection(void 0),
857
+ layouts: props.layouts
858
+ }
859
+ );
860
+ } else if ((selection == null ? void 0 : selection.type) === "form") {
861
+ content = /* @__PURE__ */ React.createElement(
862
+ TemplateFormPreviewer,
863
+ {
864
+ defaultPreviewTemplate: props.defaultPreviewTemplate,
865
+ customFieldExtensions: props.customFieldExtensions,
866
+ onClose: () => setSelection(void 0),
867
+ layouts: props.layouts
868
+ }
869
+ );
870
+ } else if ((selection == null ? void 0 : selection.type) === "field-explorer") {
871
+ content = /* @__PURE__ */ React.createElement(
872
+ CustomFieldExplorer,
873
+ {
874
+ customFieldExtensions: props.customFieldExtensions,
875
+ onClose: () => setSelection(void 0)
876
+ }
877
+ );
878
+ } else {
879
+ content = /* @__PURE__ */ React.createElement(Content, null, /* @__PURE__ */ React.createElement(
880
+ TemplateEditorIntro,
881
+ {
882
+ onSelect: (option) => {
883
+ if (option === "local") {
884
+ WebFileSystemAccess.requestDirectoryAccess().then((directory) => setSelection({ type: "local", directory })).catch(() => {
885
+ });
886
+ } else if (option === "form") {
887
+ setSelection({ type: "form" });
888
+ } else if (option === "field-explorer") {
889
+ setSelection({ type: "field-explorer" });
890
+ }
891
+ }
892
+ }
893
+ ));
894
+ }
895
+ return /* @__PURE__ */ React.createElement(Page, { themeId: "home" }, /* @__PURE__ */ React.createElement(
896
+ Header,
897
+ {
898
+ title: "Template Editor",
899
+ subtitle: "Edit, preview, and try out templates and template forms"
900
+ }
901
+ ), content);
902
+ }
903
+
904
+ const Router = (props) => {
905
+ const {
906
+ components: {
907
+ TemplateCardComponent,
908
+ TemplateOutputsComponent,
909
+ TaskPageComponent = OngoingTask,
910
+ TemplateListPageComponent = TemplateListPage,
911
+ TemplateWizardPageComponent = TemplateWizardPage
912
+ } = {}
913
+ } = props;
914
+ const outlet = useOutlet() || props.children;
915
+ const customFieldExtensions = useCustomFieldExtensions(outlet);
916
+ const fieldExtensions = [
917
+ ...customFieldExtensions,
918
+ ...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter(
919
+ ({ name }) => !customFieldExtensions.some(
920
+ (customFieldExtension) => customFieldExtension.name === name
921
+ )
922
+ )
923
+ ];
924
+ const customLayouts = useCustomLayouts(outlet);
925
+ return /* @__PURE__ */ React.createElement(Routes, null, /* @__PURE__ */ React.createElement(
926
+ Route,
927
+ {
928
+ path: "/",
929
+ element: /* @__PURE__ */ React.createElement(
930
+ TemplateListPageComponent,
931
+ {
932
+ TemplateCardComponent,
933
+ contextMenu: props.contextMenu,
934
+ groups: props.groups,
935
+ templateFilter: props.templateFilter
936
+ }
937
+ )
938
+ }
939
+ ), /* @__PURE__ */ React.createElement(
940
+ Route,
941
+ {
942
+ path: selectedTemplateRouteRef.path,
943
+ element: /* @__PURE__ */ React.createElement(SecretsContextProvider, null, /* @__PURE__ */ React.createElement(
944
+ TemplateWizardPageComponent,
945
+ {
946
+ customFieldExtensions: fieldExtensions,
947
+ layouts: customLayouts,
948
+ FormProps: props.FormProps
949
+ }
950
+ ))
951
+ }
952
+ ), /* @__PURE__ */ React.createElement(
953
+ Route,
954
+ {
955
+ path: scaffolderTaskRouteRef.path,
956
+ element: /* @__PURE__ */ React.createElement(
957
+ TaskPageComponent,
958
+ {
959
+ TemplateOutputsComponent
960
+ }
961
+ )
962
+ }
963
+ ), /* @__PURE__ */ React.createElement(
964
+ Route,
965
+ {
966
+ path: editRouteRef.path,
967
+ element: /* @__PURE__ */ React.createElement(SecretsContextProvider, null, /* @__PURE__ */ React.createElement(
968
+ TemplateEditorPage,
969
+ {
970
+ customFieldExtensions: fieldExtensions,
971
+ layouts: customLayouts
972
+ }
973
+ ))
974
+ }
975
+ ), /* @__PURE__ */ React.createElement(Route, { path: actionsRouteRef.path, element: /* @__PURE__ */ React.createElement(ActionsPage, null) }), /* @__PURE__ */ React.createElement(
976
+ Route,
977
+ {
978
+ path: scaffolderListTaskRouteRef.path,
979
+ element: /* @__PURE__ */ React.createElement(ListTasksPage, null)
980
+ }
981
+ ), /* @__PURE__ */ React.createElement(
982
+ Route,
983
+ {
984
+ path: "*",
985
+ element: /* @__PURE__ */ React.createElement(ErrorPage, { status: "404", statusMessage: "Page not found" })
986
+ }
987
+ ));
988
+ };
989
+
990
+ export { Router };
991
+ //# sourceMappingURL=index-e3edaa49.esm.js.map