@backstage/plugin-scaffolder 0.15.0 → 1.0.1-next.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 CHANGED
@@ -1,5 +1,64 @@
1
1
  # @backstage/plugin-scaffolder
2
2
 
3
+ ## 1.0.1-next.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 4431873583: Update `usePermission` usage.
8
+ - Updated dependencies
9
+ - @backstage/integration@1.1.0-next.1
10
+ - @backstage/plugin-permission-react@0.4.0-next.0
11
+ - @backstage/plugin-catalog-react@1.0.1-next.1
12
+ - @backstage/plugin-catalog-common@1.0.1-next.1
13
+ - @backstage/integration-react@1.0.1-next.1
14
+
15
+ ## 1.0.1-next.0
16
+
17
+ ### Patch Changes
18
+
19
+ - d34900af81: Added a new `NextScaffolderRouter` which will eventually replace the exiting router
20
+ - Updated dependencies
21
+ - @backstage/catalog-model@1.0.1-next.0
22
+ - @backstage/integration@1.0.1-next.0
23
+ - @backstage/plugin-catalog-react@1.0.1-next.0
24
+ - @backstage/core-components@0.9.3-next.0
25
+ - @backstage/catalog-client@1.0.1-next.0
26
+ - @backstage/plugin-scaffolder-common@1.0.1-next.0
27
+ - @backstage/integration-react@1.0.1-next.0
28
+ - @backstage/plugin-catalog-common@1.0.1-next.0
29
+
30
+ ## 1.0.0
31
+
32
+ ### Major Changes
33
+
34
+ - b58c70c223: This package has been promoted to v1.0! To understand how this change affects the package, please check out our [versioning policy](https://backstage.io/docs/overview/versioning-policy).
35
+
36
+ ### Minor Changes
37
+
38
+ - 9a408928a1: **BREAKING**: Removed the unused `titleComponent` property of `groups` passed to the `ScaffolderPage`. The property was already ignored, but existing usage should migrated to use the `title` property instead, which now accepts any `ReactNode`.
39
+
40
+ ### Patch Changes
41
+
42
+ - 9b7e361783: Remove beta labels
43
+ - a422d7ce5e: chore(deps): bump `@testing-library/react` from 11.2.6 to 12.1.3
44
+ - 20a262c214: The `ScaffolderPage` now uses the `CatalogFilterLayout`, which means the filters are put in a drawer on smaller screens.
45
+ - f24ef7864e: Minor typo fixes
46
+ - d8716924d6: Implement a template preview page (`/create/preview`) to test creating form UIs
47
+ - Updated dependencies
48
+ - @backstage/core-components@0.9.2
49
+ - @backstage/core-plugin-api@1.0.0
50
+ - @backstage/integration-react@1.0.0
51
+ - @backstage/plugin-catalog-react@1.0.0
52
+ - @backstage/plugin-permission-react@0.3.4
53
+ - @backstage/catalog-model@1.0.0
54
+ - @backstage/plugin-scaffolder-common@1.0.0
55
+ - @backstage/integration@1.0.0
56
+ - @backstage/catalog-client@1.0.0
57
+ - @backstage/config@1.0.0
58
+ - @backstage/errors@1.0.0
59
+ - @backstage/types@1.0.0
60
+ - @backstage/plugin-catalog-common@1.0.0
61
+
3
62
  ## 0.15.0
4
63
 
5
64
  ### Minor Changes
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@backstage/plugin-scaffolder",
3
+ "version": "1.0.1-next.1",
4
+ "main": "../dist/index.esm.js",
5
+ "types": "../dist/index.alpha.d.ts"
6
+ }
@@ -1,11 +1,11 @@
1
1
  import React, { useState, useContext, useCallback } from 'react';
2
2
  import { useNavigate, Navigate, useOutlet, Routes, Route } from 'react-router';
