Merge branch 'master' into merge_upstream

Signed-off-by: Christian Marker <cfmarker@gmail.com>
This commit is contained in:
Christian Marker
2022-09-21 16:47:15 +02:00
committed by GitHub
802 changed files with 29108 additions and 10432 deletions
+38
View File
@@ -1,5 +1,43 @@
# @backstage/plugin-scaffolder
## 1.6.0
### Minor Changes
- 3424a8075d: Added support for `async` validation for the `next` version of the plugin
- ad036784e9: Ability to modify the layout of the step form
- 192d856495: Implementing review step for the scaffolder under `create/next`
### Patch Changes
- 817f3196f6: Updated React Router dependencies to be peer dependencies.
- eadf56bbbf: Bump `git-url-parse` version to `^13.0.0`
- 9ffb75616d: Fix bug with empty strings in `EntityPicker`
- 3f739be9d9: Minor API signatures cleanup
- 763fb81e82: Internal refactor to use more type safe code when dealing with route parameters.
- a66d44b72b: Fixing bug when the workspace would not be automatically saved when using `allowedOwners`
- 7d47def9c4: Removed dependency on `@types/jest`.
- 6522e459aa: Support displaying and ordering by counts in `EntityTagPicker` field. Add the `showCounts` option to enable this. Also support configuring `helperText`.
- f0510a20b5: Addition of a dismissible Error Banner in Scaffolder page
- 667d917488: Updated dependency `msw` to `^0.47.0`.
- 87ec2ba4d6: Updated dependency `msw` to `^0.46.0`.
- 9097278df2: Updated dependency `json-schema-library` to `^7.0.0`.
- bf5e9030eb: Updated dependency `msw` to `^0.45.0`.
- de336de9cd: fix uiSchema generation when using complex dependencies
- Updated dependencies
- @backstage/core-components@0.11.1
- @backstage/core-plugin-api@1.0.6
- @backstage/plugin-catalog-react@1.1.4
- @backstage/plugin-permission-react@0.4.5
- @backstage/integration@1.3.1
- @backstage/catalog-client@1.1.0
- @backstage/catalog-model@1.1.1
- @backstage/config@1.0.2
- @backstage/errors@1.1.1
- @backstage/integration-react@1.1.4
- @backstage/plugin-scaffolder-common@1.2.0
- @backstage/plugin-catalog-common@1.0.6
## 1.6.0-next.3
### Patch Changes
+23
View File
@@ -16,6 +16,7 @@ import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { FetchApi } from '@backstage/core-plugin-api';
import { FieldProps } from '@rjsf/core';
import { FieldValidation } from '@rjsf/core';
import type { FormProps } from '@rjsf/core';
import { IdentityApi } from '@backstage/core-plugin-api';
import { JsonObject } from '@backstage/types';
import { JSONSchema7 } from 'json-schema';
@@ -37,6 +38,11 @@ export function createScaffolderFieldExtension<
options: FieldExtensionOptions<TReturnValue, TInputProps>,
): Extension<FieldExtensionComponent<TReturnValue, TInputProps>>;
// @public
export function createScaffolderLayout<TInputProps = unknown>(
options: LayoutOptions,
): Extension<LayoutComponent<TInputProps>>;
// @public
export type CustomFieldValidator<TFieldReturnValue> = (
data: TFieldReturnValue,
@@ -110,6 +116,20 @@ export type FieldExtensionOptions<
validation?: CustomFieldValidator<TFieldReturnValue>;
};
// @public
export type LayoutComponent<_TInputProps> = () => null;
// @public
export interface LayoutOptions<P = any> {
// (undocumented)
component: LayoutTemplate<P>;
// (undocumented)
name: string;
}
// @public
export type LayoutTemplate<T = any> = FormProps<T>['ObjectFieldTemplate'];
// @public
export type ListActionsResponse = Array<{
id: string;
@@ -349,6 +369,9 @@ export interface ScaffolderGetIntegrationsListResponse {
}[];
}
// @public
export const ScaffolderLayouts: React.ComponentType;
// @public (undocumented)
export type ScaffolderOutputLink = {
title?: string;
+20 -21
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-scaffolder",
"description": "The Backstage plugin that helps you create new things",
"version": "1.6.0-next.3",
"version": "1.6.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -33,21 +33,20 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
"@backstage/catalog-client": "^1.1.0-next.2",
"@backstage/catalog-model": "^1.1.1-next.0",
"@backstage/config": "^1.0.2-next.0",
"@backstage/core-components": "^0.11.1-next.3",
"@backstage/core-plugin-api": "^1.0.6-next.3",
"@backstage/errors": "^1.1.1-next.0",
"@backstage/integration": "^1.3.1-next.2",
"@backstage/integration-react": "^1.1.4-next.2",
"@backstage/plugin-catalog": "^1.5.1-next.2",
"@backstage/plugin-catalog-common": "^1.0.6-next.0",
"@backstage/plugin-catalog-react": "^1.1.4-next.2",
"@backstage/plugin-permission-react": "^0.4.5-next.2",
"@backstage/plugin-scaffolder-common": "^1.2.0-next.1",
"@backstage/theme": "^0.2.16",
"@backstage/types": "^1.0.0",
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/integration-react": "workspace:^",
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/plugin-permission-react": "workspace:^",
"@backstage/plugin-scaffolder-common": "workspace:^",
"@backstage/theme": "workspace:^",
"@backstage/types": "workspace:^",
"@codemirror/language": "^6.0.0",
"@codemirror/legacy-modes": "^6.1.0",
"@codemirror/view": "^6.0.0",
@@ -80,11 +79,11 @@
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
"@backstage/cli": "^0.19.0-next.3",
"@backstage/core-app-api": "^1.1.0-next.3",
"@backstage/dev-utils": "^1.0.6-next.2",
"@backstage/plugin-catalog": "^1.5.1-next.3",
"@backstage/test-utils": "^1.2.0-next.3",
"@backstage/cli": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/dev-utils": "workspace:^",
"@backstage/plugin-catalog": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^8.0.0",
@@ -36,6 +36,7 @@ import { transformSchemaToProps } from './schema';
import { Content, StructuredMetadataTable } from '@backstage/core-components';
import cloneDeep from 'lodash/cloneDeep';
import * as fieldOverrides from './FieldOverrides';
import { LayoutOptions } from '../../layouts';
const Form = withTheme(MuiTheme);
type Step = {
@@ -55,6 +56,7 @@ type Props = {
widgets?: FormProps<any>['widgets'];
fields?: FormProps<any>['fields'];
finishButtonLabel?: string;
layouts: LayoutOptions[];
};
export function getUiSchemasFromSteps(steps: Step[]): UiSchema[] {
@@ -119,6 +121,7 @@ export const MultistepJsonForm = (props: Props) => {
fields,
widgets,
finishButtonLabel,
layouts,
} = props;
const [activeStep, setActiveStep] = useState(0);
const [disableButtons, setDisableButtons] = useState(false);
@@ -214,7 +217,7 @@ export const MultistepJsonForm = (props: Props) => {
if (e.errors.length === 0) handleNext();
}}
{...formProps}
{...transformSchemaToProps(schema)}
{...transformSchemaToProps(schema, layouts)}
>
<Button disabled={activeStep === 0} onClick={handleBack}>
Back
@@ -15,7 +15,8 @@
*/
import { JsonObject } from '@backstage/types';
import { FormProps } from '@rjsf/core';
import { FormProps, UiSchema } from '@rjsf/core';
import { LayoutOptions } from '../../layouts';
function isObject(value: unknown): value is JsonObject {
return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -99,14 +100,29 @@ function extractUiSchema(schema: JsonObject, uiSchema: JsonObject) {
}
}
export function transformSchemaToProps(inputSchema: JsonObject): {
export function transformSchemaToProps(
inputSchema: JsonObject,
layouts: LayoutOptions[] = [],
): {
schema: FormProps<any>['schema'];
uiSchema: FormProps<any>['uiSchema'];
} {
const customLayoutName = inputSchema['ui:ObjectFieldTemplate'];
inputSchema.type = inputSchema.type || 'object';
const schema = JSON.parse(JSON.stringify(inputSchema));
delete schema.title; // Rendered separately
const uiSchema = {};
const uiSchema: UiSchema = {};
extractUiSchema(schema, uiSchema);
if (customLayoutName) {
const Layout = layouts.find(
layout => layout.name === customLayoutName,
)?.component;
if (Layout) {
uiSchema['ui:ObjectFieldTemplate'] = Layout;
}
}
return { schema, uiSchema };
}
+17 -1
View File
@@ -45,6 +45,7 @@ import {
selectedTemplateRouteRef,
} from '../routes';
import { ListTasksPage } from './ListTasksPage';
import { LayoutOptions, LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from '../layouts';
/**
* The props for the entrypoint `ScaffolderPage` component the plugin.
@@ -105,6 +106,17 @@ export const Router = (props: RouterProps) => {
),
),
];
const customLayouts = useElementFilter(outlet, elements =>
elements
.selectByComponentData({
key: LAYOUTS_WRAPPER_KEY,
})
.findComponentData<LayoutOptions>({
key: LAYOUTS_KEY,
}),
);
/**
* This component can be deleted once the older routes have been deprecated.
*/
@@ -142,7 +154,10 @@ export const Router = (props: RouterProps) => {
path={selectedTemplateRouteRef.path}
element={
<SecretsContextProvider>
<TemplatePage customFieldExtensions={fieldExtensions} />
<TemplatePage
customFieldExtensions={fieldExtensions}
layouts={customLayouts}
/>
</SecretsContextProvider>
}
/>
@@ -159,6 +174,7 @@ export const Router = (props: RouterProps) => {
<TemplateEditorPage
defaultPreviewTemplate={defaultPreviewTemplate}
customFieldExtensions={fieldExtensions}
layouts={customLayouts}
/>
</SecretsContextProvider>
}
@@ -16,6 +16,7 @@
import { makeStyles } from '@material-ui/core';
import React, { useState } from 'react';
import { FieldExtensionOptions } from '../../extensions';
import type { LayoutOptions } from '../../layouts';
import { TemplateDirectoryAccess } from '../../lib/filesystem';
import { DirectoryEditorProvider } from './DirectoryEditorContext';
import { DryRunProvider } from './DryRunContext';
@@ -57,6 +58,7 @@ const useStyles = makeStyles({
export const TemplateEditor = (props: {
directory: TemplateDirectoryAccess;
fieldExtensions?: FieldExtensionOptions<any, any>[];
layouts?: LayoutOptions[];
onClose?: () => void;
}) => {
const classes = useStyles();
@@ -77,6 +79,7 @@ export const TemplateEditor = (props: {
<TemplateEditorForm.DirectoryEditorDryRun
setErrorText={setErrorText}
fieldExtensions={props.fieldExtensions}
layouts={props.layouts}
/>
</section>
<section className={classes.results}>
@@ -20,6 +20,7 @@ import React, { Component, ReactNode, useMemo, useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import yaml from 'yaml';
import { FieldExtensionOptions } from '../../extensions';
import { LayoutOptions } from '../../layouts';
import { TemplateParameterSchema } from '../../types';
import { MultistepJsonForm } from '../MultistepJsonForm';
import { createValidator } from '../TemplatePage';
@@ -83,6 +84,7 @@ interface TemplateEditorFormProps {
onDryRun?: (data: JsonObject) => Promise<void>;
fieldExtensions?: FieldExtensionOptions<any, any>[];
layouts?: LayoutOptions[];
}
function isJsonObject(value: JsonValue | undefined): value is JsonObject {
@@ -99,6 +101,7 @@ export function TemplateEditorForm(props: TemplateEditorFormProps) {
onDryRun,
setErrorText,
fieldExtensions = [],
layouts = [],
} = props;
const classes = useStyles();
const apiHolder = useApiHolder();
@@ -138,6 +141,7 @@ export function TemplateEditorForm(props: TemplateEditorFormProps) {
}
const { parameters } = rootObj;
if (!Array.isArray(parameters)) {
setErrorText('Template parameters must be an array');
setSteps(undefined);
@@ -188,6 +192,7 @@ export function TemplateEditorForm(props: TemplateEditorFormProps) {
onReset={() => onUpdate({})}
finishButtonLabel={onDryRun && 'Try It'}
onFinish={onDryRun && (() => onDryRun(data))}
layouts={layouts}
/>
</ErrorBoundary>
</div>
@@ -197,7 +202,10 @@ export function TemplateEditorForm(props: TemplateEditorFormProps) {
/** A version of the TemplateEditorForm that is connected to the DirectoryEditor and DryRun contexts */
export function TemplateEditorFormDirectoryEditorDryRun(
props: Pick<TemplateEditorFormProps, 'setErrorText' | 'fieldExtensions'>,
props: Pick<
TemplateEditorFormProps,
'setErrorText' | 'fieldExtensions' | 'layouts'
>,
) {
const { setErrorText, fieldExtensions = [] } = props;
const dryRun = useDryRun();
@@ -23,6 +23,7 @@ import { TemplateEditorIntro } from './TemplateEditorIntro';
import { TemplateEditor } from './TemplateEditor';
import { TemplateFormPreviewer } from './TemplateFormPreviewer';
import { FieldExtensionOptions } from '../../extensions';
import type { LayoutOptions } from '../../layouts';
type Selection =
| {
@@ -36,6 +37,7 @@ type Selection =
interface TemplateEditorPageProps {
defaultPreviewTemplate?: string;
customFieldExtensions?: FieldExtensionOptions<any, any>[];
layouts?: LayoutOptions[];
}
export function TemplateEditorPage(props: TemplateEditorPageProps) {
@@ -48,6 +50,7 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) {
directory={selection.directory}
fieldExtensions={props.customFieldExtensions}
onClose={() => setSelection(undefined)}
layouts={props.layouts}
/>
);
} else if (selection?.type === 'form') {
@@ -56,6 +59,7 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) {
defaultPreviewTemplate={props.defaultPreviewTemplate}
customFieldExtensions={props.customFieldExtensions}
onClose={() => setSelection(undefined)}
layouts={props.layouts}
/>
);
} else {
@@ -33,6 +33,7 @@ import React, { useCallback, useState } from 'react';
import useAsync from 'react-use/lib/useAsync';
import yaml from 'yaml';
import { FieldExtensionOptions } from '../../extensions';
import { LayoutOptions } from '../../layouts';
import { TemplateEditorForm } from './TemplateEditorForm';
import { TemplateEditorTextArea } from './TemplateEditorTextArea';
@@ -110,10 +111,12 @@ export const TemplateFormPreviewer = ({
defaultPreviewTemplate = EXAMPLE_TEMPLATE_PARAMS_YAML,
customFieldExtensions = [],
onClose,
layouts = [],
}: {
defaultPreviewTemplate?: string;
customFieldExtensions?: FieldExtensionOptions<any, any>[];
onClose?: () => void;
layouts?: LayoutOptions[];
}) => {
const classes = useStyles();
const alertApi = useApi(alertApiRef);
@@ -208,6 +211,7 @@ export const TemplateFormPreviewer = ({
data={formState}
onUpdate={setFormState}
setErrorText={setErrorText}
layouts={layouts}
/>
</div>
</main>
@@ -39,6 +39,7 @@ import {
useRouteRefParams,
} from '@backstage/core-plugin-api';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { LayoutOptions } from '../../layouts';
const useTemplateParameterSchema = (templateRef: string) => {
const scaffolderApi = useApi(scaffolderApiRef);
@@ -51,8 +52,10 @@ const useTemplateParameterSchema = (templateRef: string) => {
export const TemplatePage = ({
customFieldExtensions = [],
layouts = [],
}: {
customFieldExtensions?: FieldExtensionOptions<any, any>[];
layouts?: LayoutOptions[];
}) => {
const apiHolder = useApiHolder();
const secretsContext = useContext(SecretsContext);
@@ -146,6 +149,7 @@ export const TemplatePage = ({
onChange={handleChange}
onReset={handleFormReset}
onFinish={handleCreate}
layouts={layouts}
steps={schema.steps.map(step => {
return {
...step,
+2
View File
@@ -48,6 +48,8 @@ export type {
FieldExtensionComponentProps,
FieldExtensionComponent,
} from './extensions';
export { createScaffolderLayout, ScaffolderLayouts } from './layouts';
export type { LayoutOptions, LayoutTemplate, LayoutComponent } from './layouts';
export {
EntityPickerFieldExtension,
EntityNamePickerFieldExtension,
+59
View File
@@ -0,0 +1,59 @@
/*
* 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 { attachComponentData, Extension } from '@backstage/core-plugin-api';
import type { LayoutOptions } from './types';
export const LAYOUTS_KEY = 'scaffolder.layout.v1';
export const LAYOUTS_WRAPPER_KEY = 'scaffolder.layouts.wrapper.v1';
/**
* The type used to wrap up the Layout and embed the input props
*
* @public
*/
export type LayoutComponent<_TInputProps> = () => null;
/**
* Method for creating custom Layouts that can be used in the scaffolder frontend form
*
* @public
*/
export function createScaffolderLayout<TInputProps = unknown>(
options: LayoutOptions,
): Extension<LayoutComponent<TInputProps>> {
return {
expose() {
const LayoutDataHolder: any = () => null;
attachComponentData(LayoutDataHolder, LAYOUTS_KEY, options);
return LayoutDataHolder;
},
};
}
/**
* The wrapping component for defining scaffolder layouts as children
*
* @public
*/
export const ScaffolderLayouts: React.ComponentType = (): JSX.Element | null =>
null;
attachComponentData(ScaffolderLayouts, LAYOUTS_WRAPPER_KEY, true);
export type { LayoutOptions, LayoutTemplate } from './types';
+33
View File
@@ -0,0 +1,33 @@
/*
* 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 type { FormProps } from '@rjsf/core';
/**
* The field template from \@rjsf/core which is a react component that gets passed \@rjsf/core field related props.
*
* @public
*/
export type LayoutTemplate<T = any> = FormProps<T>['ObjectFieldTemplate'];
/**
* The type of layouts that is passed to the TemplateForms
*
* @public
*/
export interface LayoutOptions<P = any> {
name: string;
component: LayoutTemplate<P>;
}