From 906dc9223c8897f5e5f877f87a528c2a327ef739 Mon Sep 17 00:00:00 2001 From: Zander Franks Date: Wed, 3 May 2023 17:35:16 -0500 Subject: [PATCH 1/8] feat(scaffolder): support text outputs from templates Signed-off-by: Zander Franks --- .../src/Template.v1beta3.schema.json | 27 +++++++++ plugins/scaffolder-react/src/api/types.ts | 8 +++ .../DefaultTemplateOutputs.tsx | 52 +++++++++++++---- .../TemplateOutputs/TextOutputs.tsx | 58 +++++++++++++++++++ 4 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx diff --git a/plugins/scaffolder-common/src/Template.v1beta3.schema.json b/plugins/scaffolder-common/src/Template.v1beta3.schema.json index beb0ca5e31..beb86f43cb 100644 --- a/plugins/scaffolder-common/src/Template.v1beta3.schema.json +++ b/plugins/scaffolder-common/src/Template.v1beta3.schema.json @@ -218,6 +218,33 @@ } } } + }, + "text": { + "type": "array", + "description": "A list of text blobs, like output data from the template.", + "items": { + "type": "object", + "required": [], + "properties": { + "title": { + "type": "string", + "description": "A user friendly display name for the text blob.", + "examples": ["Output Data"], + "minLength": 1 + }, + "icon": { + "type": "string", + "description": "A key representing a visual icon to be displayed in the UI.", + "examples": ["dashboard"], + "minLength": 1 + }, + "data": { + "type": "string", + "description": "The text blob to display in the UI.", + "examples": ["{}"] + } + } + } } }, "additionalProperties": { diff --git a/plugins/scaffolder-react/src/api/types.ts b/plugins/scaffolder-react/src/api/types.ts index b39555c14f..64bb0d05ac 100644 --- a/plugins/scaffolder-react/src/api/types.ts +++ b/plugins/scaffolder-react/src/api/types.ts @@ -84,9 +84,17 @@ export type ScaffolderOutputLink = { entityRef?: string; }; +/** @public */ +export type ScaffolderOutputText = { + title?: string; + icon?: string; + data?: string; +}; + /** @public */ export type ScaffolderTaskOutput = { links?: ScaffolderOutputLink[]; + text?: ScaffolderOutputText[]; } & { [key: string]: unknown; }; diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx index 4d908461b0..de52b62700 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { Box, Paper } from '@material-ui/core'; -import { LinkOutputs } from './LinkOutputs'; +import { InfoCard, MarkdownContent } from '@backstage/core-components'; import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; +import { Box, Paper } from '@material-ui/core'; +import React, { useMemo, useState } from 'react'; +import { LinkOutputs } from './LinkOutputs'; +import { TextOutputs } from './TextOutputs'; /** * The DefaultOutputs renderer for the scaffolder task output @@ -26,17 +28,47 @@ import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; export const DefaultTemplateOutputs = (props: { output?: ScaffolderTaskOutput; }) => { - if (!props.output?.links) { + const [textOutputIndex, setTextOutputIndex] = useState(); + + const textOutput = useMemo( + () => + textOutputIndex !== undefined + ? props.output?.text?.[textOutputIndex] + : null, + [props.output, textOutputIndex], + ); + + if (!props.output) { return null; } return ( - - - - + <> + + + + + + + + + {textOutput ? ( + + + + + + - - + ) : null} + ); }; diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx new file mode 100644 index 0000000000..8600a281a0 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx @@ -0,0 +1,58 @@ +/* + * 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 { IconComponent, useApp } from '@backstage/core-plugin-api'; +import { Button } from '@material-ui/core'; +import WebIcon from '@material-ui/icons/Web'; +import React from 'react'; +import { ScaffolderTaskOutput } from '../../../api'; + +export const TextOutputs = (props: { + output: ScaffolderTaskOutput; + index?: number; + setIndex?: (index: number | undefined) => void; +}) => { + const { + output: { text = [] }, + index, + setIndex, + } = props; + + const app = useApp(); + + const iconResolver = (key?: string): IconComponent => + app.getSystemIcon(key!) ?? WebIcon; + + return ( + <> + {text + .filter(({ data }) => data) + .map(({ title, icon }, i) => { + const Icon = iconResolver(icon); + return ( + + ); + })} + + ); +}; From 82e10a6939c2a9fbf641393598972d30d634f57d Mon Sep 17 00:00:00 2001 From: Zander Franks Date: Wed, 3 May 2023 17:42:07 -0500 Subject: [PATCH 2/8] chore: add changesets Signed-off-by: Zander Franks --- .changeset/heavy-penguins-burn.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/heavy-penguins-burn.md diff --git a/.changeset/heavy-penguins-burn.md b/.changeset/heavy-penguins-burn.md new file mode 100644 index 0000000000..f9fba9bfb8 --- /dev/null +++ b/.changeset/heavy-penguins-burn.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-common': patch +'@backstage/plugin-scaffolder-react': patch +--- + +Add support for Markdown text blob outputs from templates From d2727a2eb0024e9e9c7d8bffa0edee1517b1fefc Mon Sep 17 00:00:00 2001 From: Zander Franks Date: Wed, 3 May 2023 17:57:32 -0500 Subject: [PATCH 3/8] chore: add docs Signed-off-by: Zander Franks --- docs/features/software-templates/writing-templates.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 15e6a833cc..da8e54875b 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -510,10 +510,8 @@ take a look at, or you can Each individual step can output some variables that can be used in the scaffolder frontend for after the job is finished. This is useful for things -like linking to the entity that has been created with the backend, and also -linking to the created repository. - -The main two that are used are the following: +like linking to the entity that has been created with the backend, linking +to the created repository, or showing Markdown text blobs. ```yaml output: @@ -523,6 +521,10 @@ output: - title: Open in catalog icon: catalog entityRef: ${{ steps['register'].output.entityRef }} # link to the entity that has been ingested to the catalog + text: + - title: More data + data: | + Access the [remote repository](${{ steps['publish'].output.remoteUrl }}). ``` ## The templating syntax From 8c444f37d8156350673c9a670416a5ca556469de Mon Sep 17 00:00:00 2001 From: Zander Franks Date: Wed, 3 May 2023 18:19:09 -0500 Subject: [PATCH 4/8] chore: scaffolder api report Signed-off-by: Zander Franks --- plugins/scaffolder-react/api-report.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md index baf5afb312..a2a7dca3b2 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -217,6 +217,13 @@ export type ScaffolderOutputLink = { entityRef?: string; }; +// @public (undocumented) +export type ScaffolderOutputText = { + title?: string; + icon?: string; + data?: string; +}; + // @public export interface ScaffolderScaffoldOptions { // (undocumented) @@ -261,6 +268,7 @@ export type ScaffolderTask = { // @public (undocumented) export type ScaffolderTaskOutput = { links?: ScaffolderOutputLink[]; + text?: ScaffolderOutputText[]; } & { [key: string]: unknown; }; From ebf3c0e7a1f7249063d2cd7f2924c0ed901885aa Mon Sep 17 00:00:00 2001 From: Zander Franks Date: Wed, 3 May 2023 18:39:09 -0500 Subject: [PATCH 5/8] chore: fix docs Markdown example link Signed-off-by: Zander Franks --- docs/features/software-templates/writing-templates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index da8e54875b..2508f4a592 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -524,7 +524,7 @@ output: text: - title: More data data: | - Access the [remote repository](${{ steps['publish'].output.remoteUrl }}). + Access the \[remote repository\](${{ steps['publish'].output.remoteUrl }}). ``` ## The templating syntax From b779e9875664ee2e966b13d51d91cb615564a8c4 Mon Sep 17 00:00:00 2001 From: Zander Franks Date: Thu, 4 May 2023 12:37:35 -0500 Subject: [PATCH 6/8] fix(scaffolder): requested changes Signed-off-by: Zander Franks --- .changeset/heavy-penguins-burn.md | 4 ++-- .../features/software-templates/writing-templates.md | 4 ++-- .../src/Template.v1beta3.schema.json | 12 ++++++------ plugins/scaffolder-react/api-report.md | 2 +- plugins/scaffolder-react/src/api/types.ts | 2 +- .../TemplateOutputs/DefaultTemplateOutputs.tsx | 2 +- .../next/components/TemplateOutputs/TextOutputs.tsx | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.changeset/heavy-penguins-burn.md b/.changeset/heavy-penguins-burn.md index f9fba9bfb8..b8c243a99b 100644 --- a/.changeset/heavy-penguins-burn.md +++ b/.changeset/heavy-penguins-burn.md @@ -1,6 +1,6 @@ --- -'@backstage/plugin-scaffolder-common': patch -'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-scaffolder-common': minor +'@backstage/plugin-scaffolder-react': minor --- Add support for Markdown text blob outputs from templates diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 2508f4a592..44bd5e5928 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -522,8 +522,8 @@ output: icon: catalog entityRef: ${{ steps['register'].output.entityRef }} # link to the entity that has been ingested to the catalog text: - - title: More data - data: | + - title: More information + content: | Access the \[remote repository\](${{ steps['publish'].output.remoteUrl }}). ``` diff --git a/plugins/scaffolder-common/src/Template.v1beta3.schema.json b/plugins/scaffolder-common/src/Template.v1beta3.schema.json index beb86f43cb..1de68205b2 100644 --- a/plugins/scaffolder-common/src/Template.v1beta3.schema.json +++ b/plugins/scaffolder-common/src/Template.v1beta3.schema.json @@ -221,15 +221,15 @@ }, "text": { "type": "array", - "description": "A list of text blobs, like output data from the template.", + "description": "A list of Markdown text blobs, like output data from the template.", "items": { "type": "object", "required": [], "properties": { "title": { "type": "string", - "description": "A user friendly display name for the text blob.", - "examples": ["Output Data"], + "description": "A user friendly display name for the text.", + "examples": ["Output Content"], "minLength": 1 }, "icon": { @@ -238,10 +238,10 @@ "examples": ["dashboard"], "minLength": 1 }, - "data": { + "content": { "type": "string", - "description": "The text blob to display in the UI.", - "examples": ["{}"] + "description": "The text blob to display in the UI, rendered as Markdown.", + "examples": ["**hey** _I'm_ Markdown"] } } } diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md index a2a7dca3b2..1761c88edf 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -221,7 +221,7 @@ export type ScaffolderOutputLink = { export type ScaffolderOutputText = { title?: string; icon?: string; - data?: string; + content?: string; }; // @public diff --git a/plugins/scaffolder-react/src/api/types.ts b/plugins/scaffolder-react/src/api/types.ts index 64bb0d05ac..2adecdd0d2 100644 --- a/plugins/scaffolder-react/src/api/types.ts +++ b/plugins/scaffolder-react/src/api/types.ts @@ -88,7 +88,7 @@ export type ScaffolderOutputLink = { export type ScaffolderOutputText = { title?: string; icon?: string; - data?: string; + content?: string; }; /** @public */ diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx index de52b62700..a495e4ca9e 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx @@ -64,7 +64,7 @@ export const DefaultTemplateOutputs = (props: { titleTypographyProps={{ component: 'h2' }} > - + diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx index 8600a281a0..4a9110d79c 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx @@ -38,7 +38,7 @@ export const TextOutputs = (props: { return ( <> {text - .filter(({ data }) => data) + .filter(({ content }) => content !== undefined) .map(({ title, icon }, i) => { const Icon = iconResolver(icon); return ( From 977aa9130023c003b29286950bde0e02d7c4c055 Mon Sep 17 00:00:00 2001 From: Zander Date: Fri, 5 May 2023 11:55:25 -0500 Subject: [PATCH 7/8] feat(scaffolder): DefaultTemplateOutputs test Signed-off-by: Zander --- .../DefaultTemplateOutputs.test.tsx | 56 +++++++++++++++++++ .../TemplateOutputs/TextOutputs.tsx | 1 + 2 files changed, 57 insertions(+) create mode 100644 plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx new file mode 100644 index 0000000000..8366dcadf8 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx @@ -0,0 +1,56 @@ +/* + * 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 { entityRouteRef } from '@backstage/plugin-catalog-react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { fireEvent } from '@testing-library/react'; +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { DefaultTemplateOutputs } from '.'; + +describe('', () => { + it('should render template output', async () => { + const output = { + links: [{ title: 'Link 1', url: 'https://backstage.io/' }], + text: [{ title: 'Text 1', content: 'Hello, **world**!' }], + }; + + const { getByRole } = await renderInTestApp( + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + // test link outputs + for (const link of output.links ?? []) { + expect( + getByRole('button', { name: link.title }).closest('a'), + ).toHaveAttribute('href', link.url); + } + + // test text outputs + for (const text of output.text ?? []) { + await act(async () => { + fireEvent.click(getByRole('button', { name: text.title })); + }); + + expect(getByRole('heading', { level: 2 }).innerHTML).toBe(text.title); + } + }); +}); diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx index 4a9110d79c..7e64603e92 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx @@ -43,6 +43,7 @@ export const TextOutputs = (props: { const Icon = iconResolver(icon); return (