feat(scaffolder): support updating template inputs of tasks

Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
Phil Kuang
2021-12-15 15:53:48 -05:00
parent f13228792e
commit d078377f67
11 changed files with 215 additions and 122 deletions
+7
View File
@@ -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
+8 -48
View File
@@ -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 };
```
@@ -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
*
+59
View File
@@ -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;
};
```
+74
View File
@@ -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;
+2
View File
@@ -30,3 +30,5 @@ export const templateEntityV1beta3Schema: JSONSchema = v1beta3Schema as Omit<
JSONSchema,
'examples'
>;
export * from './TaskSpec';
+1 -1
View File
@@ -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)
+1
View File
@@ -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",
@@ -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 (
<Page themeId="home">
<Header
@@ -297,6 +324,15 @@ export const TaskPage = () => {
{output && hasLinks(output) && (
<TaskPageLinks output={output} />
)}
<Button
className={classes.button}
onClick={handleStartOver}
disabled={!completed}
variant="contained"
color="primary"
>
Start Over
</Button>
</Paper>
</Grid>
<Grid item xs={9}>
@@ -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<string, any>,
);
const handleFormReset = () => setFormState({});
const handleChange = useCallback(
(e: IChangeEvent) => setFormState(e.formData),
+2 -11
View File
@@ -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;