Merge branch 'master' into techdocs/remove-warnings
Signed-off-by: Morgan Bentell <morgan.bentell@gmail.com>
This commit is contained in:
@@ -1,5 +1,67 @@
|
||||
# @backstage/plugin-scaffolder-react
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 8f4d13f21cf: Move `useTaskStream`, `TaskBorder`, `TaskLogStream` and `TaskSteps` into `scaffolder-react`.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 65454876fb2: Minor API report tweaks
|
||||
- 3c96e77b513: Make scaffolder adhere to page themes by using page `fontColor` consistently. If your theme overwrites template list or card headers, review those styles.
|
||||
- c8d78b9ae9d: fix bug with `hasErrors` returning false when dealing with empty objects
|
||||
- 9b8c374ace5: Remove timer for skipped steps in Scaffolder Next's TaskSteps
|
||||
- 44941fc97eb: scaffolder/next: Move the `uiSchema` to its own property in the validation `context` to align with component development and access of `ui:options`
|
||||
- d9893263ba9: scaffolder/next: Fix for steps without properties
|
||||
- 928a12a9b3e: Internal refactor of `/alpha` exports.
|
||||
- cc418d652a7: scaffolder/next: Added the ability to get the fields definition in the schema in the validation function
|
||||
- d4100d0ec42: Fix alignment bug for owners on `TemplateCard`
|
||||
- Updated dependencies
|
||||
- @backstage/catalog-client@1.4.0
|
||||
- @backstage/core-components@0.12.5
|
||||
- @backstage/plugin-catalog-react@1.4.0
|
||||
- @backstage/errors@1.1.5
|
||||
- @backstage/core-plugin-api@1.5.0
|
||||
- @backstage/catalog-model@1.2.1
|
||||
- @backstage/theme@0.2.18
|
||||
- @backstage/types@1.0.2
|
||||
- @backstage/version-bridge@1.0.3
|
||||
- @backstage/plugin-scaffolder-common@1.2.6
|
||||
|
||||
## 1.2.0-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 65454876fb2: Minor API report tweaks
|
||||
- 3c96e77b513: Make scaffolder adhere to page themes by using page `fontColor` consistently. If your theme overwrites template list or card headers, review those styles.
|
||||
- d9893263ba9: scaffolder/next: Fix for steps without properties
|
||||
- Updated dependencies
|
||||
- @backstage/core-components@0.12.5-next.2
|
||||
- @backstage/plugin-catalog-react@1.4.0-next.2
|
||||
- @backstage/core-plugin-api@1.5.0-next.2
|
||||
|
||||
## 1.2.0-next.1
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 8f4d13f21cf: Move `useTaskStream`, `TaskBorder`, `TaskLogStream` and `TaskSteps` into `scaffolder-react`.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 44941fc97eb: scaffolder/next: Move the `uiSchema` to its own property in the validation `context` to align with component development and access of `ui:options`
|
||||
- Updated dependencies
|
||||
- @backstage/core-components@0.12.5-next.1
|
||||
- @backstage/errors@1.1.5-next.0
|
||||
- @backstage/catalog-client@1.4.0-next.1
|
||||
- @backstage/core-plugin-api@1.4.1-next.1
|
||||
- @backstage/theme@0.2.18-next.0
|
||||
- @backstage/plugin-catalog-react@1.4.0-next.1
|
||||
- @backstage/catalog-model@1.2.1-next.1
|
||||
- @backstage/types@1.0.2
|
||||
- @backstage/version-bridge@1.0.3
|
||||
- @backstage/plugin-scaffolder-common@1.2.6-next.1
|
||||
|
||||
## 1.1.1-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -113,7 +113,7 @@ export type ListActionsResponse = Array<Action>;
|
||||
|
||||
// @public
|
||||
export type LogEvent = {
|
||||
type: 'log' | 'completion';
|
||||
type: 'log' | 'completion' | 'cancelled';
|
||||
body: {
|
||||
message: string;
|
||||
stepId?: string;
|
||||
@@ -126,6 +126,7 @@ export type LogEvent = {
|
||||
|
||||
// @public
|
||||
export interface ScaffolderApi {
|
||||
cancelTask(taskId: string): Promise<void>;
|
||||
// (undocumented)
|
||||
dryRun?(options: ScaffolderDryRunOptions): Promise<ScaffolderDryRunResponse>;
|
||||
// (undocumented)
|
||||
@@ -266,10 +267,11 @@ export type ScaffolderTaskOutput = {
|
||||
|
||||
// @public
|
||||
export type ScaffolderTaskStatus =
|
||||
| 'cancelled'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'open'
|
||||
| 'processing'
|
||||
| 'failed'
|
||||
| 'completed'
|
||||
| 'skipped';
|
||||
|
||||
// @public
|
||||
@@ -281,12 +283,13 @@ export interface ScaffolderUseTemplateSecrets {
|
||||
}
|
||||
|
||||
// @public
|
||||
export const SecretsContextProvider: ({
|
||||
children,
|
||||
}: PropsWithChildren<{}>) => JSX.Element;
|
||||
export const SecretsContextProvider: (
|
||||
props: PropsWithChildren<{}>,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type TaskStream = {
|
||||
cancelled: boolean;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
stepLogs: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/plugin-scaffolder-react",
|
||||
"description": "A frontend library that helps other Backstage plugins interact with the Scaffolder",
|
||||
"version": "1.1.1-next.0",
|
||||
"version": "1.2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -24,10 +24,11 @@ import { TemplateParameterSchema } from '../types';
|
||||
* @public
|
||||
*/
|
||||
export type ScaffolderTaskStatus =
|
||||
| 'cancelled'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'open'
|
||||
| 'processing'
|
||||
| 'failed'
|
||||
| 'completed'
|
||||
| 'skipped';
|
||||
|
||||
/**
|
||||
@@ -96,7 +97,7 @@ export type ScaffolderTaskOutput = {
|
||||
* @public
|
||||
*/
|
||||
export type LogEvent = {
|
||||
type: 'log' | 'completion';
|
||||
type: 'log' | 'completion' | 'cancelled';
|
||||
body: {
|
||||
message: string;
|
||||
stepId?: string;
|
||||
@@ -196,6 +197,13 @@ export interface ScaffolderApi {
|
||||
|
||||
getTask(taskId: string): Promise<ScaffolderTask>;
|
||||
|
||||
/**
|
||||
* Sends a signal to a task broker to cancel the running task by taskId.
|
||||
*
|
||||
* @param taskId - the id of the task
|
||||
*/
|
||||
cancelTask(taskId: string): Promise<void>;
|
||||
|
||||
listTasks?(options: {
|
||||
filterByOwnership: 'owned' | 'all';
|
||||
}): Promise<{ tasks: ScaffolderTask[] }>;
|
||||
|
||||
@@ -44,6 +44,7 @@ export type ScaffolderStep = {
|
||||
* @public
|
||||
*/
|
||||
export type TaskStream = {
|
||||
cancelled: boolean;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
stepLogs: { [stepId in string]: string[] };
|
||||
@@ -66,6 +67,7 @@ type ReducerLogEntry = {
|
||||
|
||||
type ReducerAction =
|
||||
| { type: 'INIT'; data: ScaffolderTask }
|
||||
| { type: 'CANCELLED' }
|
||||
| { type: 'LOGS'; data: ReducerLogEntry[] }
|
||||
| { type: 'COMPLETED'; data: ReducerLogEntry }
|
||||
| { type: 'ERROR'; data: Error };
|
||||
@@ -111,7 +113,7 @@ function reducer(draft: TaskStream, action: ReducerAction) {
|
||||
}
|
||||
|
||||
if (
|
||||
['cancelled', 'failed', 'completed'].includes(currentStep.status)
|
||||
['cancelled', 'completed', 'failed'].includes(currentStep.status)
|
||||
) {
|
||||
currentStep.endedAt = entry.createdAt;
|
||||
}
|
||||
@@ -131,6 +133,11 @@ function reducer(draft: TaskStream, action: ReducerAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
case 'CANCELLED': {
|
||||
draft.cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
case 'ERROR': {
|
||||
draft.error = action.data;
|
||||
draft.loading = false;
|
||||
@@ -151,6 +158,7 @@ function reducer(draft: TaskStream, action: ReducerAction) {
|
||||
export const useTaskEventStream = (taskId: string): TaskStream => {
|
||||
const scaffolderApi = useApi(scaffolderApiRef);
|
||||
const [state, dispatch] = useImmerReducer(reducer, {
|
||||
cancelled: false,
|
||||
loading: true,
|
||||
completed: false,
|
||||
stepLogs: {} as { [stepId in string]: string[] },
|
||||
@@ -196,6 +204,9 @@ export const useTaskEventStream = (taskId: string): TaskStream => {
|
||||
switch (event.type) {
|
||||
case 'log':
|
||||
return collectedLogEvents.push(event);
|
||||
case 'cancelled':
|
||||
dispatch({ type: 'CANCELLED' });
|
||||
return undefined;
|
||||
case 'completion':
|
||||
emitLogs();
|
||||
dispatch({ type: 'COMPLETED', data: event });
|
||||
|
||||
@@ -406,4 +406,39 @@ describe('createAsyncValidators', () => {
|
||||
|
||||
expect(validators.TagField).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call validator for array object property from a custom field extension', async () => {
|
||||
const schema: JsonObject = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
links: {
|
||||
title: 'Links',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['url', 'title', 'icon'],
|
||||
properties: {
|
||||
url: {
|
||||
title: 'url',
|
||||
description: 'url',
|
||||
type: 'object',
|
||||
'ui:field': 'CustomLinkField',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const validators = { CustomLinkField: jest.fn() };
|
||||
|
||||
const validate = createAsyncValidators(schema, validators, {
|
||||
apiHolder: { get: jest.fn() },
|
||||
});
|
||||
|
||||
await validate({
|
||||
links: [{ url: 'http://my-url.spotify.com' }],
|
||||
});
|
||||
|
||||
expect(validators.CustomLinkField).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,31 +77,39 @@ export const createAsyncValidators = (
|
||||
const definitionInSchema = parsedSchema.getSchema(path, formData);
|
||||
const { schema, uiSchema } = extractSchemaFromStep(definitionInSchema);
|
||||
|
||||
if (definitionInSchema && 'ui:field' in definitionInSchema) {
|
||||
if ('ui:field' in definitionInSchema) {
|
||||
await validateForm(
|
||||
definitionInSchema['ui:field'],
|
||||
key,
|
||||
value,
|
||||
schema,
|
||||
uiSchema,
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
definitionInSchema &&
|
||||
definitionInSchema.items &&
|
||||
'ui:field' in definitionInSchema.items
|
||||
) {
|
||||
if ('ui:field' in definitionInSchema.items) {
|
||||
const hasItems = definitionInSchema && definitionInSchema.items;
|
||||
|
||||
const doValidateItem = async (
|
||||
propValue: JsonObject,
|
||||
itemSchema: JsonObject,
|
||||
itemUiSchema: NextFieldExtensionUiSchema<unknown, unknown>,
|
||||
) => {
|
||||
await validateForm(
|
||||
propValue['ui:field'] as string,
|
||||
key,
|
||||
value,
|
||||
itemSchema,
|
||||
itemUiSchema,
|
||||
);
|
||||
};
|
||||
|
||||
const doValidate = async (propValue: JsonObject) => {
|
||||
if ('ui:field' in propValue) {
|
||||
const { schema: itemsSchema, uiSchema: itemsUiSchema } =
|
||||
extractSchemaFromStep(definitionInSchema.items);
|
||||
await validateForm(
|
||||
definitionInSchema.items['ui:field'],
|
||||
key,
|
||||
value,
|
||||
itemsSchema,
|
||||
itemsUiSchema,
|
||||
);
|
||||
await doValidateItem(propValue, itemsSchema, itemsUiSchema);
|
||||
}
|
||||
};
|
||||
|
||||
if (definitionInSchema && 'ui:field' in definitionInSchema) {
|
||||
await doValidateItem(definitionInSchema, schema, uiSchema);
|
||||
} else if (hasItems && 'ui:field' in definitionInSchema.items) {
|
||||
await doValidate(definitionInSchema.items);
|
||||
} else if (hasItems && definitionInSchema.items.type === 'object') {
|
||||
const properties = (definitionInSchema.items?.properties ??
|
||||
[]) as JsonObject[];
|
||||
for (const [, propValue] of Object.entries(properties)) {
|
||||
await doValidate(propValue);
|
||||
}
|
||||
} else if (isObject(value)) {
|
||||
formValidation[key] = await validate(formData, path, value);
|
||||
|
||||
@@ -61,4 +61,55 @@ describe('TaskSteps', () => {
|
||||
expect(getByText(step.name)).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
it('should only show timer for in progress, failed, and completed steps', async () => {
|
||||
const steps = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Fail',
|
||||
status: 'failed' as ScaffolderTaskStatus,
|
||||
|
||||
startedAt: Date.now().toLocaleString(),
|
||||
endedAt: Date.now().toLocaleString(),
|
||||
action: 'action1',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Process',
|
||||
status: 'processing' as ScaffolderTaskStatus,
|
||||
startedAt: Date.now().toLocaleString(),
|
||||
action: 'action2',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Complete',
|
||||
status: 'completed' as ScaffolderTaskStatus,
|
||||
startedAt: Date.now().toLocaleString(),
|
||||
endedAt: Date.now().toLocaleString(),
|
||||
action: 'action3',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Skip',
|
||||
status: 'skipped' as ScaffolderTaskStatus,
|
||||
startedAt: Date.now().toLocaleString(),
|
||||
action: 'action4',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Not Started',
|
||||
status: 'open' as ScaffolderTaskStatus,
|
||||
action: 'action5',
|
||||
},
|
||||
];
|
||||
|
||||
const screen = await renderInTestApp(<TaskSteps steps={steps} />);
|
||||
const stepLabels = await screen.findAllByTestId('step-label');
|
||||
expect(stepLabels.length).toBe(steps.length);
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
expect(stepLabels[i].textContent?.startsWith(steps[i].name)).toBe(true);
|
||||
expect(stepLabels[i].textContent?.endsWith('seconds')).toBe(
|
||||
steps[i].status !== 'skipped' && !!steps[i].startedAt,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,9 +78,10 @@ export const TaskSteps = (props: TaskStepsProps) => {
|
||||
<MuiStepLabel
|
||||
StepIconProps={stepIconProps}
|
||||
StepIconComponent={StepIcon}
|
||||
data-testid="step-label"
|
||||
>
|
||||
<Box>{step.name}</Box>
|
||||
<StepTime step={step} />
|
||||
{!isSkipped && <StepTime step={step} />}
|
||||
</MuiStepLabel>
|
||||
</MuiStepButton>
|
||||
</MuiStep>
|
||||
|
||||
@@ -21,17 +21,22 @@ import { BackstageTheme } from '@backstage/theme';
|
||||
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
import { FavoriteEntity } from '@backstage/plugin-catalog-react';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme, { cardBackgroundImage: string }>(
|
||||
() => ({
|
||||
header: {
|
||||
backgroundImage: ({ cardBackgroundImage }) => cardBackgroundImage,
|
||||
},
|
||||
subtitleWrapper: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
}),
|
||||
);
|
||||
const useStyles = makeStyles<
|
||||
BackstageTheme,
|
||||
{
|
||||
cardFontColor: string;
|
||||
cardBackgroundImage: string;
|
||||
}
|
||||
>(() => ({
|
||||
header: {
|
||||
backgroundImage: ({ cardBackgroundImage }) => cardBackgroundImage,
|
||||
color: ({ cardFontColor }) => cardFontColor,
|
||||
},
|
||||
subtitleWrapper: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* Props for the CardHeader component
|
||||
@@ -54,6 +59,7 @@ export const CardHeader = (props: CardHeaderProps) => {
|
||||
const themeForType = getPageTheme({ themeId: type });
|
||||
|
||||
const styles = useStyles({
|
||||
cardFontColor: themeForType.fontColor,
|
||||
cardBackgroundImage: themeForType.backgroundImage,
|
||||
});
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@ 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';
|
||||
import { ScaffolderApi, scaffolderApiRef } from '../../../api';
|
||||
|
||||
const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
|
||||
cancelTask: jest.fn(),
|
||||
scaffold: jest.fn(),
|
||||
getTemplateParameterSchema: jest.fn(),
|
||||
getIntegrationsList: jest.fn(),
|
||||
|
||||
@@ -232,5 +232,39 @@ describe('useTemplateSchema', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should deal with steps having no properties', () => {
|
||||
const manifest: TemplateParameterSchema = {
|
||||
title: 'Test Template',
|
||||
description: 'Test Template Description',
|
||||
steps: [
|
||||
{
|
||||
title: 'About step',
|
||||
description:
|
||||
'The first step giving the initial information about the template',
|
||||
schema: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useTemplateSchema(manifest), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider
|
||||
apis={[[featureFlagsApiRef, { isActive: () => false }]]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
});
|
||||
|
||||
const [first] = result.current.steps;
|
||||
|
||||
expect(first.schema).toEqual({
|
||||
type: 'object',
|
||||
properties: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@ export const useTemplateSchema = (
|
||||
// Title is rendered at the top of the page, so let's ignore this from jsonschemaform
|
||||
title: undefined,
|
||||
properties: Object.fromEntries(
|
||||
Object.entries(step.schema.properties as JsonObject).filter(
|
||||
Object.entries((step.schema?.properties ?? []) as JsonObject).filter(
|
||||
([key]) => {
|
||||
const stepFeatureFlag =
|
||||
step.uiSchema[key]?.['ui:backstage']?.featureFlag;
|
||||
|
||||
@@ -43,14 +43,14 @@ const SecretsContext = createVersionedContext<{
|
||||
* The Context Provider that holds the state for the secrets.
|
||||
* @public
|
||||
*/
|
||||
export const SecretsContextProvider = ({ children }: PropsWithChildren<{}>) => {
|
||||
export const SecretsContextProvider = (props: PropsWithChildren<{}>) => {
|
||||
const [secrets, setSecrets] = useState<Record<string, string>>({});
|
||||
|
||||
return (
|
||||
<SecretsContext.Provider
|
||||
value={createVersionedValueMap({ 1: { secrets, setSecrets } })}
|
||||
>
|
||||
{children}
|
||||
{props.children}
|
||||
</SecretsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user