From 44ddbab4cda81804f6a46d930eee4df2f974d739 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 11 Mar 2022 16:00:16 +0100 Subject: [PATCH 01/28] chore: added simple pageTitleOverride \ Signed-off-by: blam Signed-off-by: blam --- .../TemplateWizardPage/TemplateWizardPage.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx index 4d160548ca..f2da12930a 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import React from 'react'; +import { Page, Header, Lifecycle } from '@backstage/core-components'; import { FieldExtensionOptions } from '../../extensions'; export interface TemplateWizardPageProps { @@ -21,5 +22,16 @@ export interface TemplateWizardPageProps { } export const TemplateWizardPage = (_props: TemplateWizardPageProps) => { - return null; + return ( + +
+ Create a New Component + + } + /> + + ); }; From ad7381dc9914782b60bf246a3c7ae9525d17d7e9 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 11 Mar 2022 18:16:01 +0100 Subject: [PATCH 02/28] chore: updating backend to return title an description Signed-off-by: blam --- plugins/scaffolder-backend/src/service/router.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index c7d9af8e64..2f72004afe 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -156,8 +156,10 @@ export async function createRouter( const parameters = [template.spec.parameters ?? []].flat(); res.json({ title: template.metadata.title ?? template.metadata.name, + description: template.metadata.description, steps: parameters.map(schema => ({ - title: schema.title ?? 'Fill in template parameters', + title: schema.title ?? 'Please enter the following information', + description: schema.description, schema, })), }); From b6975ecc4b1df6d0b5c2fdc8c8d7eed9ccb22372 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 11 Mar 2022 18:17:09 +0100 Subject: [PATCH 03/28] feat: support passing styles to the MarkdownContent card Signed-off-by: blam Signed-off-by: blam --- .../src/components/MarkdownContent/MarkdownContent.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx index 0e5aedda90..013e81817a 100644 --- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx +++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx @@ -70,6 +70,7 @@ type Props = { linkTarget?: Options['linkTarget']; transformLinkUri?: (href: string) => string; transformImageUri?: (href: string) => string; + className?: string; }; const components: Options['components'] = { @@ -99,12 +100,13 @@ export function MarkdownContent(props: Props) { linkTarget, transformLinkUri, transformImageUri, + className, } = props; const classes = useStyles(); return ( Date: Fri, 11 Mar 2022 18:17:48 +0100 Subject: [PATCH 04/28] chore: starting to build some things for the wizard page component Signed-off-by: blam Signed-off-by: blam --- plugins/scaffolder/src/next/Router/Router.tsx | 4 +- .../TemplateCard/TemplateCard.tsx | 19 +++-- .../TemplateListPage/TemplateListPage.tsx | 8 +- .../TemplateWizardPage/TemplateWizardPage.tsx | 75 +++++++++++++++++-- plugins/scaffolder/src/routes.ts | 6 ++ plugins/scaffolder/src/types.ts | 2 + 6 files changed, 95 insertions(+), 19 deletions(-) diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx index 68bf9fa9e4..fb5769e4e4 100644 --- a/plugins/scaffolder/src/next/Router/Router.tsx +++ b/plugins/scaffolder/src/next/Router/Router.tsx @@ -28,7 +28,7 @@ import { import { useElementFilter } from '@backstage/core-plugin-api'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups'; -import { selectedTemplateRouteRef } from '../../routes'; +import { nextSelectedTemplateRouteRef } from '../../routes'; /** * The Props for the Scaffolder Router @@ -87,7 +87,7 @@ export const Router = (props: PropsWithChildren) => { /> diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx index 874c20a9ac..10039115e0 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx @@ -36,7 +36,7 @@ import { getEntityRelations, } from '@backstage/plugin-catalog-react'; import { useRouteRef } from '@backstage/core-plugin-api'; -import { selectedTemplateRouteRef } from '../../../routes'; +import { nextSelectedTemplateRouteRef } from '../../../routes'; import { BackstageTheme } from '@backstage/theme'; const useStyles = makeStyles(theme => ({ @@ -46,10 +46,11 @@ const useStyles = makeStyles(theme => ({ display: '-webkit-box', '-webkit-line-clamp': 10, '-webkit-box-orient': 'vertical', + }, + markdown: { /** to make the styles for React Markdown not leak into the description */ - '& p:first-child': { + '& :first-child': { marginTop: 0, - marginBottom: theme.spacing(2), }, }, label: { @@ -94,9 +95,14 @@ export const TemplateCard = (props: TemplateCardProps) => { const { template } = props; const styles = useStyles(); const ownedByRelations = getEntityRelations(template, RELATION_OWNED_BY); - const templateRoute = useRouteRef(selectedTemplateRouteRef); - const { name, namespace } = parseEntityRef(stringifyEntityRef(template)); - const href = templateRoute({ templateName: name, namespace: namespace }); + const templateRoute = useRouteRef(nextSelectedTemplateRouteRef); + const { name, namespace } = parseEntityRef( + stringifyEntityRef(props.template), + ); + const href = templateRoute({ + templateName: name, + namespace: namespace, + }); return ( @@ -104,6 +110,7 @@ export const TemplateCard = (props: TemplateCardProps) => { diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx index 1b18dce805..775c6c0318 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx @@ -56,11 +56,11 @@ export const TemplateListPage = (props: TemplateListPageProps) => { return ( - +
diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx index f2da12930a..1a57a438c6 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -14,24 +14,85 @@ * limitations under the License. */ import React from 'react'; -import { Page, Header, Lifecycle } from '@backstage/core-components'; +import { + Page, + Header, + Content, + Progress, + InfoCard, + MarkdownContent, +} from '@backstage/core-components'; import { FieldExtensionOptions } from '../../extensions'; +import { useParams } from 'react-router'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { useApi } from '@backstage/core-plugin-api'; +import { scaffolderApiRef } from '../../api'; +import useAsync from 'react-use/lib/useAsync'; +import { makeStyles } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; export interface TemplateWizardPageProps { customFieldExtensions: FieldExtensionOptions[]; } +const useStyles = makeStyles(theme => ({ + markdown: { + /** to make the styles for React Markdown not leak into the description */ + '& :first-child': { + marginTop: 0, + }, + '& :last-child': { + marginBottom: 0, + }, + }, +})); + +const useTemplateParameterSchema = (templateRef: string) => { + const scaffolderApi = useApi(scaffolderApiRef); + const { value, loading, error } = useAsync( + () => scaffolderApi.getTemplateParameterSchema(templateRef), + [scaffolderApi, templateRef], + ); + + return { manifest: value, loading, error }; +}; + export const TemplateWizardPage = (_props: TemplateWizardPageProps) => { + const styles = useStyles(); + const { templateName, namespace } = useParams(); + const { loading, manifest, error } = useTemplateParameterSchema( + stringifyEntityRef({ + kind: 'Template', + namespace, + name: templateName, + }), + ); + return ( - +
- Create a New Component - - } + title="Create a new component" + subtitle="Create new software components using standard templates in your organization" /> + + {loading && } + {manifest && ( + + } + noPadding + titleTypographyProps={{ component: 'h2' }} + > + asd + + )} + ); }; diff --git a/plugins/scaffolder/src/routes.ts b/plugins/scaffolder/src/routes.ts index 8d6875e351..19a959585d 100644 --- a/plugins/scaffolder/src/routes.ts +++ b/plugins/scaffolder/src/routes.ts @@ -43,6 +43,12 @@ export const selectedTemplateRouteRef = createSubRouteRef({ path: '/templates/:namespace/:templateName', }); +export const nextSelectedTemplateRouteRef = createSubRouteRef({ + id: 'scaffolder/next/selected-template', + parent: rootRouteRef, + path: '/templates/:namespace/:templateName', +}); + export const scaffolderTaskRouteRef = createSubRouteRef({ id: 'scaffolder/task', parent: rootRouteRef, diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index 710af6ad23..d814119398 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -79,8 +79,10 @@ export type ScaffolderTaskOutput = { */ export type TemplateParameterSchema = { title: string; + description?: string; steps: Array<{ title: string; + description?: string; schema: JsonObject; }>; }; From e67ab9c4a280cd50271ca7bf4e15f044cbbd126a Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 7 Jun 2022 13:19:23 +0200 Subject: [PATCH 05/28] chore: fixing wizardy things Signed-off-by: blam --- packages/app/src/App.tsx | 19 +++++++++++++++++++ .../TemplateListPage/TemplateListPage.tsx | 4 ++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 289af8d40f..3f748813f5 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -62,6 +62,7 @@ import { NewRelicPage } from '@backstage/plugin-newrelic'; import { ScaffolderFieldExtensions, ScaffolderPage, + NextScaffolderPage, scaffolderPlugin, } from '@backstage/plugin-scaffolder'; import { SearchPage } from '@backstage/plugin-search'; @@ -212,6 +213,24 @@ const routes = ( + + entity?.metadata?.tags?.includes('recommended') ?? false, + }, + ]} + /> + } + > + + + + } /> { const registerComponentLink = useRouteRef(registerComponentRouteRef); - const { TemplateCardComponent, groups = [defaultGroup] } = props; + const { TemplateCardComponent, groups = [] } = props; return ( @@ -88,7 +88,7 @@ export const TemplateListPage = (props: TemplateListPageProps) => { From 39f1355eba8c58aed961ab4b9268b35a9b513b4d Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 18 Mar 2022 18:04:03 +0100 Subject: [PATCH 06/28] chore: starting to build the json schema form Stepper Signed-off-by: blam --- .../TemplateWizardPage/Stepper/Stepper.tsx | 69 +++++++++++++++++++ .../next/TemplateWizardPage/Stepper/index.ts | 16 +++++ .../TemplateWizardPage/TemplateWizardPage.tsx | 3 +- 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx create mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/Stepper/index.ts diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx new file mode 100644 index 0000000000..da8da2d00d --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx @@ -0,0 +1,69 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Stepper as MuiStepper, + Step as MuiStep, + StepLabel as MuiStepLabel, + Box, + Button, + makeStyles, +} from '@material-ui/core'; +import React, { useState } from 'react'; +import { TemplateParameterSchema } from '../../../types'; + +const useStyles = makeStyles(theme => ({ + backButton: { + marginRight: theme.spacing(1), + }, + footer: { + padding: theme.spacing(1, 1), + display: 'flex', + flexDirection: 'row', + justifyContent: 'right', + }, +})); + +export interface StepperProps { + manifest: TemplateParameterSchema; +} + +export const Stepper = (props: StepperProps) => { + const { steps } = props.manifest; + const [activeStep, setActiveStep] = useState(0); + const styles = useStyles(); + const handleBack = () => {}; + const handleNext = () => {}; + + return ( + + + {steps.map((step, index) => ( + + {step.title} + + ))} + +
+ + +
+
+ ); +}; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/index.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/index.ts new file mode 100644 index 0000000000..5532563e28 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { Stepper } from './Stepper'; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx index 1a57a438c6..5e0b20d468 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -29,6 +29,7 @@ import { useApi } from '@backstage/core-plugin-api'; import { scaffolderApiRef } from '../../api'; import useAsync from 'react-use/lib/useAsync'; import { makeStyles } from '@material-ui/core'; +import { Stepper } from './Stepper'; import { BackstageTheme } from '@backstage/theme'; export interface TemplateWizardPageProps { @@ -89,7 +90,7 @@ export const TemplateWizardPage = (_props: TemplateWizardPageProps) => { noPadding titleTypographyProps={{ component: 'h2' }} > - asd + )} From 97d34d4765b82f307130b509a40023a0d56435b8 Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 19 Mar 2022 10:16:54 +0100 Subject: [PATCH 07/28] chore: reworking some more styles for the stepperf Signed-off-by: blam --- .../src/next/TemplateWizardPage/Stepper/Stepper.tsx | 5 +++-- plugins/scaffolder/src/plugin.ts | 8 ++++++-- plugins/scaffolder/src/routes.ts | 6 +++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx index da8da2d00d..ba6fb754c5 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx @@ -48,7 +48,7 @@ export const Stepper = (props: StepperProps) => { const handleNext = () => {}; return ( - + <> {steps.map((step, index) => ( @@ -56,6 +56,7 @@ export const Stepper = (props: StepperProps) => { ))} +
-
+ ); }; diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index 5ea4ae49a4..7387232948 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -23,7 +23,11 @@ import { OwnerPicker } from './components/fields/OwnerPicker/OwnerPicker'; import { repoPickerValidation } from './components/fields/RepoUrlPicker'; import { RepoUrlPicker } from './components/fields/RepoUrlPicker/RepoUrlPicker'; import { createScaffolderFieldExtension } from './extensions'; -import { registerComponentRouteRef, rootRouteRef } from './routes'; +import { + nextRouteRef, + registerComponentRouteRef, + rootRouteRef, +} from './routes'; import { createApiFactory, createPlugin, @@ -162,6 +166,6 @@ export const NextScaffolderPage = scaffolderPlugin.provide( createRoutableExtension({ name: 'NextScaffolderPage', component: () => import('./next/Router').then(m => m.Router), - mountPoint: rootRouteRef, + mountPoint: nextRouteRef, }), ); diff --git a/plugins/scaffolder/src/routes.ts b/plugins/scaffolder/src/routes.ts index 19a959585d..f6ca7ba467 100644 --- a/plugins/scaffolder/src/routes.ts +++ b/plugins/scaffolder/src/routes.ts @@ -37,6 +37,10 @@ export const legacySelectedTemplateRouteRef = createSubRouteRef({ path: '/templates/:templateName', }); +const nextRouteRef = createRouteRef({ + id: 'scaffolder/next', +}); + export const selectedTemplateRouteRef = createSubRouteRef({ id: 'scaffolder/selected-template', parent: rootRouteRef, @@ -45,7 +49,7 @@ export const selectedTemplateRouteRef = createSubRouteRef({ export const nextSelectedTemplateRouteRef = createSubRouteRef({ id: 'scaffolder/next/selected-template', - parent: rootRouteRef, + parent: nextRouteRef, path: '/templates/:namespace/:templateName', }); From d41a786669770bb35b5155f09bbcf4d089c54ecc Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 21 Mar 2022 10:08:52 +0100 Subject: [PATCH 08/28] chore: some more progress on the forms Signed-off-by: blam --- .../src/next/TemplateWizardPage/Stepper/Stepper.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx index ba6fb754c5..9acd906e2f 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx @@ -17,10 +17,11 @@ import { Stepper as MuiStepper, Step as MuiStep, StepLabel as MuiStepLabel, - Box, Button, makeStyles, } from '@material-ui/core'; +import { withTheme } from '@rjsf/core'; +import { Theme as MuiTheme } from '@rjsf/material-ui'; import React, { useState } from 'react'; import { TemplateParameterSchema } from '../../../types'; @@ -34,12 +35,17 @@ const useStyles = makeStyles(theme => ({ flexDirection: 'row', justifyContent: 'right', }, + formWrapper: { + padding: theme.spacing(2), + }, })); export interface StepperProps { manifest: TemplateParameterSchema; } +const Form = withTheme(MuiTheme); + export const Stepper = (props: StepperProps) => { const { steps } = props.manifest; const [activeStep, setActiveStep] = useState(0); @@ -56,7 +62,9 @@ export const Stepper = (props: StepperProps) => { ))} - +
+
+
- + +
+ + +
+
); From 4b3687939f9b17f360286c077de364ba398be422 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 23 Mar 2022 19:18:06 +0100 Subject: [PATCH 10/28] chore: starting to write ui:schema extractors Signed-off-by: blam --- .../src/next/TemplateWizardPage/Stepper/schema.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts new file mode 100644 index 0000000000..b61d59e88d --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts @@ -0,0 +1,15 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ From 06f44b95bd2b0fd168343a0d4cf074d010980dc8 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 24 Mar 2022 09:51:00 +0100 Subject: [PATCH 11/28] more schema stuff Signed-off-by: blam --- .../TemplateWizardPage/Stepper/schema.test.ts | 77 +++++++++++++++++++ .../next/TemplateWizardPage/Stepper/schema.ts | 24 ++++++ .../TemplateWizardPage/TemplateWizardPage.tsx | 2 +- 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts new file mode 100644 index 0000000000..b368b832a4 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { extractSchemaFromManifest } from './schema'; + +describe('schema utils', () => { + it('should do stuff', () => { + const inputSchema = { + type: 'object', + 'ui:welp': 'warp', + properties: { + field1: { + type: 'string', + 'ui:derp': 'herp', + }, + field2: { + type: 'object', + properties: { + fieldX: { + type: 'string', + 'ui:derp': 'xerp', + }, + }, + }, + }, + }; + const expectedSchema = { + type: 'object', + properties: { + field1: { + type: 'string', + }, + field2: { + type: 'object', + properties: { + fieldX: { + type: 'string', + }, + }, + }, + }, + }; + const expectedUiSchema = { + 'ui:welp': 'warp', + field1: { + 'ui:derp': 'herp', + }, + field2: { + fieldX: { + 'ui:derp': 'xerp', + }, + }, + }; + + expect( + extractSchemaFromManifest({ + title: 'test', + steps: [{ title: 'test', schema: inputSchema }], + }), + ).toEqual({ + schema: expectedSchema, + uiSchema: expectedUiSchema, + }); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts index b61d59e88d..6140cfd978 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts @@ -13,3 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { UiSchema } from '@rjsf/core'; +import { JSONSchema7 } from 'json-schema'; + +import { TemplateParameterSchema } from '../../../types'; + +export const extractSchemaFromManifest = ( + manifest: TemplateParameterSchema, +): { uiSchema: UiSchema; schema: JSONSchema7 } => { + const schema = manifest.steps[0].schema; + const uiSchema = JSON.parse(JSON.stringify(schema), (key, value) => { + if (typeof value === 'object') { + return Object.fromEntries( + Object.entries(value).filter( + ([k, v]) => k.startsWith('ui:') || typeof v === 'object', + ), + ); + } + + return value; + }); + + console.log(uiSchema); + return { uiSchema, schema: {} }; +}; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx index 5e0b20d468..58059f55bf 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -90,7 +90,7 @@ export const TemplateWizardPage = (_props: TemplateWizardPageProps) => { noPadding titleTypographyProps={{ component: 'h2' }} > - + )} From f534f52696ba398bcb23a8cf7f323f0b973d5b9e Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 24 Mar 2022 11:55:49 +0100 Subject: [PATCH 12/28] chore: added step extraction with tests Signed-off-by: blam --- .../TemplateWizardPage/Stepper/schema.test.ts | 360 +++++++++++++++++- .../next/TemplateWizardPage/Stepper/schema.ts | 102 ++++- 2 files changed, 436 insertions(+), 26 deletions(-) diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts index b368b832a4..5c56c861b1 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * Copyright 2020 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { extractSchemaFromManifest } from './schema'; -describe('schema utils', () => { - it('should do stuff', () => { - const inputSchema = { +import { JsonObject } from '@backstage/types'; +import { TemplateParameterSchema } from '../../../types'; +import { extractSchemaFromStep } from './schema'; + +describe('extractSchemaFromStep', () => { + it('transforms deep schema', () => { + const inputSchema: JsonObject = { type: 'object', 'ui:welp': 'warp', properties: { @@ -64,12 +67,347 @@ describe('schema utils', () => { }, }; - expect( - extractSchemaFromManifest({ - title: 'test', - steps: [{ title: 'test', schema: inputSchema }], - }), - ).toEqual({ + expect(extractSchemaFromStep(inputSchema)).toEqual({ + schema: expectedSchema, + uiSchema: expectedUiSchema, + }); + }); + + it('transforms schema with anyOf fields', () => { + const inputSchema: JsonObject = { + type: 'object', + anyOf: [ + { + properties: { + field3: { + type: 'string', + default: 'Value 1', + 'ui:readonly': true, + }, + }, + }, + { + properties: { + field3: { + type: 'string', + default: 'Value 2', + 'ui:readonly': true, + }, + }, + }, + ], + oneOf: [ + { + properties: { + field4: { + type: 'string', + default: 'Value 1', + 'ui:readonly': true, + }, + }, + }, + ], + allOf: [ + { + properties: { + field5: { + type: 'string', + default: 'Value 1', + 'ui:readonly': true, + }, + }, + }, + ], + properties: { + field1: { + type: 'object', + anyOf: [ + { + properties: { + field3: { + type: 'string', + default: 'Value 1', + 'ui:readonly': true, + }, + }, + }, + { + properties: { + field3: { + type: 'string', + default: 'Value 2', + 'ui:readonly': true, + }, + }, + }, + ], + oneOf: [ + { + properties: { + field4: { + type: 'string', + default: 'Value 1', + 'ui:readonly': true, + }, + }, + }, + ], + allOf: [ + { + properties: { + field5: { + type: 'string', + default: 'Value 1', + 'ui:readonly': true, + }, + }, + }, + ], + }, + field2: { + type: 'string', + 'ui:derp': 'xerp', + }, + }, + }; + const expectedSchema = { + type: 'object', + anyOf: [ + { + properties: { + field3: { + type: 'string', + default: 'Value 1', + }, + }, + }, + { + properties: { + field3: { + type: 'string', + default: 'Value 2', + }, + }, + }, + ], + oneOf: [ + { + properties: { + field4: { + type: 'string', + default: 'Value 1', + }, + }, + }, + ], + allOf: [ + { + properties: { + field5: { + type: 'string', + default: 'Value 1', + }, + }, + }, + ], + properties: { + field1: { + type: 'object', + anyOf: [ + { + properties: { + field3: { + type: 'string', + default: 'Value 1', + }, + }, + }, + { + properties: { + field3: { + type: 'string', + default: 'Value 2', + }, + }, + }, + ], + oneOf: [ + { + properties: { + field4: { + type: 'string', + default: 'Value 1', + }, + }, + }, + ], + allOf: [ + { + properties: { + field5: { + type: 'string', + default: 'Value 1', + }, + }, + }, + ], + }, + field2: { + type: 'string', + }, + }, + }; + const expectedUiSchema = { + field3: { + 'ui:readonly': true, + }, + field4: { + 'ui:readonly': true, + }, + field5: { + 'ui:readonly': true, + }, + field1: { + field3: { + 'ui:readonly': true, + }, + field4: { + 'ui:readonly': true, + }, + field5: { + 'ui:readonly': true, + }, + }, + field2: { + 'ui:derp': 'xerp', + }, + }; + + expect(extractSchemaFromStep(inputSchema)).toEqual({ + schema: expectedSchema, + uiSchema: expectedUiSchema, + }); + }); + + it('transforms schema with dependencies', () => { + const inputSchema: JsonObject = { + type: 'object', + properties: { + name: { + type: 'string', + }, + credit_card: { + type: 'number', + }, + }, + required: ['name'], + dependencies: { + credit_card: { + properties: { + billing_address: { + type: 'string', + 'ui:widget': 'textarea', + }, + }, + required: ['billing_address'], + }, + }, + }; + const expectedSchema = { + type: 'object', + properties: { + name: { + type: 'string', + }, + credit_card: { + type: 'number', + }, + }, + required: ['name'], + dependencies: { + credit_card: { + properties: { + billing_address: { + type: 'string', + }, + }, + required: ['billing_address'], + }, + }, + }; + const expectedUiSchema = { + billing_address: { + 'ui:widget': 'textarea', + }, + credit_card: {}, + name: {}, + }; + + expect(extractSchemaFromStep(inputSchema)).toEqual({ + schema: expectedSchema, + uiSchema: expectedUiSchema, + }); + }); + + it('transforms schema with array items', () => { + const inputSchema: JsonObject = { + type: 'object', + properties: { + person: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + }, + address: { + type: 'string', + 'ui:widget': 'textarea', + }, + }, + }, + }, + accountNumber: { + type: 'number', + }, + }, + }; + const expectedSchema = { + type: 'object', + properties: { + person: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + }, + address: { + type: 'string', + }, + }, + }, + }, + accountNumber: { + type: 'number', + }, + }, + }; + const expectedUiSchema = { + accountNumber: {}, + person: { + items: { + name: {}, + address: { + 'ui:widget': 'textarea', + }, + }, + }, + }; + + expect(extractSchemaFromStep(inputSchema)).toEqual({ schema: expectedSchema, uiSchema: expectedUiSchema, }); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts index 6140cfd978..d255e56e27 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts @@ -13,27 +13,99 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { JsonObject } from '@backstage/types'; import { UiSchema } from '@rjsf/core'; import { JSONSchema7 } from 'json-schema'; import { TemplateParameterSchema } from '../../../types'; -export const extractSchemaFromManifest = ( - manifest: TemplateParameterSchema, -): { uiSchema: UiSchema; schema: JSONSchema7 } => { - const schema = manifest.steps[0].schema; - const uiSchema = JSON.parse(JSON.stringify(schema), (key, value) => { - if (typeof value === 'object') { - return Object.fromEntries( - Object.entries(value).filter( - ([k, v]) => k.startsWith('ui:') || typeof v === 'object', - ), - ); +function isObject(value: unknown): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function extractUiSchema(schema: JsonObject, uiSchema: JsonObject) { + if (!isObject(schema)) { + return; + } + + const { properties, items, anyOf, oneOf, allOf, dependencies } = schema; + + for (const propName in schema) { + if (!schema.hasOwnProperty(propName)) { + continue; } - return value; - }); + if (propName.startsWith('ui:')) { + uiSchema[propName] = schema[propName]; + delete schema[propName]; + } + } - console.log(uiSchema); - return { uiSchema, schema: {} }; + if (isObject(properties)) { + for (const propName in properties) { + if (!properties.hasOwnProperty(propName)) { + continue; + } + + const schemaNode = properties[propName]; + if (!isObject(schemaNode)) { + continue; + } + const innerUiSchema = {}; + uiSchema[propName] = innerUiSchema; + extractUiSchema(schemaNode, innerUiSchema); + } + } + + if (isObject(items)) { + const innerUiSchema = {}; + uiSchema.items = innerUiSchema; + extractUiSchema(items, innerUiSchema); + } + + if (Array.isArray(anyOf)) { + for (const schemaNode of anyOf) { + if (!isObject(schemaNode)) { + continue; + } + extractUiSchema(schemaNode, uiSchema); + } + } + + if (Array.isArray(oneOf)) { + for (const schemaNode of oneOf) { + if (!isObject(schemaNode)) { + continue; + } + extractUiSchema(schemaNode, uiSchema); + } + } + + if (Array.isArray(allOf)) { + for (const schemaNode of allOf) { + if (!isObject(schemaNode)) { + continue; + } + extractUiSchema(schemaNode, uiSchema); + } + } + + if (isObject(dependencies)) { + for (const depName of Object.keys(dependencies)) { + const schemaNode = dependencies[depName]; + if (!isObject(schemaNode)) { + continue; + } + extractUiSchema(schemaNode, uiSchema); + } + } +} + +export const extractSchemaFromStep = ( + inputStep: JSONSchema7, +): { uiSchema: UiSchema; schema: JSONSchema7 } => { + const uiSchema: UiSchema = {}; + const returnSchema: JsonObject = JSON.parse(JSON.stringify(inputStep)); + extractUiSchema(returnSchema, uiSchema); + return { uiSchema, schema: returnSchema }; }; From d3e8169a62a2a4dccc3baf826c4a78548f8c87fc Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 25 Mar 2022 15:45:01 +0100 Subject: [PATCH 13/28] chore: adding a hook to deal with all the logic around the parameters schema Signed-off-by: blam --- .../TemplateWizardPage/Stepper/Stepper.tsx | 1 + .../TemplateWizardPage/Stepper/schema.test.ts | 1 - .../next/TemplateWizardPage/Stepper/schema.ts | 6 +- .../Stepper/useTemplateSchema.test.tsx | 120 ++++++++++++++++++ .../Stepper/useTemplateSchema.ts | 27 ++++ 5 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.test.tsx create mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx index dda4b6fe28..234f837973 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx @@ -55,6 +55,7 @@ export const Stepper = (props: StepperProps) => { const handleNext = () => { setActiveStep(prevActiveStep => prevActiveStep + 1); }; + return ( <> diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts index 5c56c861b1..01e6cea3cc 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts @@ -15,7 +15,6 @@ */ import { JsonObject } from '@backstage/types'; -import { TemplateParameterSchema } from '../../../types'; import { extractSchemaFromStep } from './schema'; describe('extractSchemaFromStep', () => { diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts index d255e56e27..bb4c56f7b0 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts @@ -17,8 +17,6 @@ import { JsonObject } from '@backstage/types'; import { UiSchema } from '@rjsf/core'; import { JSONSchema7 } from 'json-schema'; -import { TemplateParameterSchema } from '../../../types'; - function isObject(value: unknown): value is JsonObject { return typeof value === 'object' && value !== null && !Array.isArray(value); } @@ -101,6 +99,10 @@ function extractUiSchema(schema: JsonObject, uiSchema: JsonObject) { } } +/** + * @alpha + * Takes a step from a Backstage Template Manifest and converts it to a JSON Schema and UI Schema for rjsf + */ export const extractSchemaFromStep = ( inputStep: JSONSchema7, ): { uiSchema: UiSchema; schema: JSONSchema7 } => { diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.test.tsx new file mode 100644 index 0000000000..18cdc97264 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.test.tsx @@ -0,0 +1,120 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TemplateParameterSchema } from '../../../types'; +import { useTemplateSchema } from './useTemplateSchema'; +import { renderHook } from '@testing-library/react-hooks'; +import { TestApiProvider } from '@backstage/test-utils'; +import React from 'react'; +import { featureFlagsApiRef } from '@backstage/core-plugin-api'; + +describe('useTemplateSchema', () => { + it('should generate the correct schema', () => { + const manifest: TemplateParameterSchema = { + title: 'Test Template', + description: 'Test Template Description', + steps: [ + { + title: 'Step 1', + description: 'Step 1 Description', + schema: { + type: 'object', + properties: { + field1: { type: 'string', 'ui:field': 'MyCoolComponent' }, + }, + }, + }, + { + title: 'Step 2', + description: 'Step 2 Description', + schema: { + type: 'object', + properties: { + field2: { type: 'string', 'ui:field': 'MyCoolerComponent' }, + }, + }, + }, + ], + }; + + const { + steps: [first, second], + } = useTemplateSchema(manifest); + + expect(first.uiSchema).toEqual({ + field1: { 'ui:field': 'MyCoolComponent' }, + }); + + expect(first.schema).toEqual({ + type: 'object', + properties: { + field1: { type: 'string' }, + }, + }); + + expect(second.uiSchema).toEqual({ + field2: { 'ui:field': 'MyCoolerComponent' }, + }); + + expect(second.schema).toEqual({ + type: 'object', + properties: { + field2: { type: 'string' }, + }, + }); + }); + + it('should use featureFlags property to skip a step if the whole step is disabled', () => { + const manifest: TemplateParameterSchema = { + title: 'Test Template', + description: 'Test Template Description', + steps: [ + { + title: 'Step 1', + description: 'Step 1 Description', + schema: { + type: 'object', + 'ui:backstage': { + featureFlag: 'my-feature-flag', + }, + properties: { + field1: { type: 'string', 'ui:field': 'MyCoolComponent' }, + }, + }, + }, + { + title: 'Step 2', + description: 'Step 2 Description', + schema: { + type: 'object', + properties: { + field2: { type: 'string', 'ui:field': 'MyCoolerComponent' }, + }, + }, + }, + ], + }; + + const result = renderHook(() => useTemplateSchema(manifest), { + wrapper: ({ children }) => ( + false }]]} + > + {children} + + ), + }); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts new file mode 100644 index 0000000000..38ea34048e --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { UiSchema } from '@rjsf/core'; +import { JSONSchema7 } from 'json-schema'; +import { TemplateParameterSchema } from '../../../types'; +import { extractSchemaFromStep } from './schema'; + +export const useTemplateSchema = ( + manifest: TemplateParameterSchema, +): { steps: { uiSchema: UiSchema; schema: JSONSchema7 }[] } => { + return { + steps: manifest.steps.map(({ schema }) => extractSchemaFromStep(schema)), + }; +}; From 797c17eef749fa7bdac1300a0db30014233962b3 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 25 Mar 2022 16:25:01 +0100 Subject: [PATCH 14/28] chore: updating template schema to support ui:backstage featureFlag Signed-off-by: blam --- .../next/TemplateWizardPage/Stepper/schema.ts | 5 +- .../Stepper/useTemplateSchema.test.tsx | 190 ++++++++++++++---- .../Stepper/useTemplateSchema.ts | 35 +++- 3 files changed, 187 insertions(+), 43 deletions(-) diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts index bb4c56f7b0..5a33a622bb 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts @@ -15,7 +15,6 @@ */ import { JsonObject } from '@backstage/types'; import { UiSchema } from '@rjsf/core'; -import { JSONSchema7 } from 'json-schema'; function isObject(value: unknown): value is JsonObject { return typeof value === 'object' && value !== null && !Array.isArray(value); @@ -104,8 +103,8 @@ function extractUiSchema(schema: JsonObject, uiSchema: JsonObject) { * Takes a step from a Backstage Template Manifest and converts it to a JSON Schema and UI Schema for rjsf */ export const extractSchemaFromStep = ( - inputStep: JSONSchema7, -): { uiSchema: UiSchema; schema: JSONSchema7 } => { + inputStep: JsonObject, +): { uiSchema: UiSchema; schema: JsonObject } => { const uiSchema: UiSchema = {}; const returnSchema: JsonObject = JSON.parse(JSON.stringify(inputStep)); extractUiSchema(returnSchema, uiSchema); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.test.tsx index 18cdc97264..9be6faa0fa 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.test.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.test.tsx @@ -49,9 +49,17 @@ describe('useTemplateSchema', () => { ], }; - const { - steps: [first, second], - } = useTemplateSchema(manifest); + const { result } = renderHook(() => useTemplateSchema(manifest), { + wrapper: ({ children }) => ( + false }]]} + > + {children} + + ), + }); + + const [first, second] = result.current.steps; expect(first.uiSchema).toEqual({ field1: { 'ui:field': 'MyCoolComponent' }, @@ -76,45 +84,153 @@ describe('useTemplateSchema', () => { }); }); - it('should use featureFlags property to skip a step if the whole step is disabled', () => { - const manifest: TemplateParameterSchema = { - title: 'Test Template', - description: 'Test Template Description', - steps: [ - { - title: 'Step 1', - description: 'Step 1 Description', - schema: { - type: 'object', - 'ui:backstage': { - featureFlag: 'my-feature-flag', - }, - properties: { - field1: { type: 'string', 'ui:field': 'MyCoolComponent' }, + describe('FeatureFlags', () => { + it('should use featureFlags property to skip a step if the whole step is disabled', () => { + const manifest: TemplateParameterSchema = { + title: 'Test Template', + description: 'Test Template Description', + steps: [ + { + title: 'Step 1', + description: 'Step 1 Description', + schema: { + type: 'object', + 'ui:backstage': { + featureFlag: 'my-feature-flag', + }, + properties: { + field1: { type: 'string', 'ui:field': 'MyCoolComponent' }, + }, }, }, - }, - { - title: 'Step 2', - description: 'Step 2 Description', - schema: { - type: 'object', - properties: { - field2: { type: 'string', 'ui:field': 'MyCoolerComponent' }, + { + title: 'Step 2', + description: 'Step 2 Description', + schema: { + type: 'object', + properties: { + field2: { type: 'string', 'ui:field': 'MyCoolerComponent' }, + }, }, }, - }, - ], - }; + ], + }; - const result = renderHook(() => useTemplateSchema(manifest), { - wrapper: ({ children }) => ( - false }]]} - > - {children} - - ), + const { result } = renderHook(() => useTemplateSchema(manifest), { + wrapper: ({ children }) => ( + false }]]} + > + {children} + + ), + }); + + expect(result.current.steps).toHaveLength(1); + }); + + it('should use featureFlags property to enable a step if the whole step is enabled', () => { + const manifest: TemplateParameterSchema = { + title: 'Test Template', + description: 'Test Template Description', + steps: [ + { + title: 'Step 1', + description: 'Step 1 Description', + schema: { + type: 'object', + 'ui:backstage': { + featureFlag: 'my-feature-flag', + }, + properties: { + field1: { type: 'string', 'ui:field': 'MyCoolComponent' }, + }, + }, + }, + { + title: 'Step 2', + description: 'Step 2 Description', + schema: { + type: 'object', + properties: { + field2: { type: 'string', 'ui:field': 'MyCoolerComponent' }, + }, + }, + }, + ], + }; + + const { result } = renderHook(() => useTemplateSchema(manifest), { + wrapper: ({ children }) => ( + true }]]} + > + {children} + + ), + }); + + expect(result.current.steps).toHaveLength(2); + }); + + it('should filter out the particular property if the featureFlag is disabled', () => { + const manifest: TemplateParameterSchema = { + title: 'Test Template', + description: 'Test Template Description', + steps: [ + { + title: 'Step 1', + description: 'Step 1 Description', + schema: { + type: 'object', + properties: { + field1: { + type: 'string', + 'ui:field': 'MyCoolComponent', + 'ui:backstage': { + featureFlag: 'my-feature-flag', + }, + }, + visibleField: { + type: 'string', + 'ui:field': 'MyCoolComponent', + }, + }, + }, + }, + { + title: 'Step 2', + description: 'Step 2 Description', + schema: { + type: 'object', + properties: { + field2: { type: 'string', 'ui:field': 'MyCoolerComponent' }, + }, + }, + }, + ], + }; + + const { result } = renderHook(() => useTemplateSchema(manifest), { + wrapper: ({ children }) => ( + false }]]} + > + {children} + + ), + }); + + const [first] = result.current.steps; + + expect(first.schema).toEqual({ + type: 'object', + properties: { + visibleField: { + type: 'string', + }, + }, + }); }); }); }); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts index 38ea34048e..1fe6398228 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts @@ -13,15 +13,44 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { featureFlagsApiRef, useApi } from '@backstage/core-plugin-api'; +import { JsonObject } from '@backstage/types'; import { UiSchema } from '@rjsf/core'; -import { JSONSchema7 } from 'json-schema'; import { TemplateParameterSchema } from '../../../types'; import { extractSchemaFromStep } from './schema'; export const useTemplateSchema = ( manifest: TemplateParameterSchema, -): { steps: { uiSchema: UiSchema; schema: JSONSchema7 }[] } => { +): { steps: { uiSchema: UiSchema; schema: JsonObject }[] } => { + const featureFlags = useApi(featureFlagsApiRef); + const steps = manifest.steps.map(({ schema }) => + extractSchemaFromStep(schema), + ); + + const returningSteps = steps + .filter(step => { + const stepFeatureFlag = step.uiSchema['ui:backstage']?.featureFlag; + return stepFeatureFlag ? featureFlags.isActive(stepFeatureFlag) : true; + }) + .map(step => ({ + uiSchema: step.uiSchema, + schema: { + ...step.schema, + properties: Object.fromEntries( + Object.entries(step.schema.properties as JsonObject).filter( + ([key]) => { + const stepFeatureFlag = + step.uiSchema[key]?.['ui:backstage']?.featureFlag; + return stepFeatureFlag + ? featureFlags.isActive(stepFeatureFlag) + : true; + }, + ), + ), + }, + })); + return { - steps: manifest.steps.map(({ schema }) => extractSchemaFromStep(schema)), + steps: returningSteps, }; }; From 5a6e291cff385f9de4d2be6f5ff5268021f6bc46 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 28 Mar 2022 13:19:27 +0200 Subject: [PATCH 15/28] chore: reworking how the schema looks Signed-off-by: blam --- .../TemplateWizardPage/Stepper/Stepper.tsx | 3 ++- .../Stepper/useTemplateSchema.ts | 23 +++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx index 234f837973..75df18c786 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx @@ -24,6 +24,7 @@ import { withTheme } from '@rjsf/core'; import { Theme as MuiTheme } from '@rjsf/material-ui'; import React, { useState } from 'react'; import { TemplateParameterSchema } from '../../../types'; +import { useTemplateSchema } from './useTemplateSchema'; const useStyles = makeStyles(theme => ({ backButton: { @@ -46,7 +47,7 @@ export interface StepperProps { const Form = withTheme(MuiTheme); export const Stepper = (props: StepperProps) => { - const { steps } = props.manifest; + const { steps } = useTemplateSchema(props.manifest); const [activeStep, setActiveStep] = useState(0); const styles = useStyles(); const handleBack = () => { diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts index 1fe6398228..d5fe244a25 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts @@ -21,21 +21,34 @@ import { extractSchemaFromStep } from './schema'; export const useTemplateSchema = ( manifest: TemplateParameterSchema, -): { steps: { uiSchema: UiSchema; schema: JsonObject }[] } => { +): { + steps: { + uiSchema: UiSchema; + schema: JsonObject; + title: string; + description?: string; + }[]; +} => { const featureFlags = useApi(featureFlagsApiRef); - const steps = manifest.steps.map(({ schema }) => - extractSchemaFromStep(schema), - ); + const steps = manifest.steps.map(({ title, description, schema }) => ({ + title, + description, + ...extractSchemaFromStep(schema), + })); const returningSteps = steps + // Filter out steps that are not enabled with the feature flags .filter(step => { const stepFeatureFlag = step.uiSchema['ui:backstage']?.featureFlag; return stepFeatureFlag ? featureFlags.isActive(stepFeatureFlag) : true; }) + // Then filter out the properties that are not enabled with feature flag .map(step => ({ - uiSchema: step.uiSchema, + ...step, schema: { ...step.schema, + // Title is rendered at the top of the page, so let's ignore this from jsonschemaform + title: undefined, properties: Object.fromEntries( Object.entries(step.schema.properties as JsonObject).filter( ([key]) => { From 2e8cf988e3d38b7eb656db3cdf079e840fd84a79 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 1 Apr 2022 10:52:20 +0200 Subject: [PATCH 16/28] chore: need to use the proper routeRef Signed-off-by: blam --- .../TemplateCard/TemplateCard.test.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx index 44e3a0bbbf..4648d47af1 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx @@ -25,7 +25,7 @@ import { } from '@backstage/test-utils'; import { TemplateCard } from './TemplateCard'; import React from 'react'; -import { rootRouteRef } from '../../../routes'; +import { nextRouteRef } from '../../../routes'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { RELATION_OWNED_BY } from '@backstage/catalog-model'; @@ -54,7 +54,7 @@ describe('TemplateCard', () => { > , - { mountedRoutes: { '/': rootRouteRef } }, + { mountedRoutes: { '/': nextRouteRef } }, ); expect(getByText('bob')).toBeInTheDocument(); @@ -84,7 +84,7 @@ describe('TemplateCard', () => { > , - { mountedRoutes: { '/': rootRouteRef } }, + { mountedRoutes: { '/': nextRouteRef } }, ); const description = getByText('hello'); @@ -115,7 +115,7 @@ describe('TemplateCard', () => { > , - { mountedRoutes: { '/': rootRouteRef } }, + { mountedRoutes: { '/': nextRouteRef } }, ); expect(getByText('No description')).toBeInTheDocument(); @@ -145,7 +145,7 @@ describe('TemplateCard', () => { > , - { mountedRoutes: { '/': rootRouteRef } }, + { mountedRoutes: { '/': nextRouteRef } }, ); for (const tag of mockTemplate.metadata.tags!) { @@ -185,7 +185,7 @@ describe('TemplateCard', () => { , { mountedRoutes: { - '/': rootRouteRef, + '/': nextRouteRef, '/catalog/:kind/:namespace/:name': entityRouteRef, }, }, @@ -224,7 +224,7 @@ describe('TemplateCard', () => { , { mountedRoutes: { - '/': rootRouteRef, + '/': nextRouteRef, '/catalog/:kind/:namespace/:name': entityRouteRef, }, }, From 43342dcf0aca8adfd95a7ad852194353e95eea09 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 1 Apr 2022 11:32:49 +0200 Subject: [PATCH 17/28] chore: use the existing secrets provider otherwise existing fields wont work Signed-off-by: blam --- plugins/scaffolder/src/next/Router/Router.tsx | 2 +- .../SecretsContext/SecretsContext.test.tsx | 43 ----------- .../SecretsContext/SecretsContext.tsx | 73 ------------------- .../SecretsContext/index.ts | 20 ----- .../TemplateWizardPage/Stepper/Stepper.tsx | 13 +++- .../TemplateWizardPage/TemplateWizardPage.tsx | 19 ++++- 6 files changed, 28 insertions(+), 142 deletions(-) delete mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.test.tsx delete mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.tsx delete mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/index.ts diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx index fb5769e4e4..91efe850e0 100644 --- a/plugins/scaffolder/src/next/Router/Router.tsx +++ b/plugins/scaffolder/src/next/Router/Router.tsx @@ -16,7 +16,6 @@ import React, { PropsWithChildren } from 'react'; import { Routes, Route, useOutlet } from 'react-router'; import { TemplateListPage } from '../TemplateListPage'; -import { SecretsContextProvider } from '../TemplateWizardPage/SecretsContext'; import { TemplateWizardPage } from '../TemplateWizardPage'; import { FieldExtensionOptions, @@ -29,6 +28,7 @@ import { useElementFilter } from '@backstage/core-plugin-api'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups'; import { nextSelectedTemplateRouteRef } from '../../routes'; +import { SecretsContextProvider } from '../../components/secrets/SecretsContext'; /** * The Props for the Scaffolder Router diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.test.tsx deleted file mode 100644 index 37d35c9015..0000000000 --- a/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React, { useContext } from 'react'; -import { - useTemplateSecrets, - SecretsContextProvider, - SecretsContext, -} from './SecretsContext'; -import { renderHook, act } from '@testing-library/react-hooks'; - -describe('SecretsContext', () => { - it('should allow the setting of secrets in the context', async () => { - const { result } = renderHook( - () => ({ - hook: useTemplateSecrets(), - context: useContext(SecretsContext), - }), - { - wrapper: ({ children }) => ( - {children} - ), - }, - ); - expect(result.current.context?.secrets.foo).toEqual(undefined); - - act(() => result.current.hook.setSecret({ foo: 'bar' })); - - expect(result.current.context?.secrets.foo).toEqual('bar'); - }); -}); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.tsx deleted file mode 100644 index d6ca47d41c..0000000000 --- a/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React, { - useState, - useCallback, - useContext, - createContext, - PropsWithChildren, -} from 'react'; - -type SecretsContextContents = { - secrets: Record; - setSecrets: React.Dispatch>>; -}; - -/** - * The actual context object. - */ -export const SecretsContext = createContext( - undefined, -); - -/** - * The Context Provider that holds the state for the secrets. - * - * @alpha - */ -export const SecretsContextProvider = ({ children }: PropsWithChildren<{}>) => { - const [secrets, setSecrets] = useState>({}); - - return ( - - {children} - - ); -}; - -/** - * Hook to access the secrets context. - * @alpha - */ -export const useTemplateSecrets = () => { - const value = useContext(SecretsContext); - if (!value) { - throw new Error( - 'useTemplateSecrets must be used within a SecretsContextProvider', - ); - } - - const { setSecrets } = value; - - const setSecret = useCallback( - (input: Record) => { - setSecrets(currentSecrets => ({ ...currentSecrets, ...input })); - }, - [setSecrets], - ); - - return { setSecret }; -}; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/index.ts b/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/index.ts deleted file mode 100644 index 65b530dea1..0000000000 --- a/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export { - useTemplateSecrets, - SecretsContext, - SecretsContextProvider, -} from './SecretsContext'; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx index 75df18c786..f864e9ee09 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx @@ -22,7 +22,8 @@ import { } from '@material-ui/core'; import { withTheme } from '@rjsf/core'; import { Theme as MuiTheme } from '@rjsf/material-ui'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; +import { FieldExtensionOptions } from '../../../extensions'; import { TemplateParameterSchema } from '../../../types'; import { useTemplateSchema } from './useTemplateSchema'; @@ -42,6 +43,7 @@ const useStyles = makeStyles(theme => ({ export interface StepperProps { manifest: TemplateParameterSchema; + extensions: FieldExtensionOptions[]; } const Form = withTheme(MuiTheme); @@ -50,6 +52,13 @@ export const Stepper = (props: StepperProps) => { const { steps } = useTemplateSchema(props.manifest); const [activeStep, setActiveStep] = useState(0); const styles = useStyles(); + + const extensions = useMemo(() => { + return Object.fromEntries( + props.extensions.map(({ name, component }) => [name, component]), + ); + }, [props.extensions]); + const handleBack = () => { setActiveStep(prevActiveStep => prevActiveStep - 1); }; @@ -69,7 +78,9 @@ export const Stepper = (props: StepperProps) => {
diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx index 58059f55bf..304e7279cf 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -23,14 +23,15 @@ import { MarkdownContent, } from '@backstage/core-components'; import { FieldExtensionOptions } from '../../extensions'; -import { useParams } from 'react-router'; +import { Navigate, useParams } from 'react-router'; import { stringifyEntityRef } from '@backstage/catalog-model'; -import { useApi } from '@backstage/core-plugin-api'; +import { errorApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; import { scaffolderApiRef } from '../../api'; import useAsync from 'react-use/lib/useAsync'; import { makeStyles } from '@material-ui/core'; import { Stepper } from './Stepper'; import { BackstageTheme } from '@backstage/theme'; +import { nextRouteRef } from '../../routes'; export interface TemplateWizardPageProps { customFieldExtensions: FieldExtensionOptions[]; @@ -58,9 +59,11 @@ const useTemplateParameterSchema = (templateRef: string) => { return { manifest: value, loading, error }; }; -export const TemplateWizardPage = (_props: TemplateWizardPageProps) => { +export const TemplateWizardPage = (props: TemplateWizardPageProps) => { const styles = useStyles(); + const rootRef = useRouteRef(nextRouteRef); const { templateName, namespace } = useParams(); + const errorApi = useApi(errorApiRef); const { loading, manifest, error } = useTemplateParameterSchema( stringifyEntityRef({ kind: 'Template', @@ -69,6 +72,11 @@ export const TemplateWizardPage = (_props: TemplateWizardPageProps) => { }), ); + if (error) { + errorApi.post(new Error(`Failed to load template, ${error}`)); + return ; + } + return (
{ noPadding titleTypographyProps={{ component: 'h2' }} > - + )} From 4b5d417e17afd8b58d6e8e7cbf6a162aa5f6bb59 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 1 Apr 2022 11:43:42 +0200 Subject: [PATCH 18/28] chore: fix typescript Signed-off-by: blam --- .../src/next/TemplateWizardPage/TemplateWizardPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx index 304e7279cf..c5891c3d61 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -37,7 +37,7 @@ export interface TemplateWizardPageProps { customFieldExtensions: FieldExtensionOptions[]; } -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles(() => ({ markdown: { /** to make the styles for React Markdown not leak into the description */ '& :first-child': { From ba371f6f2b10f6c2f604ab7fdd511c8afb214d29 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 1 Apr 2022 16:07:38 +0200 Subject: [PATCH 19/28] chore: updating the api-report Signed-off-by: blam --- plugins/scaffolder/api-report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 345c726e24..24ff8e210d 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -442,8 +442,10 @@ export type TemplateGroupFilter = { // @public export type TemplateParameterSchema = { title: string; + description?: string; steps: Array<{ title: string; + description?: string; schema: JsonObject; }>; }; From c4b452e16a3bd48ef02df2749d0651972fd75128 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 1 Apr 2022 16:15:52 +0200 Subject: [PATCH 20/28] chore: added changeset Signed-off-by: blam --- .changeset/empty-apples-tie.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/empty-apples-tie.md diff --git a/.changeset/empty-apples-tie.md b/.changeset/empty-apples-tie.md new file mode 100644 index 0000000000..83e5bd36b6 --- /dev/null +++ b/.changeset/empty-apples-tie.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Starting the implement the first of the Wizard page for the `next` scaffolder plugin From 3251a491da9c84bfe030a304a167ff29c67098b4 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 8 Apr 2022 17:16:05 +0200 Subject: [PATCH 21/28] chore: fixing the actual tests and resetting the worflows so they are right Signed-off-by: blam Signed-off-by: blam --- .../src/next/TemplateListPage/TemplateListPage.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx index d5a76d2a47..26e2994c8a 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx @@ -25,7 +25,7 @@ import { TestApiProvider, } from '@backstage/test-utils'; import React from 'react'; -import { rootRouteRef } from '../../routes'; +import { nextRouteRef } from '../../routes'; import { TemplateListPage } from './TemplateListPage'; describe('TemplateListPage', () => { @@ -63,7 +63,7 @@ describe('TemplateListPage', () => { > , - { mountedRoutes: { '/': rootRouteRef } }, + { mountedRoutes: { '/': nextRouteRef } }, ); expect(getByPlaceholderText('Search')).toBeInTheDocument(); @@ -85,7 +85,7 @@ describe('TemplateListPage', () => { > , - { mountedRoutes: { '/': rootRouteRef } }, + { mountedRoutes: { '/': nextRouteRef } }, ); expect(getByRole('menuitem', { name: /All/ })).toBeInTheDocument(); @@ -108,7 +108,7 @@ describe('TemplateListPage', () => { > , - { mountedRoutes: { '/': rootRouteRef } }, + { mountedRoutes: { '/': nextRouteRef } }, ); expect(getByText('Categories')).toBeInTheDocument(); From 2463364996652d8aee0fadc3b8784ec5e0927b07 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Apr 2022 16:26:32 +0200 Subject: [PATCH 22/28] chore: added a nice new template Signed-off-by: blam --- app-config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app-config.yaml b/app-config.yaml index cb865a52b9..e8cc3d2924 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -270,6 +270,8 @@ catalog: # Backstage end-to-end tests of TechDocs - type: file target: ../../cypress/e2e-fixture.catalog.info.yaml + - type: url + target: https://github.com/benjdlambert/software-templates/blob/main/scaffolder-templates/react-ssr-template/template.yaml scaffolder: # Use to customize default commit author info used when new components are created # defaultAuthor: From a22af3edc84b33c289712098be24002634d7c3e0 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 7 Jun 2022 16:00:43 +0200 Subject: [PATCH 23/28] chore: break out the changesets Signed-off-by: blam --- .changeset/empty-apple-pie.md | 5 +++++ .changeset/empty-apples-tie.md | 5 ++--- app-config.yaml | 2 -- 3 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 .changeset/empty-apple-pie.md diff --git a/.changeset/empty-apple-pie.md b/.changeset/empty-apple-pie.md new file mode 100644 index 0000000000..8e9fecdafb --- /dev/null +++ b/.changeset/empty-apple-pie.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Adding a `className` prop to the `MarkdownContent` component diff --git a/.changeset/empty-apples-tie.md b/.changeset/empty-apples-tie.md index 83e5bd36b6..673bfd3acb 100644 --- a/.changeset/empty-apples-tie.md +++ b/.changeset/empty-apples-tie.md @@ -1,7 +1,6 @@ --- -'@backstage/core-components': patch -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder': minor +'@backstage/plugin-scaffolder-backend': minor --- Starting the implement the first of the Wizard page for the `next` scaffolder plugin diff --git a/app-config.yaml b/app-config.yaml index e8cc3d2924..cb865a52b9 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -270,8 +270,6 @@ catalog: # Backstage end-to-end tests of TechDocs - type: file target: ../../cypress/e2e-fixture.catalog.info.yaml - - type: url - target: https://github.com/benjdlambert/software-templates/blob/main/scaffolder-templates/react-ssr-template/template.yaml scaffolder: # Use to customize default commit author info used when new components are created # defaultAuthor: From 94ce0cda1396f4f45be0c5b6b51457e0bd897380 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 8 Jun 2022 15:52:45 +0200 Subject: [PATCH 24/28] chore: move to use effect Signed-off-by: blam --- .../src/next/TemplateWizardPage/TemplateWizardPage.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx index c5891c3d61..f8733061e4 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { Page, Header, @@ -72,8 +72,13 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => { }), ); + useEffect(() => { + if (error) { + errorApi.post(new Error(`Failed to load template, ${error}`)); + } + }, [error, errorApi]); + if (error) { - errorApi.post(new Error(`Failed to load template, ${error}`)); return ; } From 60ac7f3ad7832c6a5b417c4cea6e4a778ca4d081 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 10 Jun 2022 13:56:40 +0200 Subject: [PATCH 25/28] chore: starting to clean up some of the form components Signed-off-by: blam Signed-off-by: blam --- packages/core-components/src/components/Select/Select.tsx | 1 + .../src/components/fields/EntityPicker/EntityPicker.tsx | 3 ++- plugins/scaffolder/src/routes.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core-components/src/components/Select/Select.tsx b/packages/core-components/src/components/Select/Select.tsx index 2edf74882f..ff98189b09 100644 --- a/packages/core-components/src/components/Select/Select.tsx +++ b/packages/core-components/src/components/Select/Select.tsx @@ -206,6 +206,7 @@ export function SelectComponent(props: SelectProps) { data-testid="select" displayEmpty multiple={multiple} + margin="dense" onChange={handleChange} onClick={handleClick} open={isOpen} diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index c899b56683..b7e3122c31 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -102,8 +102,9 @@ export const EntityPicker = ( Date: Thu, 23 Jun 2022 11:41:02 +0200 Subject: [PATCH 26/28] chore: ammend changeset description Signed-off-by: blam Signed-off-by: blam --- .changeset/empty-apples-tie.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/empty-apples-tie.md b/.changeset/empty-apples-tie.md index 673bfd3acb..3ebfe0fc70 100644 --- a/.changeset/empty-apples-tie.md +++ b/.changeset/empty-apples-tie.md @@ -3,4 +3,4 @@ '@backstage/plugin-scaffolder-backend': minor --- -Starting the implement the first of the Wizard page for the `next` scaffolder plugin +Starting the implementation of the Wizard page for the `next` scaffolder plugin From 5b7f8fb4423f107ed69024b7517dbfa9c2c2feb5 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 23 Jun 2022 14:02:53 +0200 Subject: [PATCH 27/28] chore: saving state in the form Signed-off-by: blam --- .../Stepper/Stepper.test.tsx | 136 ++++++++++++++++++ .../TemplateWizardPage/Stepper/Stepper.tsx | 7 +- 2 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx new file mode 100644 index 0000000000..2b091883ae --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx @@ -0,0 +1,136 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { TemplateParameterSchema } from '../../../types'; +import { Stepper } from './Stepper'; +import { renderInTestApp } from '@backstage/test-utils'; +import { fireEvent } from '@testing-library/react'; + +describe('Stepper', () => { + it('should render the step titles for each step of the manifest', async () => { + const manifest: TemplateParameterSchema = { + steps: [ + { title: 'Step 1', schema: { properties: {} } }, + { title: 'Step 2', schema: { properties: {} } }, + ], + title: 'React JSON Schema Form Test', + }; + + const { getByText } = await renderInTestApp( + , + ); + + for (const step of manifest.steps) { + expect(getByText(step.title)).toBeInTheDocument(); + } + }); + + it('should render next / review button', async () => { + const manifest: TemplateParameterSchema = { + steps: [ + { title: 'Step 1', schema: { properties: {} } }, + { title: 'Step 2', schema: { properties: {} } }, + ], + title: 'React JSON Schema Form Test', + }; + + const { getByText } = await renderInTestApp( + , + ); + + expect(getByText('Next')).toBeInTheDocument(); + + await fireEvent.click(getByText('Next')); + + expect(getByText('Review')).toBeInTheDocument(); + }); + + it('should remember the state of the form when cycling through the pages', async () => { + const manifest: TemplateParameterSchema = { + steps: [ + { + title: 'Step 1', + schema: { + properties: { + name: { + type: 'string', + }, + }, + }, + }, + { + title: 'Step 2', + schema: { + properties: { + description: { + type: 'string', + }, + }, + }, + }, + ], + title: 'React JSON Schema Form Test', + }; + + const { getByRole, getByText } = await renderInTestApp( + , + ); + + await fireEvent.change(getByRole('textbox', { name: 'name' }), { + target: { value: 'im a test value' }, + }); + + await fireEvent.click(getByText('Next')); + + await fireEvent.click(getByText('Back')); + + expect(getByRole('textbox', { name: 'name' })).toHaveValue( + 'im a test value', + ); + }); + + it('should render custom field extensions properly', async () => { + const MockComponent = () => { + return

im a custom field extension

; + }; + + const manifest: TemplateParameterSchema = { + title: 'Custom Fields', + steps: [ + { + title: 'Test', + schema: { + properties: { + name: { + type: 'string', + 'ui:field': 'Mock', + }, + }, + }, + }, + ], + }; + + const { getByText } = await renderInTestApp( + , + ); + + expect(getByText('im a custom field extension')).toBeInTheDocument(); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx index f864e9ee09..19335d4993 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { JsonObject } from '@backstage/types'; import { Stepper as MuiStepper, Step as MuiStep, @@ -51,6 +52,7 @@ const Form = withTheme(MuiTheme); export const Stepper = (props: StepperProps) => { const { steps } = useTemplateSchema(props.manifest); const [activeStep, setActiveStep] = useState(0); + const [formState, setFormState] = useState({}); const styles = useStyles(); const extensions = useMemo(() => { @@ -62,8 +64,10 @@ export const Stepper = (props: StepperProps) => { const handleBack = () => { setActiveStep(prevActiveStep => prevActiveStep - 1); }; - const handleNext = () => { + + const handleNext = ({ formData }: { formData: JsonObject }) => { setActiveStep(prevActiveStep => prevActiveStep + 1); + setFormState(current => ({ ...current, ...formData })); }; return ( @@ -77,6 +81,7 @@ export const Stepper = (props: StepperProps) => {
Date: Fri, 29 Jul 2022 11:44:33 +0200 Subject: [PATCH 28/28] chore: parameterise Signed-off-by: blam --- packages/core-components/src/components/Select/Select.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core-components/src/components/Select/Select.tsx b/packages/core-components/src/components/Select/Select.tsx index ff98189b09..7229f89653 100644 --- a/packages/core-components/src/components/Select/Select.tsx +++ b/packages/core-components/src/components/Select/Select.tsx @@ -134,6 +134,7 @@ export type SelectProps = { triggerReset?: boolean; native?: boolean; disabled?: boolean; + margin?: 'dense' | 'none'; }; /** @public */ @@ -148,6 +149,7 @@ export function SelectComponent(props: SelectProps) { triggerReset, native = false, disabled = false, + margin, } = props; const classes = useStyles(); const [value, setValue] = useState( @@ -206,7 +208,7 @@ export function SelectComponent(props: SelectProps) { data-testid="select" displayEmpty multiple={multiple} - margin="dense" + margin={margin} onChange={handleChange} onClick={handleClick} open={isOpen}