diff --git a/.changeset/poor-zoos-invent.md b/.changeset/poor-zoos-invent.md new file mode 100644 index 0000000000..a1c87bd3ad --- /dev/null +++ b/.changeset/poor-zoos-invent.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-common': patch +--- + +Support navigating back to pre-filled templates to update inputs of scaffolder tasks for resubmission diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index b7c1a9146c..5c891b1802 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -25,7 +25,11 @@ import { Schema } from 'jsonschema'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmIntegrations } from '@backstage/integration'; import { SpawnOptionsWithoutStdio } from 'child_process'; +import { TaskSpec } from '@backstage/plugin-scaffolder-common'; +import { TaskSpecV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TaskSpecV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; +import { TemplateMetadata } from '@backstage/plugin-scaffolder-common'; import { UrlReader } from '@backstage/backend-common'; import { Writable } from 'stream'; @@ -432,52 +436,11 @@ export type TaskSecrets = { token: string | undefined; }; -// @public -export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3; +export { TaskSpec }; -// @public -export interface TaskSpecV1beta2 { - // (undocumented) - apiVersion: 'backstage.io/v1beta2'; - // (undocumented) - baseUrl?: string; - // (undocumented) - metadata?: TemplateMetadata; - // (undocumented) - output: { - [name: string]: string; - }; - // (undocumented) - steps: Array<{ - id: string; - name: string; - action: string; - input?: JsonObject; - if?: string | boolean; - }>; - // (undocumented) - values: JsonObject; -} +export { TaskSpecV1beta2 }; -// @public -export interface TaskSpecV1beta3 { - // (undocumented) - apiVersion: 'scaffolder.backstage.io/v1beta3'; - // (undocumented) - baseUrl?: string; - // (undocumented) - metadata?: TemplateMetadata; - // (undocumented) - output: { - [name: string]: JsonValue; - }; - // (undocumented) - parameters: JsonObject; - // Warning: (ae-forgotten-export) The symbol "TaskStep" needs to be exported by the entry point index.d.ts - // - // (undocumented) - steps: TaskStep[]; -} +export { TaskSpecV1beta3 }; // @public export interface TaskState { @@ -573,8 +536,5 @@ export class TemplateActionRegistry { ): void; } -// @public -export type TemplateMetadata = { - name: string; -}; +export { TemplateMetadata }; ``` diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index b492f8ad2c..1a69f28fb6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -15,6 +15,21 @@ */ import { JsonValue, JsonObject } from '@backstage/types'; +import { + TaskSpec, + TaskStep, + TemplateMetadata, + TaskSpecV1beta2, + TaskSpecV1beta3, +} from '@backstage/plugin-scaffolder-common'; + +export type { + TaskSpec, + TaskStep, + TemplateMetadata, + TaskSpecV1beta2, + TaskSpecV1beta3, +}; /** * Status @@ -69,64 +84,6 @@ export type SerializedTaskEvent = { createdAt: string; }; -/** - * TemplateMetadata - * - * @public - */ -export type TemplateMetadata = { - name: string; -}; - -/** - * TaskSpecV1beta2 - * - * @public - */ -export interface TaskSpecV1beta2 { - apiVersion: 'backstage.io/v1beta2'; - baseUrl?: string; - values: JsonObject; - steps: Array<{ - id: string; - name: string; - action: string; - input?: JsonObject; - if?: string | boolean; - }>; - output: { [name: string]: string }; - metadata?: TemplateMetadata; -} - -export interface TaskStep { - id: string; - name: string; - action: string; - input?: JsonObject; - if?: string | boolean; -} - -/** - * TaskSpecV1beta3 - * - * @public - */ -export interface TaskSpecV1beta3 { - apiVersion: 'scaffolder.backstage.io/v1beta3'; - baseUrl?: string; - parameters: JsonObject; - steps: TaskStep[]; - output: { [name: string]: JsonValue }; - metadata?: TemplateMetadata; -} - -/** - * TaskSpec - * - * @public - */ -export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3; - /** * TaskSecrets * diff --git a/plugins/scaffolder-common/api-report.md b/plugins/scaffolder-common/api-report.md index 8485e224b2..3bca8e74ed 100644 --- a/plugins/scaffolder-common/api-report.md +++ b/plugins/scaffolder-common/api-report.md @@ -6,6 +6,60 @@ import { Entity } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/types'; import { JSONSchema } from '@backstage/catalog-model'; +import { JsonValue } from '@backstage/types'; + +// @public +export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3; + +// @public +export interface TaskSpecV1beta2 { + // (undocumented) + apiVersion: 'backstage.io/v1beta2'; + // (undocumented) + baseUrl?: string; + // (undocumented) + metadata?: TemplateMetadata; + // (undocumented) + output: { + [name: string]: string; + }; + // (undocumented) + steps: TaskStep[]; + // (undocumented) + values: JsonObject; +} + +// @public +export interface TaskSpecV1beta3 { + // (undocumented) + apiVersion: 'scaffolder.backstage.io/v1beta3'; + // (undocumented) + baseUrl?: string; + // (undocumented) + metadata?: TemplateMetadata; + // (undocumented) + output: { + [name: string]: JsonValue; + }; + // (undocumented) + parameters: JsonObject; + // (undocumented) + steps: TaskStep[]; +} + +// @public +export interface TaskStep { + // (undocumented) + action: string; + // (undocumented) + id: string; + // (undocumented) + if?: string | boolean; + // (undocumented) + input?: JsonObject; + // (undocumented) + name: string; +} // @public (undocumented) export interface TemplateEntityV1beta3 extends Entity { @@ -33,4 +87,9 @@ export interface TemplateEntityV1beta3 extends Entity { // @public (undocumented) export const templateEntityV1beta3Schema: JSONSchema; + +// @public +export type TemplateMetadata = { + name: string; +}; ``` diff --git a/plugins/scaffolder-common/src/TaskSpec.ts b/plugins/scaffolder-common/src/TaskSpec.ts new file mode 100644 index 0000000000..8d8ccedbad --- /dev/null +++ b/plugins/scaffolder-common/src/TaskSpec.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2021 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 { JsonValue, JsonObject } from '@backstage/types'; + +/** + * TemplateMetadata + * + * @public + */ +export type TemplateMetadata = { + name: string; +}; + +/** + * TaskStep + * + * @public + */ +export interface TaskStep { + id: string; + name: string; + action: string; + input?: JsonObject; + if?: string | boolean; +} + +/** + * TaskSpecV1beta2 + * + * @public + */ +export interface TaskSpecV1beta2 { + apiVersion: 'backstage.io/v1beta2'; + baseUrl?: string; + values: JsonObject; + steps: TaskStep[]; + output: { [name: string]: string }; + metadata?: TemplateMetadata; +} + +/** + * TaskSpecV1beta3 + * + * @public + */ +export interface TaskSpecV1beta3 { + apiVersion: 'scaffolder.backstage.io/v1beta3'; + baseUrl?: string; + parameters: JsonObject; + steps: TaskStep[]; + output: { [name: string]: JsonValue }; + metadata?: TemplateMetadata; +} + +/** + * TaskSpec + * + * @public + */ +export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3; diff --git a/plugins/scaffolder-common/src/index.ts b/plugins/scaffolder-common/src/index.ts index 3b6fa71d7a..1197f48fed 100644 --- a/plugins/scaffolder-common/src/index.ts +++ b/plugins/scaffolder-common/src/index.ts @@ -30,3 +30,5 @@ export const templateEntityV1beta3Schema: JSONSchema = v1beta3Schema as Omit< JSONSchema, 'examples' >; + +export * from './TaskSpec'; diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 45419e82eb..4f6928e72d 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -21,12 +21,12 @@ import { IconButton } from '@material-ui/core'; import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { JSONSchema } from '@backstage/catalog-model'; -import { JsonValue } from '@backstage/types'; import { Observable } from '@backstage/types'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmIntegrationRegistry } from '@backstage/integration'; +import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; // Warning: (ae-missing-release-tag) "createScaffolderFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index b464e86724..2c9112a77a 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -40,6 +40,7 @@ "@backstage/integration": "^0.6.10", "@backstage/integration-react": "^0.1.16", "@backstage/plugin-catalog-react": "^0.6.8", + "@backstage/plugin-scaffolder-common": "^0.1.1", "@backstage/theme": "^0.2.14", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index 58ec3dfc02..b27f116b96 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -22,8 +22,10 @@ import { Page, LogViewer, } from '@backstage/core-components'; +import { useRouteRef } from '@backstage/core-plugin-api'; import { BackstageTheme } from '@backstage/theme'; import { + Button, CircularProgress, Paper, StepButton, @@ -40,9 +42,11 @@ import Check from '@material-ui/icons/Check'; import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; import classNames from 'classnames'; import { DateTime, Interval } from 'luxon'; +import qs from 'qs'; import React, { memo, useEffect, useMemo, useState } from 'react'; -import { useParams } from 'react-router'; +import { generatePath, useNavigate, useParams } from 'react-router'; import { useInterval } from 'react-use'; +import { rootRouteRef } from '../../routes'; import { Status, TaskOutput } from '../../types'; import { useTaskEventStream } from '../hooks/useEventStream'; import { TaskPageLinks } from './TaskPageLinks'; @@ -56,8 +60,8 @@ const useStyles = makeStyles((theme: Theme) => width: '100%', }, button: { - marginTop: theme.spacing(1), - marginRight: theme.spacing(1), + marginBottom: theme.spacing(2), + marginLeft: theme.spacing(2), }, actionsContainer: { marginBottom: theme.spacing(2), @@ -215,6 +219,9 @@ const hasLinks = ({ entityRef, remoteUrl, links = [] }: TaskOutput): boolean => !!(entityRef || remoteUrl || links.length > 0); export const TaskPage = () => { + const classes = useStyles(); + const navigate = useNavigate(); + const rootLink = useRouteRef(rootRouteRef); const [userSelectedStepId, setUserSelectedStepId] = useState< string | undefined >(undefined); @@ -266,6 +273,26 @@ export const TaskPage = () => { const { output } = taskStream; + const handleStartOver = () => { + if (!taskStream.task || !taskStream.task?.spec.metadata?.name) { + navigate(generatePath(rootLink())); + } + + const formData = + taskStream.task!.spec.apiVersion === 'backstage.io/v1beta2' + ? taskStream.task!.spec.values + : taskStream.task!.spec.parameters; + + navigate( + generatePath( + `${rootLink()}/templates/:templateName?${qs.stringify({ formData })}`, + { + templateName: taskStream.task!.spec.metadata!.name, + }, + ), + ); + }; + return (
{ {output && hasLinks(output) && ( )} + diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 3f70f4effd..e4d2cea04e 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -16,6 +16,7 @@ import { JsonObject, JsonValue } from '@backstage/types'; import { LinearProgress } from '@material-ui/core'; import { FormValidation, IChangeEvent } from '@rjsf/core'; +import qs from 'qs'; import React, { useCallback, useState } from 'react'; import { generatePath, Navigate, useNavigate } from 'react-router'; import { useParams } from 'react-router-dom'; @@ -120,7 +121,12 @@ export const TemplatePage = ({ const navigate = useNavigate(); const rootLink = useRouteRef(rootRouteRef); const { schema, loading, error } = useTemplateParameterSchema(templateName); - const [formState, setFormState] = useState({}); + const query = qs.parse(window.location.search, { + ignoreQueryPrefix: true, + }); + const [formState, setFormState] = useState( + (query.formData ?? {}) as Record, + ); const handleFormReset = () => setFormState({}); const handleChange = useCallback( (e: IChangeEvent) => setFormState(e.formData), diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index 5597082f22..291f680710 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { JSONSchema } from '@backstage/catalog-model'; -import { JsonValue } from '@backstage/types'; +import { TaskSpec } from '@backstage/plugin-scaffolder-common'; export type Status = 'open' | 'processing' | 'failed' | 'completed' | 'skipped'; export type JobStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; @@ -39,18 +39,9 @@ export type Stage = { endedAt?: string; }; -export type ScaffolderStep = { - id: string; - name: string; - action: string; - parameters?: { [name: string]: JsonValue }; -}; - export type ScaffolderTask = { id: string; - spec: { - steps: ScaffolderStep[]; - }; + spec: TaskSpec; status: 'failed' | 'completed' | 'processing' | 'open' | 'cancelled'; lastHeartbeatAt: string; createdAt: string;