3
- import { ItemCardHeader, MarkdownContent, Button, ContentHeader, Progress, WarningPanel, Link as Link$1, Content, ItemCardGrid, Page, Header, Lifecycle, CreateButton, SupportButton, StructuredMetadataTable, InfoCard, ErrorPage } from '@backstage/core-components';
4
- import { useRouteRef, useApi, errorApiRef, featureFlagsApiRef, useApiHolder, useElementFilter } from '@backstage/core-plugin-api';
5
- import { getEntityRelations, getEntitySourceLocation, FavoriteEntity, EntityRefLinks, useEntityList, EntityListProvider, EntitySearchBar, EntityKindPicker, UserListPicker, EntityTagPicker } from '@backstage/plugin-catalog-react';
6
- import { makeStyles, useTheme, Card, CardMedia, CardContent, Box, Typography, Chip, CardActions, IconButton, Tooltip, Link, Stepper, Step, StepLabel, StepContent, Button as Button$1, Paper, LinearProgress, TableContainer, Table, TableHead, TableRow, TableCell, TableBody } from '@material-ui/core';
7
- import { E as EntityPicker, a as EntityNamePicker, e as entityNamePickerValidation, b as EntityTagsPicker, R as RepoUrlPicker, r as repoPickerValidation, O as OwnerPicker, c as OwnedEntityPicker, s as selectedTemplateRouteRef, d as registerComponentRouteRef, T as TemplateTypePicker, S as SecretsContext, f as scaffolderApiRef, g as scaffolderTaskRouteRef, h as rootRouteRef, F as FIELD_EXTENSION_WRAPPER_KEY, i as FIELD_EXTENSION_KEY, j as SecretsContextProvider, k as TaskPage } from './index-ced3b204.esm.js';
3
+ import { ItemCardHeader, MarkdownContent, Button, ContentHeader, Progress, WarningPanel, Link as Link$1, Content, ItemCardGrid, Page, Header, CreateButton, SupportButton, StructuredMetadataTable, InfoCard, ErrorPage } from '@backstage/core-components';
4
+ import { useRouteRef, useApi, errorApiRef, featureFlagsApiRef, useApiHolder, alertApiRef, useElementFilter } from '@backstage/core-plugin-api';
5
+ import { getEntityRelations, getEntitySourceLocation, FavoriteEntity, EntityRefLinks, useEntityList, EntityListProvider, CatalogFilterLayout, EntitySearchBar, EntityKindPicker, UserListPicker, EntityTagPicker, catalogApiRef, humanizeEntityRef } from '@backstage/plugin-catalog-react';
6
+ import { s as selectedTemplateRouteRef, r as registerComponentRouteRef, T as TemplateTypePicker, S as SecretsContext, a as scaffolderApiRef, b as scaffolderTaskRouteRef, c as rootRouteRef, F as FIELD_EXTENSION_WRAPPER_KEY, d as FIELD_EXTENSION_KEY, e as SecretsContextProvider, f as TaskPage } from './index-ae0b91e4.esm.js';
8
7
  import { RELATION_OWNED_BY, stringifyEntityRef } from '@backstage/catalog-model';
8
+ import { makeStyles, useTheme, Card, CardMedia, CardContent, Box, Typography, Chip, CardActions, IconButton, Tooltip, Link, Stepper, Step, StepLabel, StepContent, Button as Button$1, Paper, LinearProgress, TableContainer, Table, TableHead, TableRow, TableCell, TableBody, Grid, FormControl, InputLabel, Select, MenuItem } from '@material-ui/core';
9
9
  import { scmIntegrationsApiRef, ScmIntegrationIcon } from '@backstage/integration-react';
10
10
  import WarningIcon from '@material-ui/icons/Warning';
11
11
  import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common';
@@ -17,6 +17,13 @@ import { withTheme } from '@rjsf/core';
17
17
  import { Theme } from '@rjsf/material-ui';
18
18
  import cloneDeep from 'lodash/cloneDeep';
19
19
  import classNames from 'classnames';
