From b60b2455d2ade5ddea692a949b29d832e313d419 Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 11 Nov 2023 13:47:38 +0100 Subject: [PATCH 01/18] chore: adding secret field extension Signed-off-by: blam Signed-off-by: blam --- .../src/components/fields/Secret/Secret.tsx | 50 +++++++++++++++++++ .../src/components/fields/Secret/index.tsx | 16 ++++++ .../scaffolder/src/components/fields/index.ts | 2 + plugins/scaffolder/src/extensions/default.ts | 5 ++ 4 files changed, 73 insertions(+) create mode 100644 plugins/scaffolder/src/components/fields/Secret/Secret.tsx create mode 100644 plugins/scaffolder/src/components/fields/Secret/index.tsx diff --git a/plugins/scaffolder/src/components/fields/Secret/Secret.tsx b/plugins/scaffolder/src/components/fields/Secret/Secret.tsx new file mode 100644 index 0000000000..bdd862f8ce --- /dev/null +++ b/plugins/scaffolder/src/components/fields/Secret/Secret.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2023 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 { useTemplateSecrets } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderRJSFFieldProps } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderField } from '@backstage/plugin-scaffolder-react/alpha'; +import { Input, InputLabel } from '@material-ui/core'; + +export const Secret = (props: ScaffolderRJSFFieldProps) => { + const { setSecrets } = useTemplateSecrets(); + const { + name, + schema: { title, description }, + rawErrors, + disabled, + errors, + required, + } = props; + + return ( + + {title} + setSecrets({ [name]: e.target?.value })} + type="password" + /> + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/Secret/index.tsx b/plugins/scaffolder/src/components/fields/Secret/index.tsx new file mode 100644 index 0000000000..e28248ef7d --- /dev/null +++ b/plugins/scaffolder/src/components/fields/Secret/index.tsx @@ -0,0 +1,16 @@ +/* + * Copyright 2023 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 * from './Secret'; diff --git a/plugins/scaffolder/src/components/fields/index.ts b/plugins/scaffolder/src/components/fields/index.ts index 7ee4448f6b..e29a4d791c 100644 --- a/plugins/scaffolder/src/components/fields/index.ts +++ b/plugins/scaffolder/src/components/fields/index.ts @@ -19,4 +19,6 @@ export * from './RepoUrlPicker'; export * from './OwnedEntityPicker'; export * from './EntityTagsPicker'; export * from './MyGroupsPicker'; +export * from './Secret'; + export { type FieldSchema, makeFieldSchemaFromZod } from './utils'; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index 013f242a12..ef4a701e86 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -43,6 +43,7 @@ import { MyGroupsPicker, MyGroupsPickerSchema, } from '../components/fields/MyGroupsPicker/MyGroupsPicker'; +import { Secret } from '../components'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { @@ -82,4 +83,8 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ name: 'MyGroupsPicker', schema: MyGroupsPickerSchema, }, + { + component: Secret, + name: 'Secret', + }, ]; From 77333b626d57c06d0ddf7a3c3e80f0e41273ffc3 Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 11 Nov 2023 13:56:27 +0100 Subject: [PATCH 02/18] chore: added things Signed-off-by: blam --- plugins/scaffolder/src/components/fields/Secret/Secret.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/components/fields/Secret/Secret.tsx b/plugins/scaffolder/src/components/fields/Secret/Secret.tsx index bdd862f8ce..59256c5c09 100644 --- a/plugins/scaffolder/src/components/fields/Secret/Secret.tsx +++ b/plugins/scaffolder/src/components/fields/Secret/Secret.tsx @@ -41,9 +41,10 @@ export const Secret = (props: ScaffolderRJSFFieldProps) => { {title} setSecrets({ [name]: e.target?.value })} type="password" + autoComplete="off" /> ); From da34ebee34dd04b2e2b1ba64fee142550d1e1a20 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 13 Nov 2023 11:14:52 +0100 Subject: [PATCH 03/18] wip Signed-off-by: blam Signed-off-by: blam --- .../sample-templates/all-templates.yaml | 1 + .../sample-templates/template.yaml | 19 +++++++++++++ .../src/components/fields/Secret/index.tsx | 1 + .../components/fields/Secret/validation.ts | 27 +++++++++++++++++++ plugins/scaffolder/src/extensions/default.ts | 3 ++- 5 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 plugins/scaffolder-backend/sample-templates/template.yaml create mode 100644 plugins/scaffolder/src/components/fields/Secret/validation.ts diff --git a/plugins/scaffolder-backend/sample-templates/all-templates.yaml b/plugins/scaffolder-backend/sample-templates/all-templates.yaml index 6637c9479b..fb2db2e608 100644 --- a/plugins/scaffolder-backend/sample-templates/all-templates.yaml +++ b/plugins/scaffolder-backend/sample-templates/all-templates.yaml @@ -6,6 +6,7 @@ metadata: spec: targets: - ./remote-templates.yaml + - ./template.yaml # For local development of a template, you can reference your local templates here. # Examples: # diff --git a/plugins/scaffolder-backend/sample-templates/template.yaml b/plugins/scaffolder-backend/sample-templates/template.yaml new file mode 100644 index 0000000000..6aad96b2e7 --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/template.yaml @@ -0,0 +1,19 @@ +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: test + title: Test template +spec: + parameters: + - title: Do something + properties: + myKey: + title: My key + description: asd + type: string + ui:field: Secret + ui:options: + required: true + steps: [] + type: service + \ No newline at end of file diff --git a/plugins/scaffolder/src/components/fields/Secret/index.tsx b/plugins/scaffolder/src/components/fields/Secret/index.tsx index e28248ef7d..83d7576552 100644 --- a/plugins/scaffolder/src/components/fields/Secret/index.tsx +++ b/plugins/scaffolder/src/components/fields/Secret/index.tsx @@ -14,3 +14,4 @@ * limitations under the License. */ export * from './Secret'; +export * from './validation'; diff --git a/plugins/scaffolder/src/components/fields/Secret/validation.ts b/plugins/scaffolder/src/components/fields/Secret/validation.ts new file mode 100644 index 0000000000..e7cb6566e7 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/Secret/validation.ts @@ -0,0 +1,27 @@ +import { CustomFieldValidator } from '@backstage/plugin-scaffolder-react'; + +/* + * Copyright 2023 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 const secretFieldValidation: CustomFieldValidator = ( + value, + errors, + { uiSchema }, +) => { + console.log(uiSchema); + if (uiSchema['ui:options'].required && !value) { + errors.addError('Required'); + } +}; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index ef4a701e86..24b70b6a0c 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -43,7 +43,7 @@ import { MyGroupsPicker, MyGroupsPickerSchema, } from '../components/fields/MyGroupsPicker/MyGroupsPicker'; -import { Secret } from '../components'; +import { Secret, secretFieldValidation } from '../components'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { @@ -86,5 +86,6 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { component: Secret, name: 'Secret', + validation: secretFieldValidation, }, ]; From c22929665fa5a770e9ad4fc622ff341138562496 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 18 Dec 2023 13:56:15 +0100 Subject: [PATCH 04/18] chore: work around some of the secret validation Signed-off-by: blam --- .../sample-templates/template.yaml | 4 +-- .../src/components/fields/Secret/Secret.tsx | 14 +++++++++- .../src/components/fields/Secret/index.tsx | 1 - .../components/fields/Secret/validation.ts | 27 ------------------- plugins/scaffolder/src/extensions/default.ts | 3 +-- 5 files changed, 16 insertions(+), 33 deletions(-) delete mode 100644 plugins/scaffolder/src/components/fields/Secret/validation.ts diff --git a/plugins/scaffolder-backend/sample-templates/template.yaml b/plugins/scaffolder-backend/sample-templates/template.yaml index 6aad96b2e7..8d0ec0316f 100644 --- a/plugins/scaffolder-backend/sample-templates/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/template.yaml @@ -6,14 +6,14 @@ metadata: spec: parameters: - title: Do something + required: + - myKey properties: myKey: title: My key description: asd type: string ui:field: Secret - ui:options: - required: true steps: [] type: service \ No newline at end of file diff --git a/plugins/scaffolder/src/components/fields/Secret/Secret.tsx b/plugins/scaffolder/src/components/fields/Secret/Secret.tsx index 59256c5c09..2933a405f1 100644 --- a/plugins/scaffolder/src/components/fields/Secret/Secret.tsx +++ b/plugins/scaffolder/src/components/fields/Secret/Secret.tsx @@ -27,6 +27,7 @@ export const Secret = (props: ScaffolderRJSFFieldProps) => { rawErrors, disabled, errors, + onChange, required, } = props; @@ -42,7 +43,18 @@ export const Secret = (props: ScaffolderRJSFFieldProps) => { setSecrets({ [name]: e.target?.value })} + onChange={e => { + // TODO(blam): this is a bit of a hack. We need to to probably figure out + // how to provide our own validator that can filter out the secrets from the + // jsonschema, or merge the secrets with the formData for validation? + // Makes the review step a little cleaner with this though. + onChange( + Array(e.target?.value.length ?? 0) + .fill('*') + .join(''), + ); + setSecrets({ [name]: e.target?.value }); + }} type="password" autoComplete="off" /> diff --git a/plugins/scaffolder/src/components/fields/Secret/index.tsx b/plugins/scaffolder/src/components/fields/Secret/index.tsx index 83d7576552..e28248ef7d 100644 --- a/plugins/scaffolder/src/components/fields/Secret/index.tsx +++ b/plugins/scaffolder/src/components/fields/Secret/index.tsx @@ -14,4 +14,3 @@ * limitations under the License. */ export * from './Secret'; -export * from './validation'; diff --git a/plugins/scaffolder/src/components/fields/Secret/validation.ts b/plugins/scaffolder/src/components/fields/Secret/validation.ts deleted file mode 100644 index e7cb6566e7..0000000000 --- a/plugins/scaffolder/src/components/fields/Secret/validation.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { CustomFieldValidator } from '@backstage/plugin-scaffolder-react'; - -/* - * Copyright 2023 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 const secretFieldValidation: CustomFieldValidator = ( - value, - errors, - { uiSchema }, -) => { - console.log(uiSchema); - if (uiSchema['ui:options'].required && !value) { - errors.addError('Required'); - } -}; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index 24b70b6a0c..ef4a701e86 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -43,7 +43,7 @@ import { MyGroupsPicker, MyGroupsPickerSchema, } from '../components/fields/MyGroupsPicker/MyGroupsPicker'; -import { Secret, secretFieldValidation } from '../components'; +import { Secret } from '../components'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { @@ -86,6 +86,5 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { component: Secret, name: 'Secret', - validation: secretFieldValidation, }, ]; From 96cf824297e5b6a6a176e49512945ce5c117cefc Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 18 Dec 2023 14:09:05 +0100 Subject: [PATCH 05/18] chore: added some test Signed-off-by: blam --- .../components/fields/Secret/Secret.test.tsx | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 plugins/scaffolder/src/components/fields/Secret/Secret.test.tsx diff --git a/plugins/scaffolder/src/components/fields/Secret/Secret.test.tsx b/plugins/scaffolder/src/components/fields/Secret/Secret.test.tsx new file mode 100644 index 0000000000..fd30c6afff --- /dev/null +++ b/plugins/scaffolder/src/components/fields/Secret/Secret.test.tsx @@ -0,0 +1,107 @@ +/* + * Copyright 2023 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 { + SecretsContextProvider, + useTemplateSecrets, +} from '@backstage/plugin-scaffolder-react'; +import { Secret } from './Secret'; +import { renderInTestApp } from '@backstage/test-utils'; +import { Form } from '@backstage/plugin-scaffolder-react/alpha'; +import validator from '@rjsf/validator-ajv8'; +import { fireEvent, act } from '@testing-library/react'; + +describe('', () => { + const SecretsComponent = () => { + const { secrets } = useTemplateSecrets(); + return ( +
{JSON.stringify({ secrets })}
+ ); + }; + + it('should set the current form value as a mask for the value entered', async () => { + const mockSecret = 'backstage'; + const onSubmit = jest.fn(); + + const { getByLabelText, getByRole } = await renderInTestApp( + +
+ + , + ); + + const secretInput = getByLabelText('secret'); + const submitButton = getByRole('button'); + + await act(async () => { + fireEvent.change(secretInput, { target: { value: mockSecret } }); + fireEvent.click(submitButton); + }); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + formData: '*********', + }), + expect.anything(), + ); + }); + + it('should set the secret value to the unmasked value', async () => { + const mockSecret = 'backstage'; + const onSubmit = jest.fn(); + + const { getByLabelText, getByTestId } = await renderInTestApp( + + + + , + ); + + const secretInput = getByLabelText('secret'); + + await act(async () => { + fireEvent.change(secretInput, { target: { value: mockSecret } }); + }); + + const { secrets } = JSON.parse(getByTestId('current-secrets').textContent!); + + expect(secrets.myKey).toBe(mockSecret); + }); +}); From 30f2066896929fb817687eb01ec57cfaa9fd2fcd Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 18 Dec 2023 14:18:07 +0100 Subject: [PATCH 06/18] chore: added documentation and deprecating old method Signed-off-by: blam --- .../software-templates/writing-templates.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 7019ecfb28..aa45d268d6 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -231,8 +231,53 @@ spec: inputType: tel ``` +### Using Secrets + +You may want to mark thinks as secret and make sure that these values are protected and not available through REST endpoints. You can do this by using the built in `ui:field: Secret`. + +You can define this property as any normal parameter, however the consumption of this parameter will not be available through `${{ parameters.myKey }}` you will need to use `${{ secrets.myKey }}` instead in the `template.yaml`. + +Parameters will be automatically masked in the review step. + +```yaml +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: v1beta3-demo + title: Test Action template + description: scaffolder v1beta3 template demo +spec: + owner: backstage/techdocs-core + type: service + + parameters: + - title: Authenticaion + description: Provide authentication for the resource + required: + - username + - password + properties: + username: + type: string + # use the built in Secret field extension + ui:field: Secret + password: + type: string + ui:field: Secret + + steps: + - id: setupAuthentication + action: auth:create + input: + # make sure to use ${{ secret.parameterName }} to reference these values + username: ${{ secrets.username }} + password: ${{ secrets.password }} +``` + ### Hide or mask sensitive data on Review step +> Note: this approach is soon to be deprecated, please mark things as secret by using the `Secret` field extension instead as metioned above. + Sometimes, specially in custom fields, you collect some data on Create form that must not be shown to the user on Review step. To hide or mask this data, you can use `ui:widget: password` or set some properties of `ui:backstage`: From 33edf500908eb72eebdaef4a604986137ef5d8ad Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 18 Dec 2023 14:20:34 +0100 Subject: [PATCH 07/18] chore: added chantgeset Signed-off-by: blam --- .changeset/itchy-otters-switch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/itchy-otters-switch.md diff --git a/.changeset/itchy-otters-switch.md b/.changeset/itchy-otters-switch.md new file mode 100644 index 0000000000..c55de1c700 --- /dev/null +++ b/.changeset/itchy-otters-switch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Added support for dealing with user provided secrets using a new field extension `ui:field: Secret` From 3855d2a1cd0495b05ce2b1504d6db12188bf1776 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 18 Dec 2023 14:25:53 +0100 Subject: [PATCH 08/18] chore: default the value so that the form looks ok Signed-off-by: blam --- plugins/scaffolder/src/components/fields/Secret/Secret.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/components/fields/Secret/Secret.tsx b/plugins/scaffolder/src/components/fields/Secret/Secret.tsx index 2933a405f1..98632ae135 100644 --- a/plugins/scaffolder/src/components/fields/Secret/Secret.tsx +++ b/plugins/scaffolder/src/components/fields/Secret/Secret.tsx @@ -20,7 +20,7 @@ import { ScaffolderField } from '@backstage/plugin-scaffolder-react/alpha'; import { Input, InputLabel } from '@material-ui/core'; export const Secret = (props: ScaffolderRJSFFieldProps) => { - const { setSecrets } = useTemplateSecrets(); + const { setSecrets, secrets } = useTemplateSecrets(); const { name, schema: { title, description }, @@ -55,6 +55,7 @@ export const Secret = (props: ScaffolderRJSFFieldProps) => { ); setSecrets({ [name]: e.target?.value }); }} + value={secrets[name] ?? ''} type="password" autoComplete="off" /> From 82af648de3251c3418ad7f9ffcc7866144a01948 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 18 Dec 2023 14:34:02 +0100 Subject: [PATCH 09/18] chore: reset some testing files and fix docs Signed-off-by: blam --- .../software-templates/writing-templates.md | 2 +- .../sample-templates/all-templates.yaml | 1 - .../sample-templates/template.yaml | 19 ------------------- 3 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 plugins/scaffolder-backend/sample-templates/template.yaml diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index aa45d268d6..e05923de78 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -276,7 +276,7 @@ spec: ### Hide or mask sensitive data on Review step -> Note: this approach is soon to be deprecated, please mark things as secret by using the `Secret` field extension instead as metioned above. +> Note: this approach is soon to be deprecated, please mark things as secret by using the `Secret` field extension instead as mentioned above. Sometimes, specially in custom fields, you collect some data on Create form that must not be shown to the user on Review step. To hide or mask this data, you can diff --git a/plugins/scaffolder-backend/sample-templates/all-templates.yaml b/plugins/scaffolder-backend/sample-templates/all-templates.yaml index fb2db2e608..6637c9479b 100644 --- a/plugins/scaffolder-backend/sample-templates/all-templates.yaml +++ b/plugins/scaffolder-backend/sample-templates/all-templates.yaml @@ -6,7 +6,6 @@ metadata: spec: targets: - ./remote-templates.yaml - - ./template.yaml # For local development of a template, you can reference your local templates here. # Examples: # diff --git a/plugins/scaffolder-backend/sample-templates/template.yaml b/plugins/scaffolder-backend/sample-templates/template.yaml deleted file mode 100644 index 8d0ec0316f..0000000000 --- a/plugins/scaffolder-backend/sample-templates/template.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: scaffolder.backstage.io/v1beta3 -kind: Template -metadata: - name: test - title: Test template -spec: - parameters: - - title: Do something - required: - - myKey - properties: - myKey: - title: My key - description: asd - type: string - ui:field: Secret - steps: [] - type: service - \ No newline at end of file From ae75bc12c8bf696d658a80703d31a594a2e01334 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 18 Dec 2023 15:09:11 +0100 Subject: [PATCH 10/18] chore: update the api-reports Signed-off-by: blam --- plugins/scaffolder/api-report.md | 4 ++++ plugins/scaffolder/src/components/fields/Secret/Secret.tsx | 3 +++ 2 files changed, 7 insertions(+) diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 4df1eff430..9f1240f48b 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -40,6 +40,7 @@ import { ScaffolderDryRunResponse as ScaffolderDryRunResponse_2 } from '@backsta import { ScaffolderGetIntegrationsListOptions as ScaffolderGetIntegrationsListOptions_2 } from '@backstage/plugin-scaffolder-react'; import { ScaffolderGetIntegrationsListResponse as ScaffolderGetIntegrationsListResponse_2 } from '@backstage/plugin-scaffolder-react'; import { ScaffolderOutputLink } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderRJSFFieldProps } from '@backstage/plugin-scaffolder-react'; import { ScaffolderScaffoldOptions as ScaffolderScaffoldOptions_2 } from '@backstage/plugin-scaffolder-react'; import { ScaffolderScaffoldResponse as ScaffolderScaffoldResponse_2 } from '@backstage/plugin-scaffolder-react'; import { ScaffolderStreamLogsOptions as ScaffolderStreamLogsOptions_2 } from '@backstage/plugin-scaffolder-react'; @@ -581,6 +582,9 @@ export type ScaffolderTaskStatus = ScaffolderTaskStatus_2; // @public @deprecated (undocumented) export type ScaffolderUseTemplateSecrets = ScaffolderUseTemplateSecrets_2; +// @public (undocumented) +export const Secret: (props: ScaffolderRJSFFieldProps) => React_2.JSX.Element; + // @public (undocumented) export const TaskPage: (props: { TemplateOutputsComponent?: React_2.ComponentType<{ diff --git a/plugins/scaffolder/src/components/fields/Secret/Secret.tsx b/plugins/scaffolder/src/components/fields/Secret/Secret.tsx index 98632ae135..58dd18476f 100644 --- a/plugins/scaffolder/src/components/fields/Secret/Secret.tsx +++ b/plugins/scaffolder/src/components/fields/Secret/Secret.tsx @@ -19,6 +19,9 @@ import { ScaffolderRJSFFieldProps } from '@backstage/plugin-scaffolder-react'; import { ScaffolderField } from '@backstage/plugin-scaffolder-react/alpha'; import { Input, InputLabel } from '@material-ui/core'; +/** + * @public + */ export const Secret = (props: ScaffolderRJSFFieldProps) => { const { setSecrets, secrets } = useTemplateSecrets(); const { From c963bcee4cc142656b341b981f37e26ad80693b5 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 18 Dec 2023 17:22:02 +0100 Subject: [PATCH 11/18] chore: improve the implementation a little bit Signed-off-by: blam --- .../src/next/components/Stepper/Stepper.tsx | 31 +++++++++++++++++-- .../SecretInput.test.tsx} | 6 ++-- .../SecretInput.tsx} | 19 ++---------- .../fields/{Secret => SecretInput}/index.tsx | 2 +- .../scaffolder/src/components/fields/index.ts | 1 - plugins/scaffolder/src/extensions/default.ts | 5 +-- 6 files changed, 38 insertions(+), 26 deletions(-) rename plugins/scaffolder/src/components/fields/{Secret/Secret.test.tsx => SecretInput/SecretInput.test.tsx} (96%) rename plugins/scaffolder/src/components/fields/{Secret/Secret.tsx => SecretInput/SecretInput.tsx} (70%) rename plugins/scaffolder/src/components/fields/{Secret => SecretInput}/index.tsx (95%) diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 5b41163ae4..70071721fa 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -24,7 +24,7 @@ import { LinearProgress, } from '@material-ui/core'; import { type IChangeEvent } from '@rjsf/core'; -import { ErrorSchema } from '@rjsf/utils'; +import { ErrorSchema, ValidatorType } from '@rjsf/utils'; import React, { useCallback, useMemo, @@ -49,6 +49,7 @@ import { LayoutOptions, FieldExtensionOptions, FormProps, + useTemplateSecrets, } from '@backstage/plugin-scaffolder-react'; import { ReviewStepProps } from '@backstage/plugin-scaffolder-react'; @@ -102,6 +103,7 @@ export const Stepper = (stepperProps: StepperProps) => { reviewButtonText = 'Review', } = components; const analytics = useAnalytics(); + const { secrets } = useTemplateSecrets(); const { presentation, steps } = useTemplateSchema(props.manifest); const apiHolder = useApiHolder(); const [activeStep, setActiveStep] = useState(0); @@ -111,6 +113,31 @@ export const Stepper = (stepperProps: StepperProps) => { const [errors, setErrors] = useState(); const styles = useStyles(); + const stringifiedSecrets = JSON.stringify(secrets); + + // Because secrets can be defined in the schema, we need to make sure that they + // are included in the validation process. So we merge the secrets and the formData + // together in validation. + const customValidator = useMemo( + () => ({ + isValid: (schema, formData, rootSchema) => + validator.isValid(schema, { ...formData, ...secrets }, rootSchema), + rawValidation: (schema, formData) => + validator.rawValidation(schema, { ...formData, ...secrets }), + validateFormData: (formData, schema, customFormats, transformErrors) => + validator.validateFormData( + { ...formData, ...secrets }, + schema, + customFormats, + transformErrors, + ), + // @deprecated + toErrorList: validator.toErrorList, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [secrets, stringifiedSecrets], + ); + const extensions = useMemo(() => { return Object.fromEntries( props.extensions.map(({ name, component }) => [name, component]), @@ -220,7 +247,7 @@ export const Stepper = (stepperProps: StepperProps) => { {/* eslint-disable-next-line no-nested-ternary */} {activeStep < steps.length ? ( ', () => { 'ui:field': 'Secret', }} fields={{ - Secret: Secret, + Secret: SecretInput, }} onSubmit={onSubmit} /> @@ -86,7 +86,7 @@ describe('', () => { }, }} fields={{ - Secret: Secret, + Secret: SecretInput, }} onSubmit={onSubmit} /> diff --git a/plugins/scaffolder/src/components/fields/Secret/Secret.tsx b/plugins/scaffolder/src/components/fields/SecretInput/SecretInput.tsx similarity index 70% rename from plugins/scaffolder/src/components/fields/Secret/Secret.tsx rename to plugins/scaffolder/src/components/fields/SecretInput/SecretInput.tsx index 58dd18476f..d68216650b 100644 --- a/plugins/scaffolder/src/components/fields/Secret/Secret.tsx +++ b/plugins/scaffolder/src/components/fields/SecretInput/SecretInput.tsx @@ -19,10 +19,7 @@ import { ScaffolderRJSFFieldProps } from '@backstage/plugin-scaffolder-react'; import { ScaffolderField } from '@backstage/plugin-scaffolder-react/alpha'; import { Input, InputLabel } from '@material-ui/core'; -/** - * @public - */ -export const Secret = (props: ScaffolderRJSFFieldProps) => { +export const SecretInput = (props: ScaffolderRJSFFieldProps) => { const { setSecrets, secrets } = useTemplateSecrets(); const { name, @@ -30,7 +27,6 @@ export const Secret = (props: ScaffolderRJSFFieldProps) => { rawErrors, disabled, errors, - onChange, required, } = props; @@ -46,18 +42,7 @@ export const Secret = (props: ScaffolderRJSFFieldProps) => { { - // TODO(blam): this is a bit of a hack. We need to to probably figure out - // how to provide our own validator that can filter out the secrets from the - // jsonschema, or merge the secrets with the formData for validation? - // Makes the review step a little cleaner with this though. - onChange( - Array(e.target?.value.length ?? 0) - .fill('*') - .join(''), - ); - setSecrets({ [name]: e.target?.value }); - }} + onChange={e => setSecrets({ [name]: e.target?.value })} value={secrets[name] ?? ''} type="password" autoComplete="off" diff --git a/plugins/scaffolder/src/components/fields/Secret/index.tsx b/plugins/scaffolder/src/components/fields/SecretInput/index.tsx similarity index 95% rename from plugins/scaffolder/src/components/fields/Secret/index.tsx rename to plugins/scaffolder/src/components/fields/SecretInput/index.tsx index e28248ef7d..859a04a4d3 100644 --- a/plugins/scaffolder/src/components/fields/Secret/index.tsx +++ b/plugins/scaffolder/src/components/fields/SecretInput/index.tsx @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './Secret'; +export * from './SecretInput'; diff --git a/plugins/scaffolder/src/components/fields/index.ts b/plugins/scaffolder/src/components/fields/index.ts index e29a4d791c..7f119f27be 100644 --- a/plugins/scaffolder/src/components/fields/index.ts +++ b/plugins/scaffolder/src/components/fields/index.ts @@ -19,6 +19,5 @@ export * from './RepoUrlPicker'; export * from './OwnedEntityPicker'; export * from './EntityTagsPicker'; export * from './MyGroupsPicker'; -export * from './Secret'; export { type FieldSchema, makeFieldSchemaFromZod } from './utils'; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index ef4a701e86..2e4b7032de 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -43,7 +43,8 @@ import { MyGroupsPicker, MyGroupsPickerSchema, } from '../components/fields/MyGroupsPicker/MyGroupsPicker'; -import { Secret } from '../components'; + +import { SecretInput } from '../components/fields/SecretInput'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { @@ -84,7 +85,7 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ schema: MyGroupsPickerSchema, }, { - component: Secret, + component: SecretInput, name: 'Secret', }, ]; From 640a7cc5d134c7ee33269296b327da18e2a27295 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 18 Dec 2023 17:25:17 +0100 Subject: [PATCH 12/18] chore: fix Secret field extension export Signed-off-by: blam --- plugins/scaffolder/api-report.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 9f1240f48b..4df1eff430 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -40,7 +40,6 @@ import { ScaffolderDryRunResponse as ScaffolderDryRunResponse_2 } from '@backsta import { ScaffolderGetIntegrationsListOptions as ScaffolderGetIntegrationsListOptions_2 } from '@backstage/plugin-scaffolder-react'; import { ScaffolderGetIntegrationsListResponse as ScaffolderGetIntegrationsListResponse_2 } from '@backstage/plugin-scaffolder-react'; import { ScaffolderOutputLink } from '@backstage/plugin-scaffolder-react'; -import { ScaffolderRJSFFieldProps } from '@backstage/plugin-scaffolder-react'; import { ScaffolderScaffoldOptions as ScaffolderScaffoldOptions_2 } from '@backstage/plugin-scaffolder-react'; import { ScaffolderScaffoldResponse as ScaffolderScaffoldResponse_2 } from '@backstage/plugin-scaffolder-react'; import { ScaffolderStreamLogsOptions as ScaffolderStreamLogsOptions_2 } from '@backstage/plugin-scaffolder-react'; @@ -582,9 +581,6 @@ export type ScaffolderTaskStatus = ScaffolderTaskStatus_2; // @public @deprecated (undocumented) export type ScaffolderUseTemplateSecrets = ScaffolderUseTemplateSecrets_2; -// @public (undocumented) -export const Secret: (props: ScaffolderRJSFFieldProps) => React_2.JSX.Element; - // @public (undocumented) export const TaskPage: (props: { TemplateOutputsComponent?: React_2.ComponentType<{ From b7902a715d2fb6e187f1aee40f6bc0ace7126825 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 18 Dec 2023 17:27:50 +0100 Subject: [PATCH 13/18] chore: fixing docs Signed-off-by: blam --- docs/features/software-templates/writing-templates.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index e05923de78..35458446c2 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -233,9 +233,9 @@ spec: ### Using Secrets -You may want to mark thinks as secret and make sure that these values are protected and not available through REST endpoints. You can do this by using the built in `ui:field: Secret`. +You may want to mark things as secret and make sure that these values are protected and not available through REST endpoints. You can do this by using the built in `ui:field: Secret`. -You can define this property as any normal parameter, however the consumption of this parameter will not be available through `${{ parameters.myKey }}` you will need to use `${{ secrets.myKey }}` instead in the `template.yaml`. +You can define this property as any normal parameter, however the consumption of this parameter will not be available through `${{ parameters.myKey }}` you will instead need to use `${{ secrets.myKey }}` in your `template.yaml`. Parameters will be automatically masked in the review step. @@ -269,7 +269,7 @@ spec: - id: setupAuthentication action: auth:create input: - # make sure to use ${{ secret.parameterName }} to reference these values + # make sure to use ${{ secrets.parameterName }} to reference these values username: ${{ secrets.username }} password: ${{ secrets.password }} ``` From 7c4ded2515042a1f9d14206acc0406e4cdc2ef77 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 19 Dec 2023 09:37:35 +0100 Subject: [PATCH 14/18] chore: add the other package to changeset Signed-off-by: blam --- .changeset/itchy-otters-switch.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/itchy-otters-switch.md b/.changeset/itchy-otters-switch.md index c55de1c700..72869032f2 100644 --- a/.changeset/itchy-otters-switch.md +++ b/.changeset/itchy-otters-switch.md @@ -1,5 +1,6 @@ --- '@backstage/plugin-scaffolder': minor +'@backstage/plugin-scaffolder-react': minor --- Added support for dealing with user provided secrets using a new field extension `ui:field: Secret` From 9df2ebc76008e6030f6079075de9f7a6a15f9647 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Tue, 19 Dec 2023 09:58:54 +0100 Subject: [PATCH 15/18] Update plugins/scaffolder/src/components/fields/SecretInput/SecretInput.test.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Ben Lambert Signed-off-by: blam --- .../src/components/fields/SecretInput/SecretInput.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/components/fields/SecretInput/SecretInput.test.tsx b/plugins/scaffolder/src/components/fields/SecretInput/SecretInput.test.tsx index 1765c42f16..a73f6a8fda 100644 --- a/plugins/scaffolder/src/components/fields/SecretInput/SecretInput.test.tsx +++ b/plugins/scaffolder/src/components/fields/SecretInput/SecretInput.test.tsx @@ -24,7 +24,7 @@ import { Form } from '@backstage/plugin-scaffolder-react/alpha'; import validator from '@rjsf/validator-ajv8'; import { fireEvent, act } from '@testing-library/react'; -describe('', () => { +describe('', () => { const SecretsComponent = () => { const { secrets } = useTemplateSecrets(); return ( From 2fa5f646b2408f92b2c64f3a6bfb055c717c5391 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 19 Dec 2023 10:29:33 +0100 Subject: [PATCH 16/18] feat: Stepper needs to be wrapped in the SecretsContext Signed-off-by: blam --- .../next/components/Stepper/Stepper.test.tsx | 127 +++++++++++------- .../src/next/components/Workflow/Workflow.tsx | 20 +-- .../fields/SecretInput/SecretInput.test.tsx | 37 ----- 3 files changed, 86 insertions(+), 98 deletions(-) diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx index 2bb66fd782..d7e271f8b3 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx @@ -21,6 +21,7 @@ import { act, fireEvent } from '@testing-library/react'; import type { RJSFValidationError } from '@rjsf/utils'; import { JsonValue } from '@backstage/types'; import { FieldExtensionComponentProps } from '../../../extensions'; +import { SecretsContextProvider } from '../../../secrets'; import { LayoutTemplate } from '../../../layouts'; describe('Stepper', () => { @@ -34,7 +35,9 @@ describe('Stepper', () => { }; const { getByText } = await renderInTestApp( - , + + + , ); for (const step of manifest.steps) { @@ -52,7 +55,9 @@ describe('Stepper', () => { }; const { getByRole } = await renderInTestApp( - , + + + , ); expect(getByRole('button', { name: 'Next' })).toBeInTheDocument(); @@ -92,7 +97,9 @@ describe('Stepper', () => { }; const { getByRole } = await renderInTestApp( - , + + + , ); await fireEvent.change(getByRole('textbox', { name: 'name' }), { @@ -140,7 +147,9 @@ describe('Stepper', () => { }; const { getByRole, getByLabelText } = await renderInTestApp( - , + + + , ); await fireEvent.change(getByRole('textbox', { name: 'name' }), { @@ -219,14 +228,16 @@ describe('Stepper', () => { }); const { getByRole } = await renderInTestApp( - , + + + , ); await fireEvent.change(getByRole('textbox', { name: 'repo' }), { @@ -275,11 +286,13 @@ describe('Stepper', () => { }; const { getByText } = await renderInTestApp( - , + + + , ); expect(getByText('im a custom field extension')).toBeInTheDocument(); @@ -308,17 +321,19 @@ describe('Stepper', () => { }; const { getByRole } = await renderInTestApp( - new Promise(r => setTimeout(r, 1000)), - }, - ]} - onCreate={jest.fn()} - />, + + new Promise(r => setTimeout(r, 1000)), + }, + ]} + onCreate={jest.fn()} + /> + , ); act(() => { @@ -356,12 +371,14 @@ describe('Stepper', () => { }; const { getByText, getByRole } = await renderInTestApp( - , + + + , ); await fireEvent.change(getByRole('textbox', { name: 'postcode' }), { @@ -401,7 +418,9 @@ describe('Stepper', () => { }); const { getByRole } = await renderInTestApp( - , + + + , ); expect(getByRole('textbox', { name: 'firstName' })).toHaveValue('John'); @@ -429,7 +448,9 @@ describe('Stepper', () => { }); const { getByRole } = await renderInTestApp( - , + + + , ); await act(async () => { @@ -464,15 +485,17 @@ describe('Stepper', () => { }; const { getByRole } = await renderInTestApp( - Make, - reviewButtonText: Inspect, - }} - />, + + Make, + reviewButtonText: Inspect, + }} + /> + , ); await act(async () => { @@ -516,12 +539,14 @@ describe('Stepper', () => { }; const { getByText, getByRole } = await renderInTestApp( - , + + + , ); expect(getByText('A Scaffolder Layout')).toBeInTheDocument(); diff --git a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx index c6030930c5..c2ea544463 100644 --- a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx +++ b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx @@ -111,11 +111,13 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => { noPadding titleTypographyProps={{ component: 'h2' }} > - + + + )} @@ -123,10 +125,8 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => { }; /** + * TODO(blam): work out what we want to do with these components in the new API. + * Should we really have EmbeddableWorkflow -> Workflow -> Stepper -> Form, or should we revisit this? * @alpha */ -export const EmbeddableWorkflow = (props: WorkflowProps) => ( - - - -); +export const EmbeddableWorkflow = Workflow; diff --git a/plugins/scaffolder/src/components/fields/SecretInput/SecretInput.test.tsx b/plugins/scaffolder/src/components/fields/SecretInput/SecretInput.test.tsx index a73f6a8fda..fa810b9d17 100644 --- a/plugins/scaffolder/src/components/fields/SecretInput/SecretInput.test.tsx +++ b/plugins/scaffolder/src/components/fields/SecretInput/SecretInput.test.tsx @@ -32,43 +32,6 @@ describe('', () => { ); }; - it('should set the current form value as a mask for the value entered', async () => { - const mockSecret = 'backstage'; - const onSubmit = jest.fn(); - - const { getByLabelText, getByRole } = await renderInTestApp( - - - - , - ); - - const secretInput = getByLabelText('secret'); - const submitButton = getByRole('button'); - - await act(async () => { - fireEvent.change(secretInput, { target: { value: mockSecret } }); - fireEvent.click(submitButton); - }); - - expect(onSubmit).toHaveBeenCalledWith( - expect.objectContaining({ - formData: '*********', - }), - expect.anything(), - ); - }); - it('should set the secret value to the unmasked value', async () => { const mockSecret = 'backstage'; const onSubmit = jest.fn(); From 857f44cb0321fddf22d52a916200f024343fb044 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 19 Dec 2023 10:31:26 +0100 Subject: [PATCH 17/18] chore: update changeset Signed-off-by: blam --- .changeset/itchy-otters-switch.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/itchy-otters-switch.md b/.changeset/itchy-otters-switch.md index 72869032f2..de155a05ea 100644 --- a/.changeset/itchy-otters-switch.md +++ b/.changeset/itchy-otters-switch.md @@ -4,3 +4,5 @@ --- Added support for dealing with user provided secrets using a new field extension `ui:field: Secret` + +The `@alpha` exports of `Stepper` now needs to be wrapped in a `SecretsContextProvider` for access to secrets. From 734c40811ceaa8f08a55ac67585b927b3946d514 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 19 Dec 2023 11:02:28 +0100 Subject: [PATCH 18/18] chore: update comment and api-reprots Signed-off-by: blam --- plugins/scaffolder-react/api-report-alpha.md | 6 ++++-- .../src/next/components/Workflow/Workflow.tsx | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-react/api-report-alpha.md b/plugins/scaffolder-react/api-report-alpha.md index 615b54df84..dc698e0c51 100644 --- a/plugins/scaffolder-react/api-report-alpha.md +++ b/plugins/scaffolder-react/api-report-alpha.md @@ -64,8 +64,10 @@ export const DefaultTemplateOutputs: (props: { output?: ScaffolderTaskOutput; }) => React_2.JSX.Element | null; -// @alpha (undocumented) -export const EmbeddableWorkflow: (props: WorkflowProps) => React_2.JSX.Element; +// @alpha +export const EmbeddableWorkflow: ( + workflowProps: WorkflowProps, +) => JSX.Element | null; // @alpha export const extractSchemaFromStep: (inputStep: JsonObject) => { diff --git a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx index c2ea544463..4a3758cfa9 100644 --- a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx +++ b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx @@ -126,7 +126,7 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => { /** * TODO(blam): work out what we want to do with these components in the new API. - * Should we really have EmbeddableWorkflow -> Workflow -> Stepper -> Form, or should we revisit this? + * Should we really have EmbeddableWorkflow, Workflow, Stepper and Form, or should we revisit this? * @alpha */ export const EmbeddableWorkflow = Workflow;