Merge branch 'master' into techdocs/remove-warnings

Signed-off-by: Morgan Bentell <morgan.bentell@gmail.com>
This commit is contained in:
Morgan Bentell
2023-03-16 16:18:32 +01:00
committed by GitHub
1099 changed files with 36089 additions and 25992 deletions
+62
View File
@@ -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
+9 -6
View File
@@ -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 -1
View File
@@ -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",
+11 -3
View File
@@ -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>
);
};