20
+ import useDebounce from 'react-use/lib/useDebounce';
21
+ import { yaml as yaml$1 } from '@codemirror/legacy-modes/mode/yaml';
22
+ import { showPanel } from '@codemirror/panel';
23
+ import { StreamLanguage } from '@codemirror/stream-parser';
24
+ import CodeMirror from '@uiw/react-codemirror';
25
+ import yaml from 'yaml';
26
+ import { D as DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from './default-4cf122e1.esm.js';
20
27
  import '@backstage/errors';
21
28
  import 'zen-observable';
22
29
  import '@material-ui/core/FormControl';
@@ -26,7 +33,6 @@ import '@material-ui/lab';
26
33
  import '@material-ui/core/FormHelperText';
27
34
  import '@material-ui/core/Input';
28
35
  import '@material-ui/core/InputLabel';
29
- import 'react-use/lib/useDebounce';
30
36
  import 'lodash/capitalize';
31
37
  import '@material-ui/icons/CheckBox';
32
38
  import '@material-ui/icons/CheckBoxOutlineBlank';
@@ -45,35 +51,6 @@ import 'react-use/lib/useInterval';
45
51
  import 'use-immer';
46
52
  import '@material-ui/icons/Language';
47
53
 
48
- const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [
49
- {
50
- component: EntityPicker,
51
- name: "EntityPicker"
52
- },
53
- {
54
- component: EntityNamePicker,
55
- name: "EntityNamePicker",
56
- validation: entityNamePickerValidation
57
- },
58
- {
59
- component: EntityTagsPicker,
60
- name: "EntityTagsPicker"
61
- },
62
- {
63
- component: RepoUrlPicker,
64
- name: "RepoUrlPicker",
65
- validation: repoPickerValidation
66
- },
67
- {
68
- component: OwnerPicker,
69
- name: "OwnerPicker"
70
- },
71
- {
72
- component: OwnedEntityPicker,
73
- name: "OwnedEntityPicker"
74
- }
75
- ];
76
-
77
54
  const useStyles$2 = makeStyles((theme) => ({
78
55
  cardHeader: {
79
56
  position: "relative"
@@ -210,10 +187,6 @@ const TemplateList = ({
210
187
  const Card = TemplateCardComponent || TemplateCard;
211
188
  const maybeFilteredEntities = group ? entities.filter((e) => group.filter(e)) : entities;
212
189
  const titleComponent = (() => {
213
- if (group == null ? void 0 : group.titleComponent) {
214
- console.warn("DEPRECATED: group.titleComponent is now deprecated. Use group.title instead, it can be a string or a react component");
215
- return group == null ? void 0 : group.titleComponent;
216
- }
217
190
  if (group && group.title) {
218
191
  if (typeof group.title === "string") {
219
192
  return /* @__PURE__ */ React.createElement(ContentHeader, {
@@ -242,19 +215,10 @@ const TemplateList = ({
242
215
  })))));
243
216
  };
244
217
 
245
- const useStyles$1 = makeStyles((theme) => ({
246
- contentWrapper: {
247
- display: "grid",
248
- gridTemplateAreas: "'filters' 'grid'",
249
- gridTemplateColumns: "250px 1fr",
250
- gridColumnGap: theme.spacing(2)
251
- }
252
- }));
253
218
  const ScaffolderPageContents = ({
254
219
  TemplateCardComponent,
255
220
  groups
256
221
  }) => {
257
- const styles = useStyles$1();
258
222
  const registerComponentLink = useRouteRef(registerComponentRouteRef);
259
223
  const otherTemplatesGroup = {
260
224
  title: groups ? "Other Templates" : "Templates",
@@ -263,29 +227,27 @@ const ScaffolderPageContents = ({
263
227
  return !filtered.some((result) => result === true);
264
228
  }
265
229
  };
266
- const { allowed } = usePermission(catalogEntityCreatePermission);
230
+ const { allowed } = usePermission({
231
+ permission: catalogEntityCreatePermission
232
+ });
267
233
  return /* @__PURE__ */ React.createElement(Page, {
268
234
  themeId: "home"
269
235
  }, /* @__PURE__ */ React.createElement(Header, {
270
236
  pageTitleOverride: "Create a New Component",
271
- title: /* @__PURE__ */ React.createElement(React.Fragment, null, "Create a New Component ", /* @__PURE__ */ React.createElement(Lifecycle, {
272
- shorthand: true
273
- })),
237
+ title: "Create a New Component",
274
238
  subtitle: "Create new software components using standard templates"
275
239
  }), /* @__PURE__ */ React.createElement(Content, null, /* @__PURE__ */ React.createElement(ContentHeader, {
276
240
  title: "Available Templates"
277
241
  }, allowed && /* @__PURE__ */ React.createElement(CreateButton, {
278
242
  title: "Register Existing Component",
279
243
  to: registerComponentLink && registerComponentLink()
280
- }), /* @__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("div", {
281
- className: styles.contentWrapper
282
- }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(EntitySearchBar, null), /* @__PURE__ */ React.createElement(EntityKindPicker, {
244
+ }), /* @__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, {
283
245
  initialFilter: "template",
284
246
  hidden: true
285
247
  }), /* @__PURE__ */ React.createElement(UserListPicker, {
286
248
  initialFilter: "all",
287
249
  availableFilters: ["all", "starred"]
288
- }), /* @__PURE__ */ React.createElement(TemplateTypePicker, null), /* @__PURE__ */ React.createElement(EntityTagPicker, null)), /* @__PURE__ */ React.createElement("div", null, groups && groups.map((group, index) => /* @__PURE__ */ React.createElement(TemplateList, {
250
+ }), /* @__PURE__ */ React.createElement(TemplateTypePicker, null), /* @__PURE__ */ React.createElement(EntityTagPicker, null)), /* @__PURE__ */ React.createElement(CatalogFilterLayout.Content, null, groups && groups.map((group, index) => /* @__PURE__ */ React.createElement(TemplateList, {
289
251
  key: index,
290
252
  TemplateCardComponent,
291
253
  group
@@ -477,6 +439,9 @@ const MultistepJsonForm = (props) => {
477
439
  };
478
440
  const handleBack = () => setActiveStep(Math.max(activeStep - 1, 0));
479
441
  const handleCreate = async () => {
442
+ if (!onFinish) {
443
+ return;
444
+ }
480
445
  setDisableButtons(true);
481
446
  try {
482
447
  await onFinish();
@@ -541,7 +506,7 @@ const MultistepJsonForm = (props) => {
541
506
  variant: "contained",
542
507
  color: "primary",
543
508
  onClick: handleCreate,
544
- disabled: disableButtons
509
+ disabled: !onFinish || disableButtons
545
510
  }, "Create"))));
546
511
  };
547
512
 
@@ -639,9 +604,7 @@ const TemplatePage = ({
639
604
  themeId: "home"
640
605
  }, /* @__PURE__ */ React.createElement(Header, {
641
606
  pageTitleOverride: "Create a New Component",
642
- title: /* @__PURE__ */ React.createElement(React.Fragment, null, "Create a New Component ", /* @__PURE__ */ React.createElement(Lifecycle, {
643
- shorthand: true
644
- })),
607
+ title: "Create a New Component",
645
608
  subtitle: "Create new software components using standard templates"
646
609
  }), /* @__PURE__ */ React.createElement(Content, null, loading && /* @__PURE__ */ React.createElement(LinearProgress, {
647
610
  "data-testid": "loading-progress"
@@ -664,7 +627,7 @@ const TemplatePage = ({
664
627
  }))));
665
628
  };
666
629
 
667
- const useStyles = makeStyles((theme) => ({
630
+ const useStyles$1 = makeStyles((theme) => ({
668
631
  code: {
669
632
  fontFamily: "Menlo, monospace",
670
633
  padding: theme.spacing(1),
@@ -687,7 +650,7 @@ const useStyles = makeStyles((theme) => ({
687
650
  }));
688
651
  const ActionsPage = () => {
689
652
  const api = useApi(scaffolderApiRef);
690
- const classes = useStyles();
653
+ const classes = useStyles$1();
691
654
  const { loading, value, error } = useAsync(async () => {
692
655
  return api.listActions();
693
656
  });
@@ -772,8 +735,166 @@ const ActionsPage = () => {
772
735
  }), /* @__PURE__ */ React.createElement(Content, null, items));
773
736
  };
774
737
 
738
+ const EXAMPLE_TEMPLATE_PARAMS_YAML = `# Edit the template parameters below to see how they will render in the scaffolder form UI
739
+ parameters:
740
+ - title: Fill in some steps
741
+ required:
742
+ - name
743
+ properties:
744
+ name:
745
+ title: Name
746
+ type: string
747
+ description: Unique name of the component
748
+ owner:
749
+ title: Owner
750
+ type: string
751
+ description: Owner of the component
752
+ ui:field: OwnerPicker
753
+ ui:options:
754
+ allowedKinds:
755
+ - Group
756
+ - title: Choose a location
757
+ required:
758
+ - repoUrl
759
+ properties:
760
+ repoUrl:
761
+ title: Repository Location
762
+ type: string
763
+ ui:field: RepoUrlPicker
764
+ ui:options:
765
+ allowedHosts:
766
+ - github.com
767
+ `;
768
+ const useStyles = makeStyles({
769
+ templateSelect: {
770
+ marginBottom: "10px"
771
+ },
772
+ grid: {
773
+ height: "100%"
774
+ },
775
+ codeMirror: {
776
+ height: "95%"
777
+ }
778
+ });
779
+ const TemplatePreviewPage = ({
780
+ defaultPreviewTemplate = EXAMPLE_TEMPLATE_PARAMS_YAML,
781
+ customFieldExtensions = []
782
+ }) => {
783
+ const classes = useStyles();
784
+ const alertApi = useApi(alertApiRef);
785
+ const catalogApi = useApi(catalogApiRef);
786
+ const apiHolder = useApiHolder();
787
+ const [selectedTemplate, setSelectedTemplate] = useState("");
788
+ const [schema, setSchema] = useState({
789
+ title: "",
790
+ steps: []
791
+ });
792
+ const [templateOptions, setTemplateOptions] = useState([]);
793
+ const [templateYaml, setTemplateYaml] = useState(defaultPreviewTemplate);
794
+ const [formState, setFormState] = useState({});
795
+ const { loading } = useAsync(() => catalogApi.getEntities({
796
+ filter: { kind: "template" },
797
+ fields: [
798
+ "kind",
799
+ "metadata.namespace",
800
+ "metadata.name",
801
+ "metadata.title",
802
+ "spec.parameters"
803
+ ]
804
+ }).then(({ items }) => setTemplateOptions(items.map((template) => {
805
+ var _a;
806
+ return {
807
+ label: (_a = template.metadata.title) != null ? _a : humanizeEntityRef(template, { defaultKind: "template" }),
808
+ value: template
809
+ };
810
+ }))).catch((e) => alertApi.post({
811
+ message: `Error loading exisiting templates: ${e.message}`,
812
+ severity: "error"
813
+ })), [catalogApi]);
814
+ const errorPanel = document.createElement("div");
815
+ errorPanel.style.color = "red";
816
+ useDebounce(() => {
817
+ try {
818
+ const parsedTemplate = yaml.parse(templateYaml);
819
+ setSchema({
820
+ title: "Preview",
821
+ steps: parsedTemplate.parameters.map((param) => ({
822
+ title: param.title,
823
+ schema: param
824
+ }))
825
+ });
826
+ setFormState({});
827
+ } catch (e) {
828
+ errorPanel.textContent = e.message;
829
+ }
830
+ }, 250, [setFormState, setSchema, templateYaml]);
831
+ const handleSelectChange = useCallback((selected) => {
832
+ setSelectedTemplate(selected);
833
+ setTemplateYaml(yaml.stringify(selected.spec));
834
+ }, [setTemplateYaml]);
835
+ const handleFormReset = () => setFormState({});
836
+ const handleFormChange = useCallback((e) => setFormState(e.formData), [setFormState]);
837
+ const handleCodeChange = useCallback((code) => {
838
+ setTemplateYaml(code);
839
+ }, [setTemplateYaml]);
840
+ const customFieldComponents = Object.fromEntries(customFieldExtensions.map(({ name, component }) => [name, component]));
841
+ const customFieldValidators = Object.fromEntries(customFieldExtensions.map(({ name, validation }) => [name, validation]));
842
+ return /* @__PURE__ */ React.createElement(Page, {
843
+ themeId: "home"
844
+ }, /* @__PURE__ */ React.createElement(Header, {
845
+ title: "Template Preview",
846
+ subtitle: "Preview your template parameter UI"
847
+ }), /* @__PURE__ */ React.createElement(Content, null, loading && /* @__PURE__ */ React.createElement(LinearProgress, null), /* @__PURE__ */ React.createElement(Grid, {
848
+ container: true,
849
+ className: classes.grid
850
+ }, /* @__PURE__ */ React.createElement(Grid, {
851
+ item: true,
852
+ xs: 6
853
+ }, /* @__PURE__ */ React.createElement(FormControl, {
854
+ className: classes.templateSelect,
855
+ variant: "outlined",
856
+ fullWidth: true
857
+ }, /* @__PURE__ */ React.createElement(InputLabel, {
858
+ id: "select-template-label"
859
+ }, "Load Existing Template"), /* @__PURE__ */ React.createElement(Select, {
860
+ value: selectedTemplate,
861
+ label: "Load Existing Template",
862
+ labelId: "select-template-label",
863
+ onChange: (e) => handleSelectChange(e.target.value)
864
+ }, templateOptions.map((option, idx) => /* @__PURE__ */ React.createElement(MenuItem, {
865
+ key: idx,
866
+ value: option.value
867
+ }, option.label)))), /* @__PURE__ */ React.createElement(CodeMirror, {
868
+ className: classes.codeMirror,
869
+ value: templateYaml,
870
+ theme: "dark",
871
+ height: "100%",
872
+ extensions: [
873
+ StreamLanguage.define(yaml$1),
874
+ showPanel.of(() => ({ dom: errorPanel, top: true }))
875
+ ],
876
+ onChange: handleCodeChange
877
+ })), /* @__PURE__ */ React.createElement(Grid, {
878
+ item: true,
879
+ xs: 6
880
+ }, schema && /* @__PURE__ */ React.createElement(InfoCard, {
881
+ key: JSON.stringify(schema)
882
+ }, /* @__PURE__ */ React.createElement(MultistepJsonForm, {
883
+ formData: formState,
884
+ fields: customFieldComponents,
885
+ onChange: handleFormChange,
886
+ onReset: handleFormReset,
887
+ steps: schema.steps.map((step) => {
888
+ return {
889
+ ...step,
890
+ validate: createValidator(step.schema, customFieldValidators, { apiHolder })
891
+ };
892
+ })
893
+ }))))));
894
+ };
895
+
775
896
  const Router = (props) => {
776
- const { groups, components = {} } = props;
897
+ const { groups, components = {}, defaultPreviewTemplate } = props;
777
898
  const { TemplateCardComponent, TaskPageComponent } = components;
778
899
  const outlet = useOutlet();
779
900
  const TaskPageElement = TaskPageComponent != null ? TaskPageComponent : TaskPage;
@@ -803,8 +924,14 @@ const Router = (props) => {
803
924
  }), /* @__PURE__ */ React.createElement(Route, {
804
925
  path: "/actions",
805
926
  element: /* @__PURE__ */ React.createElement(ActionsPage, null)
927
+ }), /* @__PURE__ */ React.createElement(Route, {
928
+ path: "/preview",
929
+ element: /* @__PURE__ */ React.createElement(SecretsContextProvider, null, /* @__PURE__ */ React.createElement(TemplatePreviewPage, {
930
+ defaultPreviewTemplate,
931
+ customFieldExtensions: fieldExtensions
932
+ }))
806
933
  }));
807
934
  };
808
935
 
809
936
  export { Router };
810
- //# sourceMappingURL=Router-47c9a9ee.esm.js.map
937
+ //# sourceMappingURL=Router-9a3e085b.esm.js.map