Merge pull request #15162 from dagda1/workflow-execution-poc
Embed Scaffolder create flow into Catalog entity page
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': minor
|
||||
'@backstage/plugin-scaffolder-react': minor
|
||||
---
|
||||
|
||||
Embed scaffolder workflow in other components
|
||||
@@ -21,6 +21,7 @@ import { JsonValue } from '@backstage/types';
|
||||
import { Observable } from '@backstage/types';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { SetStateAction } from 'react';
|
||||
import { TaskSpec } from '@backstage/plugin-scaffolder-common';
|
||||
import { TaskStep } from '@backstage/plugin-scaffolder-common';
|
||||
@@ -79,6 +80,9 @@ export type CustomFieldValidator<TFieldReturnValue> = (
|
||||
},
|
||||
) => void | Promise<void>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const EmbeddableWorkflow: (props: WorkflowProps) => JSX.Element;
|
||||
|
||||
// @alpha
|
||||
export const extractSchemaFromStep: (inputStep: JsonObject) => {
|
||||
uiSchema: UiSchema;
|
||||
@@ -342,7 +346,7 @@ export const SecretsContextProvider: ({
|
||||
}: PropsWithChildren<{}>) => JSX.Element;
|
||||
|
||||
// @alpha
|
||||
export const Stepper: (props: StepperProps) => JSX.Element;
|
||||
export const Stepper: (stepperProps: StepperProps) => JSX.Element;
|
||||
|
||||
// @alpha
|
||||
export type StepperProps = {
|
||||
@@ -351,7 +355,12 @@ export type StepperProps = {
|
||||
templateName?: string;
|
||||
FormProps?: FormProps;
|
||||
initialState?: Record<string, JsonValue>;
|
||||
onComplete: (values: Record<string, JsonValue>) => Promise<void>;
|
||||
onCreate: (values: Record<string, JsonValue>) => Promise<void>;
|
||||
components?: {
|
||||
ReviewStateComponent?: (props: ReviewStateProps) => JSX.Element;
|
||||
createButtonText?: ReactNode;
|
||||
reviewButtonText?: ReactNode;
|
||||
};
|
||||
};
|
||||
|
||||
// @alpha
|
||||
@@ -418,6 +427,13 @@ export const useFormDataFromQuery: (
|
||||
initialState?: Record<string, JsonValue>,
|
||||
) => [Record<string, any>, Dispatch<SetStateAction<Record<string, any>>>];
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const useTemplateParameterSchema: (templateRef: string) => {
|
||||
manifest: TemplateParameterSchema | undefined;
|
||||
loading: boolean;
|
||||
error: Error | undefined;
|
||||
};
|
||||
|
||||
// @alpha
|
||||
export const useTemplateSchema: (manifest: TemplateParameterSchema) => {
|
||||
steps: ParsedTemplateSchema[];
|
||||
@@ -426,5 +442,20 @@ export const useTemplateSchema: (manifest: TemplateParameterSchema) => {
|
||||
// @public
|
||||
export const useTemplateSecrets: () => ScaffolderUseTemplateSecrets;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const Workflow: (workflowProps: WorkflowProps) => JSX.Element | null;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type WorkflowProps = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
namespace: string;
|
||||
templateName: string;
|
||||
onError(error: Error | undefined): JSX.Element | null;
|
||||
} & Pick<
|
||||
StepperProps,
|
||||
'extensions' | 'FormProps' | 'components' | 'onCreate' | 'initialState'
|
||||
>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"json-schema-library": "^7.3.9",
|
||||
"lodash": "^4.17.21",
|
||||
"qs": "^6.9.4",
|
||||
"react-use": "^17.2.4",
|
||||
"zen-observable": "^0.10.0",
|
||||
"zod": "~3.18.0",
|
||||
"zod-to-json-schema": "~3.18.0"
|
||||
|
||||
@@ -21,3 +21,4 @@ export * from './api';
|
||||
export * from './hooks';
|
||||
|
||||
export * from './next';
|
||||
export type { FormProps } from './next';
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('Stepper', () => {
|
||||
};
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<Stepper manifest={manifest} extensions={[]} onComplete={jest.fn()} />,
|
||||
<Stepper manifest={manifest} extensions={[]} onCreate={jest.fn()} />,
|
||||
);
|
||||
|
||||
for (const step of manifest.steps) {
|
||||
@@ -51,7 +51,7 @@ describe('Stepper', () => {
|
||||
};
|
||||
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<Stepper manifest={manifest} extensions={[]} onComplete={jest.fn()} />,
|
||||
<Stepper manifest={manifest} extensions={[]} onCreate={jest.fn()} />,
|
||||
);
|
||||
|
||||
expect(getByRole('button', { name: 'Next' })).toBeInTheDocument();
|
||||
@@ -91,7 +91,7 @@ describe('Stepper', () => {
|
||||
};
|
||||
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<Stepper manifest={manifest} extensions={[]} onComplete={jest.fn()} />,
|
||||
<Stepper manifest={manifest} extensions={[]} onCreate={jest.fn()} />,
|
||||
);
|
||||
|
||||
await fireEvent.change(getByRole('textbox', { name: 'name' }), {
|
||||
@@ -162,7 +162,7 @@ describe('Stepper', () => {
|
||||
title: 'React JSON Schema Form Test',
|
||||
};
|
||||
|
||||
const onComplete = jest.fn(async (values: Record<string, JsonValue>) => {
|
||||
const onCreate = jest.fn(async (values: Record<string, JsonValue>) => {
|
||||
expect(values).toEqual({
|
||||
first: { repository: 'Repo' },
|
||||
second: { owner: 'Owner' },
|
||||
@@ -172,7 +172,7 @@ describe('Stepper', () => {
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<Stepper
|
||||
manifest={manifest}
|
||||
onComplete={onComplete}
|
||||
onCreate={onCreate}
|
||||
extensions={[
|
||||
{ name: 'Repo', component: Repo },
|
||||
{ name: 'Owner', component: Owner },
|
||||
@@ -200,7 +200,7 @@ describe('Stepper', () => {
|
||||
await fireEvent.click(getByRole('button', { name: 'Create' }));
|
||||
});
|
||||
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
expect(onCreate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should render custom field extensions properly', async () => {
|
||||
@@ -229,7 +229,7 @@ describe('Stepper', () => {
|
||||
<Stepper
|
||||
manifest={manifest}
|
||||
extensions={[{ name: 'Mock', component: MockComponent }]}
|
||||
onComplete={jest.fn()}
|
||||
onCreate={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -266,7 +266,7 @@ describe('Stepper', () => {
|
||||
<Stepper
|
||||
manifest={manifest}
|
||||
extensions={[]}
|
||||
onComplete={jest.fn()}
|
||||
onCreate={jest.fn()}
|
||||
FormProps={{ transformErrors }}
|
||||
/>,
|
||||
);
|
||||
@@ -308,7 +308,7 @@ describe('Stepper', () => {
|
||||
});
|
||||
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<Stepper manifest={manifest} extensions={[]} onComplete={jest.fn()} />,
|
||||
<Stepper manifest={manifest} extensions={[]} onCreate={jest.fn()} />,
|
||||
);
|
||||
|
||||
expect(getByRole('textbox', { name: 'firstName' })).toHaveValue('John');
|
||||
@@ -331,12 +331,12 @@ describe('Stepper', () => {
|
||||
title: 'initialize formData',
|
||||
};
|
||||
|
||||
const onComplete = jest.fn(async (values: Record<string, JsonValue>) => {
|
||||
const onCreate = jest.fn(async (values: Record<string, JsonValue>) => {
|
||||
expect(values).toHaveProperty('firstName');
|
||||
});
|
||||
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<Stepper manifest={manifest} extensions={[]} onComplete={onComplete} />,
|
||||
<Stepper manifest={manifest} extensions={[]} onCreate={onCreate} />,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
@@ -352,4 +352,44 @@ describe('Stepper', () => {
|
||||
// flush promises
|
||||
return new Promise(process.nextTick);
|
||||
});
|
||||
|
||||
it('should override the Create and Review button text', async () => {
|
||||
const manifest: TemplateParameterSchema = {
|
||||
title: 'Custom Fields',
|
||||
steps: [
|
||||
{
|
||||
title: 'Test',
|
||||
schema: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<Stepper
|
||||
manifest={manifest}
|
||||
onCreate={jest.fn()}
|
||||
extensions={[]}
|
||||
components={{
|
||||
createButtonText: <b>Make</b>,
|
||||
reviewButtonText: <i>Inspect</i>,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await fireEvent.click(getByRole('button', { name: 'Inspect' }));
|
||||
});
|
||||
|
||||
expect(getByRole('button', { name: 'Make' })).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
await fireEvent.click(getByRole('button', { name: 'Make' }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,17 +24,16 @@ import {
|
||||
} from '@material-ui/core';
|
||||
import { type IChangeEvent, withTheme } from '@rjsf/core-v5';
|
||||
import { ErrorSchema, FieldValidation } from '@rjsf/utils';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState, type ReactNode } from 'react';
|
||||
import { NextFieldExtensionOptions } from '../../extensions';
|
||||
import { TemplateParameterSchema } from '../../../types';
|
||||
import { createAsyncValidators } from './createAsyncValidators';
|
||||
import type { FormProps } from '../../types';
|
||||
import { ReviewState, type ReviewStateProps } from '../ReviewState';
|
||||
import { useTemplateSchema } from '../../hooks/useTemplateSchema';
|
||||
import { ReviewState } from '../ReviewState';
|
||||
import { useFormDataFromQuery } from '../../hooks/useFormDataFromQuery';
|
||||
import validator from '@rjsf/validator-ajv6';
|
||||
|
||||
import { useFormDataFromQuery } from '../../hooks';
|
||||
import { FormProps } from '../../types';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
backButton: {
|
||||
marginRight: theme.spacing(1),
|
||||
@@ -60,8 +59,12 @@ export type StepperProps = {
|
||||
templateName?: string;
|
||||
FormProps?: FormProps;
|
||||
initialState?: Record<string, JsonValue>;
|
||||
|
||||
onComplete: (values: Record<string, JsonValue>) => Promise<void>;
|
||||
onCreate: (values: Record<string, JsonValue>) => Promise<void>;
|
||||
components?: {
|
||||
ReviewStateComponent?: (props: ReviewStateProps) => JSX.Element;
|
||||
createButtonText?: ReactNode;
|
||||
reviewButtonText?: ReactNode;
|
||||
};
|
||||
};
|
||||
|
||||
// TODO(blam): We require here, as the types in this package depend on @rjsf/core explicitly
|
||||
@@ -73,7 +76,15 @@ const Form = withTheme(require('@rjsf/material-ui-v5').Theme);
|
||||
* The `Stepper` component is the Wizard that is rendered when a user selects a template
|
||||
* @alpha
|
||||
*/
|
||||
export const Stepper = (props: StepperProps) => {
|
||||
|
||||
export const Stepper = (stepperProps: StepperProps) => {
|
||||
const { components = {}, ...props } = stepperProps;
|
||||
const {
|
||||
ReviewStateComponent = ReviewState,
|
||||
createButtonText = 'Create',
|
||||
reviewButtonText = 'Review',
|
||||
} = components;
|
||||
|
||||
const analytics = useAnalytics();
|
||||
const { steps } = useTemplateSchema(props.manifest);
|
||||
const apiHolder = useApiHolder();
|
||||
@@ -177,13 +188,13 @@ export const Stepper = (props: StepperProps) => {
|
||||
Back
|
||||
</Button>
|
||||
<Button variant="contained" color="primary" type="submit">
|
||||
{activeStep === steps.length - 1 ? 'Review' : 'Next'}
|
||||
{activeStep === steps.length - 1 ? reviewButtonText : 'Next'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
) : (
|
||||
<>
|
||||
<ReviewState formState={formState} schemas={steps} />
|
||||
<ReviewStateComponent formState={formState} schemas={steps} />
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
onClick={handleBack}
|
||||
@@ -195,7 +206,7 @@ export const Stepper = (props: StepperProps) => {
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
props.onComplete(formState);
|
||||
props.onCreate(formState);
|
||||
const name =
|
||||
typeof formState.name === 'string'
|
||||
? formState.name
|
||||
@@ -206,7 +217,7 @@ export const Stepper = (props: StepperProps) => {
|
||||
);
|
||||
}}
|
||||
>
|
||||
Create
|
||||
{createButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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 { ApiProvider } from '@backstage/core-app-api';
|
||||
import {
|
||||
MockAnalyticsApi,
|
||||
renderInTestApp,
|
||||
TestApiRegistry,
|
||||
} from '@backstage/test-utils';
|
||||
import { act, fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Workflow } from './Workflow';
|
||||
import { analyticsApiRef } from '@backstage/core-plugin-api';
|
||||
import { ScaffolderApi } from '../../../api/types';
|
||||
import { scaffolderApiRef } from '../../../api/ref';
|
||||
|
||||
const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
|
||||
scaffold: jest.fn(),
|
||||
getTemplateParameterSchema: jest.fn(),
|
||||
getIntegrationsList: jest.fn(),
|
||||
getTask: jest.fn(),
|
||||
streamLogs: jest.fn(),
|
||||
listActions: jest.fn(),
|
||||
listTasks: jest.fn(),
|
||||
};
|
||||
|
||||
const analyticsMock = new MockAnalyticsApi();
|
||||
const apis = TestApiRegistry.from(
|
||||
[scaffolderApiRef, scaffolderApiMock],
|
||||
[analyticsApiRef, analyticsMock],
|
||||
);
|
||||
|
||||
describe('<Workflow />', () => {
|
||||
it('should complete a workflow', async () => {
|
||||
const onCreate = jest.fn();
|
||||
const onError = jest.fn();
|
||||
scaffolderApiMock.scaffold.mockResolvedValue({ taskId: 'xyz' });
|
||||
|
||||
scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({
|
||||
steps: [
|
||||
{
|
||||
title: 'Step 1',
|
||||
schema: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Step 2',
|
||||
schema: {
|
||||
properties: {
|
||||
age: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
title: 'React JSON Schema Form Test',
|
||||
});
|
||||
|
||||
const { getByRole, getAllByRole, getByText } = await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<Workflow
|
||||
title="Different title than template"
|
||||
description={`
|
||||
## This is markdown
|
||||
- overriding the template description
|
||||
`}
|
||||
onCreate={onCreate}
|
||||
onError={onError}
|
||||
namespace="default"
|
||||
templateName="docs-template"
|
||||
initialState={{
|
||||
name: 'prefilled-name',
|
||||
age: '53',
|
||||
}}
|
||||
components={{
|
||||
ReviewStateComponent: () => (
|
||||
<h1>This is a different wrapper for the review page</h1>
|
||||
),
|
||||
reviewButtonText: <i>Onwards</i>,
|
||||
createButtonText: <b>Make</b>,
|
||||
}}
|
||||
extensions={[]}
|
||||
/>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
// Test template title is overriden
|
||||
expect(getByRole('heading', { level: 2 }).innerHTML).toBe(
|
||||
'Different title than template',
|
||||
);
|
||||
|
||||
const nameInput = getByRole('textbox', {
|
||||
name: 'name',
|
||||
}) as HTMLInputElement;
|
||||
|
||||
expect(nameInput).toBeInTheDocument();
|
||||
|
||||
expect(nameInput.value).toBe('prefilled-name');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(getByRole('button', { name: 'Next' }));
|
||||
});
|
||||
|
||||
const ageInput = getByRole('textbox', { name: 'age' }) as HTMLInputElement;
|
||||
|
||||
expect(ageInput.value).toBe('53');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(getByRole('button', { name: 'Onwards' }));
|
||||
});
|
||||
|
||||
expect(
|
||||
getByText('This is a different wrapper for the review page'),
|
||||
).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(getAllByRole('button')[1] as HTMLButtonElement);
|
||||
});
|
||||
|
||||
expect(onCreate).toHaveBeenCalledWith({
|
||||
name: 'prefilled-name',
|
||||
age: '53',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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, { useEffect } from 'react';
|
||||
import {
|
||||
Content,
|
||||
InfoCard,
|
||||
MarkdownContent,
|
||||
Progress,
|
||||
} from '@backstage/core-components';
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { useTemplateParameterSchema } from '../../hooks/useTemplateParameterSchema';
|
||||
import { Stepper, type StepperProps } from '../Stepper/Stepper';
|
||||
import { SecretsContextProvider } from '../../../secrets/SecretsContext';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(() => ({
|
||||
markdown: {
|
||||
/** to make the styles for React Markdown not leak into the description */
|
||||
'& :first-child': {
|
||||
marginTop: 0,
|
||||
},
|
||||
'& :last-child': {
|
||||
marginBottom: 0,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export type WorkflowProps = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
namespace: string;
|
||||
templateName: string;
|
||||
onError(error: Error | undefined): JSX.Element | null;
|
||||
} & Pick<
|
||||
StepperProps,
|
||||
'extensions' | 'FormProps' | 'components' | 'onCreate' | 'initialState'
|
||||
>;
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => {
|
||||
const { title, description, namespace, templateName, ...props } =
|
||||
workflowProps;
|
||||
|
||||
const styles = useStyles();
|
||||
const templateRef = stringifyEntityRef({
|
||||
kind: 'Template',
|
||||
namespace: namespace,
|
||||
name: templateName,
|
||||
});
|
||||
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const { loading, manifest, error } = useTemplateParameterSchema(templateRef);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
errorApi.post(new Error(`Failed to load template, ${error}`));
|
||||
}
|
||||
}, [error, errorApi]);
|
||||
|
||||
if (error) {
|
||||
return props.onError(error);
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
{loading && <Progress />}
|
||||
{manifest && (
|
||||
<InfoCard
|
||||
title={title ?? manifest.title}
|
||||
subheader={
|
||||
<MarkdownContent
|
||||
className={styles.markdown}
|
||||
content={description ?? manifest.description ?? 'No description'}
|
||||
/>
|
||||
}
|
||||
noPadding
|
||||
titleTypographyProps={{ component: 'h2' }}
|
||||
>
|
||||
<Stepper manifest={manifest} {...props} />
|
||||
</InfoCard>
|
||||
)}
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export const EmbeddableWorkflow = (props: WorkflowProps) => (
|
||||
<SecretsContextProvider>
|
||||
<Workflow {...props} />
|
||||
</SecretsContextProvider>
|
||||
);
|
||||
@@ -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 { Workflow, EmbeddableWorkflow, type WorkflowProps } from './Workflow';
|
||||
@@ -17,3 +17,4 @@ export * from './Stepper';
|
||||
export * from './TemplateCard';
|
||||
export * from './ReviewState';
|
||||
export * from './TemplateGroup';
|
||||
export * from './Workflow';
|
||||
|
||||
@@ -18,3 +18,4 @@ export {
|
||||
useTemplateSchema,
|
||||
type ParsedTemplateSchema,
|
||||
} from './useTemplateSchema';
|
||||
export { useTemplateParameterSchema } from './useTemplateParameterSchema';
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
/*
|
||||
* 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 useAsync from 'react-use/lib/useAsync';
|
||||
import { scaffolderApiRef } from '../../api/ref';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export const useTemplateParameterSchema = (templateRef: string) => {
|
||||
const scaffolderApi = useApi(scaffolderApiRef);
|
||||
const { value, loading, error } = useAsync(
|
||||
() => scaffolderApi.getTemplateParameterSchema(templateRef),
|
||||
[scaffolderApi, templateRef],
|
||||
);
|
||||
|
||||
return { manifest: value, loading, error };
|
||||
};
|
||||
@@ -15,6 +15,6 @@
|
||||
*/
|
||||
export * from './components';
|
||||
export * from './extensions';
|
||||
export * from './types';
|
||||
export type { FormProps } from './types';
|
||||
export * from './lib';
|
||||
export * from './hooks';
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups';
|
||||
import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../../extensions/default';
|
||||
import { FormProps } from '../types';
|
||||
import { type FormProps } from '../types';
|
||||
import { nextSelectedTemplateRouteRef } from '../routes';
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,20 +13,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Page,
|
||||
Header,
|
||||
Content,
|
||||
Progress,
|
||||
InfoCard,
|
||||
MarkdownContent,
|
||||
} from '@backstage/core-components';
|
||||
import React from 'react';
|
||||
import { Navigate, useNavigate } from 'react-router-dom';
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import {
|
||||
AnalyticsContext,
|
||||
errorApiRef,
|
||||
useApi,
|
||||
useRouteRef,
|
||||
useRouteRefParams,
|
||||
@@ -34,48 +25,21 @@ import {
|
||||
import {
|
||||
scaffolderApiRef,
|
||||
useTemplateSecrets,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import {
|
||||
Stepper,
|
||||
NextFieldExtensionOptions,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { FormProps } from '../types';
|
||||
import { type FormProps } from '../types';
|
||||
import { nextRouteRef } from '../routes';
|
||||
import { scaffolderTaskRouteRef, selectedTemplateRouteRef } from '../../routes';
|
||||
import { Header, Page } from '@backstage/core-components';
|
||||
import { Workflow } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
type TemplateWizardPageProps = {
|
||||
customFieldExtensions: NextFieldExtensionOptions<any, any>[];
|
||||
FormProps?: FormProps;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(() => ({
|
||||
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 rootRef = useRouteRef(nextRouteRef);
|
||||
const taskRoute = useRouteRef(scaffolderTaskRouteRef);
|
||||
const { secrets } = useTemplateSecrets();
|
||||
@@ -91,10 +55,7 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
|
||||
name: templateName,
|
||||
});
|
||||
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const { loading, manifest, error } = useTemplateParameterSchema(templateRef);
|
||||
|
||||
const onComplete = async (values: Record<string, JsonValue>) => {
|
||||
const onCreate = async (values: Record<string, JsonValue>) => {
|
||||
const { taskId } = await scaffolderApi.scaffold({
|
||||
templateRef,
|
||||
values,
|
||||
@@ -104,15 +65,7 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
|
||||
navigate(taskRoute({ taskId }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
errorApi.post(new Error(`Failed to load template, ${error}`));
|
||||
}
|
||||
}, [error, errorApi]);
|
||||
|
||||
if (error) {
|
||||
return <Navigate to={rootRef()} />;
|
||||
}
|
||||
const onError = () => <Navigate to={rootRef()} />;
|
||||
|
||||
return (
|
||||
<AnalyticsContext attributes={{ entityRef: templateRef }}>
|
||||
@@ -122,29 +75,14 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
|
||||
title="Create a new component"
|
||||
subtitle="Create new software components using standard templates in your organization"
|
||||
/>
|
||||
<Content>
|
||||
{loading && <Progress />}
|
||||
{manifest && (
|
||||
<InfoCard
|
||||
title={manifest.title}
|
||||
subheader={
|
||||
<MarkdownContent
|
||||
className={styles.markdown}
|
||||
content={manifest.description ?? 'No description'}
|
||||
/>
|
||||
}
|
||||
noPadding
|
||||
titleTypographyProps={{ component: 'h2' }}
|
||||
>
|
||||
<Stepper
|
||||
manifest={manifest}
|
||||
extensions={props.customFieldExtensions}
|
||||
onComplete={onComplete}
|
||||
FormProps={props.FormProps}
|
||||
/>
|
||||
</InfoCard>
|
||||
)}
|
||||
</Content>
|
||||
<Workflow
|
||||
namespace={namespace}
|
||||
templateName={templateName}
|
||||
onCreate={onCreate}
|
||||
onError={onError}
|
||||
extensions={props.customFieldExtensions}
|
||||
FormProps={props.FormProps}
|
||||
/>
|
||||
</Page>
|
||||
</AnalyticsContext>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user