diff --git a/.changeset/kind-pianos-burn.md b/.changeset/kind-pianos-burn.md new file mode 100644 index 0000000000..1fb09aeb83 --- /dev/null +++ b/.changeset/kind-pianos-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': major +--- + +Re-home some of the common types, components, hooks and `scaffolderApiRef` for the `@backstage/plugin-scaffolder` to this package for easy re-use across things that want to interact with the `scaffolder`. diff --git a/.changeset/wild-socks-breathe.md b/.changeset/wild-socks-breathe.md new file mode 100644 index 0000000000..7a4dff4c3b --- /dev/null +++ b/.changeset/wild-socks-breathe.md @@ -0,0 +1,45 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +- **Deprecation** - Deprecated the following exports, please import them directly from `@backstage/plugin-scaffolder-react` instead + + ``` + createScaffolderFieldExtension + ScaffolderFieldExtensions + useTemplateSecrets + scaffolderApiRef + ScaffolderApi + ScaffolderUseTemplateSecrets + TemplateParameterSchema + CustomFieldExtensionSchema + CustomFieldValidator + FieldExtensionOptions + FieldExtensionComponentProps + FieldExtensionComponent + ListActionsResponse + LogEvent + ScaffolderDryRunOptions + ScaffolderDryRunResponse + ScaffolderGetIntegrationsListOptions + ScaffolderGetIntegrationsListResponse + ScaffolderOutputlink + ScaffolderScaffoldOptions + ScaffolderScaffoldResponse + ScaffolderStreamLogsOptions + ScaffolderTask + ScaffolderTaskOutput + ScaffolderTaskStatus + ``` + +- **Deprecation** - Deprecated the `rootRouteRef` export, this should now be used from `scaffolderPlugin.routes.root` + +- The following `/alpha` types have removed from this package and moved to the `@backstage/plugin-scaffolder-react/alpha` package + + ``` + createNextScaffolderFieldExtension + FormProps + NextCustomFieldValidator + NextFieldExtensionComponentProps + NextFieldExtensionOptions + ``` diff --git a/packages/app/package.json b/packages/app/package.json index 553754a15d..86e0402d7d 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -49,6 +49,7 @@ "@backstage/plugin-playlist": "workspace:^", "@backstage/plugin-rollbar": "workspace:^", "@backstage/plugin-scaffolder": "workspace:^", + "@backstage/plugin-scaffolder-react": "workspace:^", "@backstage/plugin-search": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "@backstage/plugin-search-react": "workspace:^", diff --git a/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx b/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx index 3265d347a2..c521bcf86c 100644 --- a/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx +++ b/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx @@ -15,15 +15,15 @@ */ import React from 'react'; import type { FieldValidation } from '@rjsf/utils'; +import { scaffolderPlugin } from '@backstage/plugin-scaffolder'; +import { TextField } from '@material-ui/core'; + import { + NextFieldExtensionComponentProps, createNextScaffolderFieldExtension, createScaffolderFieldExtension, FieldExtensionComponentProps, - NextFieldExtensionComponentProps, - scaffolderPlugin, -} from '@backstage/plugin-scaffolder'; - -import { TextField } from '@material-ui/core'; +} from '@backstage/plugin-scaffolder-react'; const TextValuePicker = (props: FieldExtensionComponentProps) => { const { diff --git a/plugins/scaffolder-react/.eslintrc.js b/plugins/scaffolder-react/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/scaffolder-react/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-react/README.md b/plugins/scaffolder-react/README.md new file mode 100644 index 0000000000..1db37d5b23 --- /dev/null +++ b/plugins/scaffolder-react/README.md @@ -0,0 +1,12 @@ +# Scaffolder React + +This is shared code of the frontend part of the default Scaffolder plugin. + +It will implement the core API for working with the Scaffolder, and +supplies components that can be reused by third-party plugins. + +## Links + +- [Frontend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/scaffolder) +- [Backend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend) +- [The Backstage homepage](https://backstage.io) diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md new file mode 100644 index 0000000000..adc097775b --- /dev/null +++ b/plugins/scaffolder-react/api-report.md @@ -0,0 +1,430 @@ +## API Report File for "@backstage/plugin-scaffolder-react" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { ApiHolder } from '@backstage/core-plugin-api'; +import { ApiRef } from '@backstage/core-plugin-api'; +import { Dispatch } from 'react'; +import { Extension } from '@backstage/core-plugin-api'; +import { FieldProps } from '@rjsf/core'; +import { FieldProps as FieldProps_2 } from '@rjsf/utils'; +import { FieldValidation } from '@rjsf/core'; +import { FieldValidation as FieldValidation_2 } from '@rjsf/utils'; +import type { FormProps as FormProps_2 } from '@rjsf/core-v5'; +import { IconComponent } from '@backstage/core-plugin-api'; +import { JsonObject } from '@backstage/types'; +import { JSONSchema7 } from 'json-schema'; +import { JsonValue } from '@backstage/types'; +import { Observable } from '@backstage/types'; +import { PropsWithChildren } from 'react'; +import { default as React_2 } from 'react'; +import { SetStateAction } from 'react'; +import { TaskSpec } from '@backstage/plugin-scaffolder-common'; +import { TaskStep } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { UIOptionsType } from '@rjsf/utils'; +import { UiSchema } from '@rjsf/utils'; + +// @public +export type Action = { + id: string; + description?: string; + schema?: { + input?: JSONSchema7; + output?: JSONSchema7; + }; + examples?: ActionExample[]; +}; + +// @public +export type ActionExample = { + description: string; + example: string; +}; + +// @alpha +export const createFieldValidation: () => FieldValidation_2; + +// @alpha +export function createNextScaffolderFieldExtension< + TReturnValue = unknown, + TInputProps extends UIOptionsType = {}, +>( + options: NextFieldExtensionOptions, +): Extension>; + +// @public +export function createScaffolderFieldExtension< + TReturnValue = unknown, + TInputProps = unknown, +>( + options: FieldExtensionOptions, +): Extension>; + +// @public +export type CustomFieldExtensionSchema = { + returnValue: JSONSchema7; + uiOptions?: JSONSchema7; +}; + +// @public +export type CustomFieldValidator = ( + data: TFieldReturnValue, + field: FieldValidation, + context: { + apiHolder: ApiHolder; + }, +) => void | Promise; + +// @alpha +export const extractSchemaFromStep: (inputStep: JsonObject) => { + uiSchema: UiSchema; + schema: JsonObject; +}; + +// @public +export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; + +// @public +export interface FieldExtensionComponentProps< + TFieldReturnValue, + TUiOptions extends {} = {}, +> extends FieldProps { + // (undocumented) + uiSchema: FieldProps['uiSchema'] & { + 'ui:options'?: TUiOptions; + }; +} + +// @public +export type FieldExtensionOptions< + TFieldReturnValue = unknown, + TInputProps = unknown, +> = { + name: string; + component: ( + props: FieldExtensionComponentProps, + ) => JSX.Element | null; + validation?: CustomFieldValidator; + schema?: CustomFieldExtensionSchema; +}; + +// @alpha +export type FormProps = Pick< + FormProps_2, + 'transformErrors' | 'noHtml5Validate' +>; + +// @public +export type ListActionsResponse = Array; + +// @public +export type LogEvent = { + type: 'log' | 'completion'; + body: { + message: string; + stepId?: string; + status?: ScaffolderTaskStatus; + }; + createdAt: string; + id: string; + taskId: string; +}; + +// @alpha +export type NextCustomFieldValidator = ( + data: TFieldReturnValue, + field: FieldValidation_2, + context: { + apiHolder: ApiHolder; + formData: JsonObject; + }, +) => void | Promise; + +// @alpha +export interface NextFieldExtensionComponentProps< + TFieldReturnValue, + TUiOptions = {}, +> extends PropsWithChildren> { + // (undocumented) + uiSchema?: UiSchema & { + 'ui:options'?: TUiOptions & UIOptionsType; + }; +} + +// @alpha +export type NextFieldExtensionOptions< + TFieldReturnValue = unknown, + TInputProps = unknown, +> = { + name: string; + component: ( + props: NextFieldExtensionComponentProps, + ) => JSX.Element | null; + validation?: NextCustomFieldValidator; + schema?: CustomFieldExtensionSchema; +}; + +// @alpha +export interface ParsedTemplateSchema { + // (undocumented) + description?: string; + // (undocumented) + mergedSchema: JsonObject; + // (undocumented) + schema: JsonObject; + // (undocumented) + title: string; + // (undocumented) + uiSchema: UiSchema; +} + +// @alpha +export const ReviewState: (props: ReviewStateProps) => JSX.Element; + +// @alpha +export type ReviewStateProps = { + schemas: ParsedTemplateSchema[]; + formState: JsonObject; +}; + +// @public +export interface ScaffolderApi { + // (undocumented) + dryRun?(options: ScaffolderDryRunOptions): Promise; + // (undocumented) + getIntegrationsList( + options: ScaffolderGetIntegrationsListOptions, + ): Promise; + // (undocumented) + getTask(taskId: string): Promise; + // (undocumented) + getTemplateParameterSchema( + templateRef: string, + ): Promise; + listActions(): Promise; + // (undocumented) + listTasks?(options: { filterByOwnership: 'owned' | 'all' }): Promise<{ + tasks: ScaffolderTask[]; + }>; + scaffold( + options: ScaffolderScaffoldOptions, + ): Promise; + // (undocumented) + streamLogs(options: ScaffolderStreamLogsOptions): Observable; +} + +// @public (undocumented) +export const scaffolderApiRef: ApiRef; + +// @public (undocumented) +export interface ScaffolderDryRunOptions { + // (undocumented) + directoryContents: { + path: string; + base64Content: string; + }[]; + // (undocumented) + secrets?: Record; + // (undocumented) + template: JsonValue; + // (undocumented) + values: JsonObject; +} + +// @public (undocumented) +export interface ScaffolderDryRunResponse { + // (undocumented) + directoryContents: Array<{ + path: string; + base64Content: string; + executable: boolean; + }>; + // (undocumented) + log: Array>; + // (undocumented) + output: ScaffolderTaskOutput; + // (undocumented) + steps: TaskStep[]; +} + +// @public +export const ScaffolderFieldExtensions: React_2.ComponentType< + React_2.PropsWithChildren<{}> +>; + +// @public +export interface ScaffolderGetIntegrationsListOptions { + // (undocumented) + allowedHosts: string[]; +} + +// @public +export interface ScaffolderGetIntegrationsListResponse { + // (undocumented) + integrations: { + type: string; + title: string; + host: string; + }[]; +} + +// @public (undocumented) +export type ScaffolderOutputLink = { + title?: string; + icon?: string; + url?: string; + entityRef?: string; +}; + +// @public +export interface ScaffolderScaffoldOptions { + // (undocumented) + secrets?: Record; + // (undocumented) + templateRef: string; + // (undocumented) + values: Record; +} + +// @public +export interface ScaffolderScaffoldResponse { + // (undocumented) + taskId: string; +} + +// @public +export interface ScaffolderStreamLogsOptions { + // (undocumented) + after?: number; + // (undocumented) + taskId: string; +} + +// @public +export type ScaffolderTask = { + id: string; + spec: TaskSpec; + status: 'failed' | 'completed' | 'processing' | 'open' | 'cancelled'; + lastHeartbeatAt: string; + createdAt: string; +}; + +// @public (undocumented) +export type ScaffolderTaskOutput = { + links?: ScaffolderOutputLink[]; +} & { + [key: string]: unknown; +}; + +// @public +export type ScaffolderTaskStatus = + | 'open' + | 'processing' + | 'failed' + | 'completed' + | 'skipped'; + +// @public +export interface ScaffolderUseTemplateSecrets { + // (undocumented) + secrets: Record; + // (undocumented) + setSecrets: (input: Record) => void; +} + +// @public +export const SecretsContextProvider: ({ + children, +}: PropsWithChildren<{}>) => JSX.Element; + +// @alpha +export const Stepper: (props: StepperProps) => JSX.Element; + +// @alpha +export type StepperProps = { + manifest: TemplateParameterSchema; + extensions: NextFieldExtensionOptions[]; + templateName?: string; + FormProps?: FormProps; + initialState?: Record; + onComplete: (values: Record) => Promise; +}; + +// @alpha +export const TemplateCard: (props: TemplateCardProps) => JSX.Element; + +// @alpha +export interface TemplateCardProps { + // (undocumented) + additionalLinks?: { + icon: IconComponent; + text: string; + url: string; + }[]; + // (undocumented) + onSelected?: (template: TemplateEntityV1beta3) => void; + // (undocumented) + template: TemplateEntityV1beta3; +} + +// @alpha +export const TemplateGroup: (props: TemplateGroupProps) => JSX.Element; + +// @alpha +export interface TemplateGroupProps { + // (undocumented) + components?: { + CardComponent?: React_2.ComponentType; + }; + // (undocumented) + onSelected: (template: TemplateEntityV1beta3) => void; + // (undocumented) + templates: { + template: TemplateEntityV1beta3; + additionalLinks?: { + icon: IconComponent; + text: string; + url: string; + }[]; + }[]; + // (undocumented) + title: React_2.ReactNode; +} + +// @public +export type TemplateParameterSchema = { + title: string; + description?: string; + steps: Array<{ + title: string; + description?: string; + schema: JsonObject; + }>; +}; + +// @public +export const useCustomFieldExtensions: < + TComponentDataType = FieldExtensionOptions, +>( + outlet: React.ReactNode, +) => TComponentDataType[]; + +// @alpha +export const useFormDataFromQuery: ( + initialState?: Record, +) => [Record, Dispatch>>]; + +// @alpha +export const useTemplateSchema: (manifest: TemplateParameterSchema) => { + steps: ParsedTemplateSchema[]; +}; + +// @public +export const useTemplateSecrets: () => ScaffolderUseTemplateSecrets; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json new file mode 100644 index 0000000000..2a841a7e8b --- /dev/null +++ b/plugins/scaffolder-react/package.json @@ -0,0 +1,85 @@ +{ + "name": "@backstage/plugin-scaffolder-react", + "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" + }, + "backstage": { + "role": "web-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/scaffolder-react" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" + }, + "dependencies": { + "@backstage/catalog-client": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-scaffolder-common": "workspace:^", + "@backstage/theme": "workspace:^", + "@backstage/types": "workspace:^", + "@backstage/version-bridge": "workspace:^", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "@rjsf/core": "^3.2.1", + "@rjsf/core-v5": "npm:@rjsf/core@^5.0.0-beta.14", + "@rjsf/material-ui": "^3.2.1", + "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@^5.0.0-beta.14", + "@rjsf/utils": "^5.0.0-beta.14", + "@rjsf/validator-ajv6": "^5.0.0-beta.14", + "@types/json-schema": "^7.0.9", + "classnames": "^2.2.6", + "json-schema": "^0.4.0", + "json-schema-library": "^7.3.9", + "lodash": "^4.17.21", + "qs": "^6.9.4", + "zen-observable": "^0.10.0", + "zod": "~3.18.0", + "zod-to-json-schema": "~3.18.0" + }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0", + "react-router-dom": "6.0.0-beta.0 || ^6.3.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/plugin-catalog": "workspace:^", + "@backstage/plugin-catalog-common": "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", + "@testing-library/user-event": "^14.0.0" + }, + "files": [ + "dist", + "alpha" + ] +} diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/index.ts b/plugins/scaffolder-react/src/api/index.ts similarity index 85% rename from plugins/scaffolder/src/next/TemplateListPage/TemplateCard/index.ts rename to plugins/scaffolder-react/src/api/index.ts index 99af0cbb50..fa8d758afb 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/index.ts +++ b/plugins/scaffolder-react/src/api/index.ts @@ -13,5 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { TemplateCard } from './TemplateCard'; -export type { TemplateCardProps } from './TemplateCard'; +export * from './ref'; +export * from './types'; diff --git a/plugins/scaffolder-react/src/api/ref.ts b/plugins/scaffolder-react/src/api/ref.ts new file mode 100644 index 0000000000..c4b7853556 --- /dev/null +++ b/plugins/scaffolder-react/src/api/ref.ts @@ -0,0 +1,28 @@ +/* + * 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 { createApiRef } from '@backstage/core-plugin-api'; +import { ScaffolderApi } from './types'; +import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; + +/** @public */ +export const scaffolderApiRef = getOrCreateGlobalSingleton( + 'scaffolder:scaffolder-api-ref', + () => + createApiRef({ + id: 'plugin.scaffolder.service', + }), +); diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder-react/src/api/types.ts similarity index 94% rename from plugins/scaffolder/src/types.ts rename to plugins/scaffolder-react/src/api/types.ts index 422661b896..0a517e7bff 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder-react/src/api/types.ts @@ -16,6 +16,7 @@ import { TaskSpec, TaskStep } from '@backstage/plugin-scaffolder-common'; import { JsonObject, JsonValue, Observable } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; +import { TemplateParameterSchema } from '../types'; /** * The status of each task in a Scaffolder Job @@ -89,22 +90,6 @@ export type ScaffolderTaskOutput = { [key: string]: unknown; }; -/** - * The shape of each entry of parameters which gets rendered - * as a separate step in the wizard input - * - * @public - */ -export type TemplateParameterSchema = { - title: string; - description?: string; - steps: Array<{ - title: string; - description?: string; - schema: JsonObject; - }>; -}; - /** * The shape of a `LogEvent` message from the `scaffolder-backend` * @@ -189,7 +174,6 @@ export interface ScaffolderDryRunResponse { steps: TaskStep[]; output: ScaffolderTaskOutput; } - /** * An API to interact with the scaffolder backend. * diff --git a/plugins/scaffolder/src/extensions/index.tsx b/plugins/scaffolder-react/src/extensions/index.tsx similarity index 65% rename from plugins/scaffolder/src/extensions/index.tsx rename to plugins/scaffolder-react/src/extensions/index.tsx index 2b3126ac58..33442f6697 100644 --- a/plugins/scaffolder/src/extensions/index.tsx +++ b/plugins/scaffolder-react/src/extensions/index.tsx @@ -20,19 +20,12 @@ import { CustomFieldValidator, FieldExtensionOptions, FieldExtensionComponentProps, - NextCustomFieldValidator, - NextFieldExtensionOptions, - NextFieldExtensionComponentProps, } from './types'; import { Extension, attachComponentData } from '@backstage/core-plugin-api'; -import { UIOptionsType } from '@rjsf/utils'; - -export const FIELD_EXTENSION_WRAPPER_KEY = 'scaffolder.extensions.wrapper.v1'; -export const FIELD_EXTENSION_KEY = 'scaffolder.extensions.field.v1'; +import { FIELD_EXTENSION_KEY, FIELD_EXTENSION_WRAPPER_KEY } from './keys'; /** * A type used to wrap up the FieldExtension to embed the ReturnValue and the InputProps - * * @public */ export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; @@ -63,32 +56,6 @@ export function createScaffolderFieldExtension< }; } -/** - * Method for creating field extensions that can be used in the scaffolder - * frontend form. - * @alpha - */ -export function createNextScaffolderFieldExtension< - TReturnValue = unknown, - TInputProps extends UIOptionsType = {}, ->( - options: NextFieldExtensionOptions, -): Extension> { - return { - expose() { - const FieldExtensionDataHolder: any = () => null; - - attachComponentData( - FieldExtensionDataHolder, - FIELD_EXTENSION_KEY, - options, - ); - - return FieldExtensionDataHolder; - }, - }; -} - /** * The Wrapping component for defining fields extensions inside * @@ -109,9 +76,4 @@ export type { CustomFieldValidator, FieldExtensionOptions, FieldExtensionComponentProps, - NextCustomFieldValidator, - NextFieldExtensionOptions, - NextFieldExtensionComponentProps, }; - -export { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from './default'; diff --git a/plugins/scaffolder-react/src/extensions/keys.ts b/plugins/scaffolder-react/src/extensions/keys.ts new file mode 100644 index 0000000000..f035e9c1c2 --- /dev/null +++ b/plugins/scaffolder-react/src/extensions/keys.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +export const FIELD_EXTENSION_WRAPPER_KEY = 'scaffolder.extensions.wrapper.v1'; +/** + * The key used to store the field extension data for any `` component + */ +export const FIELD_EXTENSION_KEY = 'scaffolder.extensions.field.v1'; diff --git a/plugins/scaffolder-react/src/extensions/types.ts b/plugins/scaffolder-react/src/extensions/types.ts new file mode 100644 index 0000000000..a9ed854afa --- /dev/null +++ b/plugins/scaffolder-react/src/extensions/types.ts @@ -0,0 +1,72 @@ +/* + * 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 { ApiHolder } from '@backstage/core-plugin-api'; +import { FieldValidation, FieldProps } from '@rjsf/core'; +import { JSONSchema7 } from 'json-schema'; + +/** + * Field validation type for Custom Field Extensions. + * + * @public + */ +export type CustomFieldValidator = ( + data: TFieldReturnValue, + field: FieldValidation, + context: { apiHolder: ApiHolder }, +) => void | Promise; + +/** + * Type for the Custom Field Extension schema. + * + * @public + */ +export type CustomFieldExtensionSchema = { + returnValue: JSONSchema7; + uiOptions?: JSONSchema7; +}; + +/** + * Type for the Custom Field Extension with the + * name and components and validation function. + * + * @public + */ +export type FieldExtensionOptions< + TFieldReturnValue = unknown, + TInputProps = unknown, +> = { + name: string; + component: ( + props: FieldExtensionComponentProps, + ) => JSX.Element | null; + validation?: CustomFieldValidator; + schema?: CustomFieldExtensionSchema; +}; + +/** + * Type for field extensions and being able to type + * incoming props easier. + * + * @public + */ +export interface FieldExtensionComponentProps< + TFieldReturnValue, + TUiOptions extends {} = {}, +> extends FieldProps { + uiSchema: FieldProps['uiSchema'] & { + 'ui:options'?: TUiOptions; + }; +} diff --git a/plugins/scaffolder-react/src/hooks/index.ts b/plugins/scaffolder-react/src/hooks/index.ts new file mode 100644 index 0000000000..022fe7c38c --- /dev/null +++ b/plugins/scaffolder-react/src/hooks/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { useCustomFieldExtensions } from './useCustomFieldExtensions'; diff --git a/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.ts b/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.ts new file mode 100644 index 0000000000..7e0aafd53f --- /dev/null +++ b/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.ts @@ -0,0 +1,41 @@ +/* + * 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 { useElementFilter } from '@backstage/core-plugin-api'; +import { FieldExtensionOptions } from '../extensions'; +import { + FIELD_EXTENSION_KEY, + FIELD_EXTENSION_WRAPPER_KEY, +} from '../extensions/keys'; + +/** + * Hook that returns all custom field extensions from the current outlet. + * @public + */ +export const useCustomFieldExtensions = < + TComponentDataType = FieldExtensionOptions, +>( + outlet: React.ReactNode, +) => { + return useElementFilter(outlet, elements => + elements + .selectByComponentData({ + key: FIELD_EXTENSION_WRAPPER_KEY, + }) + .findComponentData({ + key: FIELD_EXTENSION_KEY, + }), + ); +}; diff --git a/plugins/scaffolder-react/src/index.ts b/plugins/scaffolder-react/src/index.ts new file mode 100644 index 0000000000..0b053e6bf4 --- /dev/null +++ b/plugins/scaffolder-react/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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 * from './extensions'; +export * from './types'; +export * from './secrets'; +export * from './api'; +export * from './hooks'; + +export * from './next'; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.test.tsx b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.test.tsx similarity index 98% rename from plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.test.tsx rename to plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.test.tsx index c03df8f73c..0f56a32d1d 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.test.tsx +++ b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { ReviewState } from './ReviewState'; import { render } from '@testing-library/react'; -import { ParsedTemplateSchema } from './useTemplateSchema'; +import { ParsedTemplateSchema } from '../../hooks/useTemplateSchema'; describe('ReviewState', () => { it('should render the text as normal with no options', () => { diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.tsx b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx similarity index 87% rename from plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.tsx rename to plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx index f39a26622a..52ad80c4c2 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.tsx +++ b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx @@ -16,14 +16,22 @@ import React from 'react'; import { StructuredMetadataTable } from '@backstage/core-components'; import { JsonObject } from '@backstage/types'; -import { ParsedTemplateSchema } from './useTemplateSchema'; import { Draft07 as JSONSchema } from 'json-schema-library'; +import { ParsedTemplateSchema } from '../../hooks/useTemplateSchema'; -interface ReviewStateProps { +/** + * The props for the {@link ReviewState} component. + * @alpha + */ +export type ReviewStateProps = { schemas: ParsedTemplateSchema[]; formState: JsonObject; -} +}; +/** + * The component used by the {@link Stepper} to render the review step. + * @alpha + */ export const ReviewState = (props: ReviewStateProps) => { const reviewData = Object.fromEntries( Object.entries(props.formState).map(([key, value]) => { diff --git a/plugins/scaffolder/src/components/secrets/index.ts b/plugins/scaffolder-react/src/next/components/ReviewState/index.ts similarity index 82% rename from plugins/scaffolder/src/components/secrets/index.ts rename to plugins/scaffolder-react/src/next/components/ReviewState/index.ts index c608834803..1bde4685d8 100644 --- a/plugins/scaffolder/src/components/secrets/index.ts +++ b/plugins/scaffolder-react/src/next/components/ReviewState/index.ts @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { useTemplateSecrets } from './SecretsContext'; -export type { ScaffolderUseTemplateSecrets } from './SecretsContext'; +export { ReviewState, type ReviewStateProps } from './ReviewState'; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx similarity index 99% rename from plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx rename to plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx index 2660eed47d..786f45b05d 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx @@ -20,7 +20,7 @@ import { renderInTestApp } from '@backstage/test-utils'; import { act, fireEvent } from '@testing-library/react'; import type { RJSFValidationError } from '@rjsf/utils'; import { JsonValue } from '@backstage/types'; -import { NextFieldExtensionComponentProps } from '../../../extensions/types'; +import { NextFieldExtensionComponentProps } from '../../extensions'; describe('Stepper', () => { it('should render the step titles for each step of the manifest', async () => { diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx similarity index 90% rename from plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx rename to plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index d4d57eed22..cde299da7e 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -13,11 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - useAnalytics, - useApiHolder, - useRouteRefParams, -} from '@backstage/core-plugin-api'; +import { useAnalytics, useApiHolder } from '@backstage/core-plugin-api'; import { JsonValue } from '@backstage/types'; import { Stepper as MuiStepper, @@ -29,14 +25,14 @@ import { import { type IChangeEvent, withTheme } from '@rjsf/core-v5'; import { ErrorSchema, FieldValidation } from '@rjsf/utils'; import React, { useCallback, useMemo, useState } from 'react'; -import { NextFieldExtensionOptions } from '../../../extensions'; +import { NextFieldExtensionOptions } from '../../extensions'; import { TemplateParameterSchema } from '../../../types'; import { createAsyncValidators } from './createAsyncValidators'; -import { useTemplateSchema } from './useTemplateSchema'; -import { ReviewState } from './ReviewState'; +import { useTemplateSchema } from '../../hooks/useTemplateSchema'; +import { ReviewState } from '../ReviewState'; import validator from '@rjsf/validator-ajv6'; -import { selectedTemplateRouteRef } from '../../../routes'; -import { useFormData } from './useFormData'; + +import { useFormDataFromQuery } from '../../hooks'; import { FormProps } from '../../types'; const useStyles = makeStyles(theme => ({ @@ -54,11 +50,18 @@ const useStyles = makeStyles(theme => ({ }, })); +/** + * The Props for {@link Stepper} component + * @alpha + */ export type StepperProps = { manifest: TemplateParameterSchema; extensions: NextFieldExtensionOptions[]; - onComplete: (values: Record) => Promise; + templateName?: string; FormProps?: FormProps; + initialState?: Record; + + onComplete: (values: Record) => Promise; }; // TODO(blam): We require here, as the types in this package depend on @rjsf/core explicitly @@ -66,13 +69,16 @@ export type StepperProps = { // of the re-writing we're doing. Once we've migrated, we can import this the exact same as before. 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) => { - const { templateName } = useRouteRefParams(selectedTemplateRouteRef); const analytics = useAnalytics(); const { steps } = useTemplateSchema(props.manifest); const apiHolder = useApiHolder(); const [activeStep, setActiveStep] = useState(0); - const [formState, setFormState] = useFormData(); + const [formState, setFormState] = useFormDataFromQuery(props.initialState); const [errors, setErrors] = useState< undefined | Record @@ -196,7 +202,7 @@ export const Stepper = (props: StepperProps) => { : undefined; analytics.captureEvent( 'create', - name || `new ${templateName}`, + name ?? props.templateName ?? 'unknown', ); }} > diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.test.ts b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts similarity index 98% rename from plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.test.ts rename to plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts index a4a937e6fe..6ad14e3cc4 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.test.ts +++ b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { JsonObject } from '@backstage/types'; -import { NextCustomFieldValidator } from '../../../extensions'; +import { NextCustomFieldValidator } from '../../extensions'; import { createAsyncValidators } from './createAsyncValidators'; describe('createAsyncValidators', () => { diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.ts b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts similarity index 94% rename from plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.ts rename to plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts index 6483ce2393..79466ee602 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.ts +++ b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts @@ -17,9 +17,9 @@ import { FieldValidation } from '@rjsf/utils'; import { JsonObject } from '@backstage/types'; import { ApiHolder } from '@backstage/core-plugin-api'; -import { NextCustomFieldValidator } from '../../../extensions'; import { Draft07 as JSONSchema } from 'json-schema-library'; -import { createFieldValidation } from './schema'; +import { createFieldValidation } from '../../lib'; +import { NextCustomFieldValidator } from '../../extensions'; export const createAsyncValidators = ( rootSchema: JsonObject, diff --git a/plugins/scaffolder-react/src/next/components/Stepper/index.ts b/plugins/scaffolder-react/src/next/components/Stepper/index.ts new file mode 100644 index 0000000000..daa47f078e --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/Stepper/index.ts @@ -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 { Stepper, type StepperProps } from './Stepper'; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx similarity index 100% rename from plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx rename to plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.tsx similarity index 100% rename from plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx rename to plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.tsx diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardLink.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/CardLink.tsx similarity index 100% rename from plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardLink.tsx rename to plugins/scaffolder-react/src/next/components/TemplateCard/CardLink.tsx diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx similarity index 91% rename from plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx rename to plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx index 4648d47af1..2d5b203cfb 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx @@ -25,9 +25,9 @@ import { } from '@backstage/test-utils'; import { TemplateCard } from './TemplateCard'; import React from 'react'; -import { nextRouteRef } from '../../../routes'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { fireEvent } from '@testing-library/react'; describe('TemplateCard', () => { it('should render the card title', async () => { @@ -54,7 +54,6 @@ describe('TemplateCard', () => { > , - { mountedRoutes: { '/': nextRouteRef } }, ); expect(getByText('bob')).toBeInTheDocument(); @@ -84,7 +83,6 @@ describe('TemplateCard', () => { > , - { mountedRoutes: { '/': nextRouteRef } }, ); const description = getByText('hello'); @@ -115,7 +113,6 @@ describe('TemplateCard', () => { > , - { mountedRoutes: { '/': nextRouteRef } }, ); expect(getByText('No description')).toBeInTheDocument(); @@ -145,7 +142,6 @@ describe('TemplateCard', () => { > , - { mountedRoutes: { '/': nextRouteRef } }, ); for (const tag of mockTemplate.metadata.tags!) { @@ -185,7 +181,6 @@ describe('TemplateCard', () => { , { mountedRoutes: { - '/': nextRouteRef, '/catalog/:kind/:namespace/:name': entityRouteRef, }, }, @@ -198,7 +193,7 @@ describe('TemplateCard', () => { ); }); - it('should render the choose button to navigate to the selected template', async () => { + it('should call the onSelected handler when clicking the choose button', async () => { const mockTemplate: TemplateEntityV1beta3 = { apiVersion: 'scaffolder.backstage.io/v1beta3', kind: 'Template', @@ -208,6 +203,7 @@ describe('TemplateCard', () => { type: 'service', }, }; + const mockOnSelected = jest.fn(); const { getByRole } = await renderInTestApp( { ], ]} > - + , { mountedRoutes: { - '/': nextRouteRef, '/catalog/:kind/:namespace/:name': entityRouteRef, }, }, ); expect(getByRole('button', { name: 'Choose' })).toBeInTheDocument(); - expect(getByRole('button', { name: 'Choose' })).toHaveAttribute( - 'href', - '/templates/default/bob', - ); + + fireEvent.click(getByRole('button', { name: 'Choose' })); + + expect(mockOnSelected).toHaveBeenCalledWith(mockTemplate); }); }); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.tsx similarity index 74% rename from plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx rename to plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.tsx index 67b50ba8d6..6f89808405 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.tsx @@ -13,14 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - parseEntityRef, - RELATION_OWNED_BY, - stringifyEntityRef, -} from '@backstage/catalog-model'; -import { Button, MarkdownContent, UserIcon } from '@backstage/core-components'; -import { IconComponent, useApp, useRouteRef } from '@backstage/core-plugin-api'; +import { RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { MarkdownContent, UserIcon } from '@backstage/core-components'; +import { IconComponent, useApp } from '@backstage/core-plugin-api'; import { EntityRefLinks, getEntityRelations, @@ -34,15 +29,12 @@ import { CardContent, Chip, Divider, + Button, Grid, makeStyles, } from '@material-ui/core'; import LanguageIcon from '@material-ui/icons/Language'; import React from 'react'; -import { - nextSelectedTemplateRouteRef, - viewTechDocRouteRef, -} from '../../../routes'; import { CardHeader } from './CardHeader'; import { CardLink } from './CardLink'; @@ -83,48 +75,32 @@ const useStyles = makeStyles(theme => ({ })); /** - * The Props for the Template Card component + * The Props for the {@link TemplateCard} component * @alpha */ export interface TemplateCardProps { template: TemplateEntityV1beta3; - deprecated?: boolean; + additionalLinks?: { + icon: IconComponent; + text: string; + url: string; + }[]; + + onSelected?: (template: TemplateEntityV1beta3) => void; } /** - * The Template Card component that is rendered in a list for each template + * The `TemplateCard` component that is rendered in a list for each template * @alpha */ export const TemplateCard = (props: TemplateCardProps) => { const { template } = props; const styles = useStyles(); const ownedByRelations = getEntityRelations(template, RELATION_OWNED_BY); - const templateRoute = useRouteRef(nextSelectedTemplateRouteRef); - const { name, namespace } = parseEntityRef( - stringifyEntityRef(props.template), - ); - const href = templateRoute({ - templateName: name, - namespace: namespace, - }); - const app = useApp(); const iconResolver = (key?: string): IconComponent => key ? app.getSystemIcon(key) ?? LanguageIcon : LanguageIcon; - // TechDocs Link - const viewTechDoc = useRouteRef(viewTechDocRouteRef); - const viewTechDocsAnnotation = - template.metadata.annotations?.['backstage.io/techdocs-ref']; - const viewTechDocsLink = - !!viewTechDocsAnnotation && - !!viewTechDoc && - viewTechDoc({ - namespace: template.metadata.namespace || DEFAULT_NAMESPACE, - kind: template.kind, - name: template.metadata.name, - }); - return ( @@ -159,22 +135,18 @@ export const TemplateCard = (props: TemplateCardProps) => { )} - {(!!viewTechDocsLink || template.metadata.links?.length) && ( + {(props.additionalLinks || template.metadata.links?.length) && ( <> - {viewTechDocsLink && ( + {props.additionalLinks?.map(({ icon, text, url }) => ( - + - )} + ))} {template.metadata.links?.map(({ url, icon, title }) => ( { )} - diff --git a/plugins/scaffolder-react/src/next/components/TemplateCard/index.ts b/plugins/scaffolder-react/src/next/components/TemplateCard/index.ts new file mode 100644 index 0000000000..529aa938e4 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/TemplateCard/index.ts @@ -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 { TemplateCard, type TemplateCardProps } from './TemplateCard'; diff --git a/plugins/scaffolder-react/src/next/components/TemplateGroup/TemplateGroup.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateGroup/TemplateGroup.test.tsx new file mode 100644 index 0000000000..02756839c9 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/TemplateGroup/TemplateGroup.test.tsx @@ -0,0 +1,176 @@ +/* + * 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. + */ +jest.mock('../TemplateCard', () => ({ TemplateCard: jest.fn(() => null) })); + +import React from 'react'; +import { TemplateGroup } from './TemplateGroup'; +import { render } from '@testing-library/react'; +import { TemplateCard } from '../TemplateCard'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; + +describe('TemplateGroup', () => { + it('should return a message when no templates are passed in', async () => { + const { getByText } = render( + , + ); + + expect( + getByText(/No templates found that match your filter/), + ).toBeInTheDocument(); + }); + + it('should render a card for each template with the template being passed as a prop', () => { + const mockOnSelected = jest.fn(); + const mockTemplates: { template: TemplateEntityV1beta3 }[] = [ + { + template: { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'test' }, + spec: { + parameters: [], + steps: [], + type: 'website', + }, + }, + }, + { + template: { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'test2' }, + spec: { + parameters: [], + steps: [], + type: 'service', + }, + }, + }, + ]; + + render( + , + ); + + expect(TemplateCard).toHaveBeenCalledTimes(2); + + for (const { template } of mockTemplates) { + expect(TemplateCard).toHaveBeenCalledWith( + expect.objectContaining({ template, onSelected: mockOnSelected }), + {}, + ); + } + }); + + it('should use the passed in TemplateCard prop to render the template card', () => { + const mockTemplateCardComponent = jest.fn(() => null); + const mockOnSelected = jest.fn(); + const mockTemplates: { template: TemplateEntityV1beta3 }[] = [ + { + template: { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'test' }, + spec: { + parameters: [], + steps: [], + type: 'website', + }, + }, + }, + { + template: { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'test2' }, + spec: { + parameters: [], + steps: [], + type: 'service', + }, + }, + }, + ]; + + render( + , + ); + + expect(mockTemplateCardComponent).toHaveBeenCalledTimes(2); + + for (const { template } of mockTemplates) { + expect(mockTemplateCardComponent).toHaveBeenCalledWith( + expect.objectContaining({ + onSelected: mockOnSelected, + template, + }), + {}, + ); + } + }); + + it('should render the title when no templates passed', () => { + const { getByText } = render( + , + ); + expect(getByText('Test')).toBeInTheDocument(); + }); + + it('should render the title when there are templates in the list', () => { + const mockTemplates: { template: TemplateEntityV1beta3 }[] = [ + { + template: { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'test' }, + spec: { parameters: [], steps: [], type: 'website' }, + }, + }, + ]; + + const { getByText } = render( + , + ); + + expect(getByText('Test')).toBeInTheDocument(); + }); + + it('should allow for passing through a user given title component', () => { + const TitleComponent =

Im a custom header

; + const { getByText } = render( + , + ); + + expect(getByText('Im a custom header')).toBeInTheDocument(); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx b/plugins/scaffolder-react/src/next/components/TemplateGroup/TemplateGroup.tsx similarity index 67% rename from plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx rename to plugins/scaffolder-react/src/next/components/TemplateGroup/TemplateGroup.tsx index 4344c195ae..8134ea256d 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateGroup/TemplateGroup.tsx @@ -22,19 +22,41 @@ import { Link, } from '@backstage/core-components'; import { Typography } from '@material-ui/core'; -import { TemplateCard, TemplateCardProps } from './TemplateCard'; import { stringifyEntityRef } from '@backstage/catalog-model'; +import { TemplateCardProps, TemplateCard } from '../TemplateCard'; +import { IconComponent } from '@backstage/core-plugin-api'; +/** + * The props for the {@link TemplateGroup} component. + * @alpha + */ export interface TemplateGroupProps { - templates: TemplateEntityV1beta3[]; + templates: { + template: TemplateEntityV1beta3; + additionalLinks?: { + icon: IconComponent; + text: string; + url: string; + }[]; + }[]; + onSelected: (template: TemplateEntityV1beta3) => void; title: React.ReactNode; components?: { CardComponent?: React.ComponentType; }; } +/** + * The `TemplateGroup` component is used to display a group of templates with a title. + * @alpha + */ export const TemplateGroup = (props: TemplateGroupProps) => { - const { templates, title, components: { CardComponent } = {} } = props; + const { + templates, + title, + components: { CardComponent } = {}, + onSelected, + } = props; const titleComponent = typeof title === 'string' ? : title; @@ -59,8 +81,13 @@ export const TemplateGroup = (props: TemplateGroupProps) => { {titleComponent} - {templates.map(template => ( - + {templates.map(({ template, additionalLinks }) => ( + ))} diff --git a/plugins/scaffolder-react/src/next/components/TemplateGroup/index.ts b/plugins/scaffolder-react/src/next/components/TemplateGroup/index.ts new file mode 100644 index 0000000000..3fd27c84ed --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/TemplateGroup/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { TemplateGroup, type TemplateGroupProps } from './TemplateGroup'; diff --git a/plugins/scaffolder-react/src/next/components/index.ts b/plugins/scaffolder-react/src/next/components/index.ts new file mode 100644 index 0000000000..c710bd8891 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/index.ts @@ -0,0 +1,19 @@ +/* + * 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 * from './Stepper'; +export * from './TemplateCard'; +export * from './ReviewState'; +export * from './TemplateGroup'; diff --git a/plugins/scaffolder-react/src/next/extensions/index.tsx b/plugins/scaffolder-react/src/next/extensions/index.tsx new file mode 100644 index 0000000000..ee9b1cd326 --- /dev/null +++ b/plugins/scaffolder-react/src/next/extensions/index.tsx @@ -0,0 +1,57 @@ +/* + * 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 { + NextCustomFieldValidator, + NextFieldExtensionOptions, + NextFieldExtensionComponentProps, +} from './types'; +import { Extension, attachComponentData } from '@backstage/core-plugin-api'; +import { UIOptionsType } from '@rjsf/utils'; +import { FieldExtensionComponent } from '../../extensions'; +import { FIELD_EXTENSION_KEY } from '../../extensions/keys'; + +/** + * Method for creating field extensions that can be used in the scaffolder + * frontend form. + * @alpha + */ +export function createNextScaffolderFieldExtension< + TReturnValue = unknown, + TInputProps extends UIOptionsType = {}, +>( + options: NextFieldExtensionOptions, +): Extension> { + return { + expose() { + const FieldExtensionDataHolder: any = () => null; + + attachComponentData( + FieldExtensionDataHolder, + FIELD_EXTENSION_KEY, + options, + ); + + return FieldExtensionDataHolder; + }, + }; +} + +export type { + NextCustomFieldValidator, + NextFieldExtensionOptions, + NextFieldExtensionComponentProps, +}; diff --git a/plugins/scaffolder/src/extensions/types.ts b/plugins/scaffolder-react/src/next/extensions/types.ts similarity index 60% rename from plugins/scaffolder/src/extensions/types.ts rename to plugins/scaffolder-react/src/next/extensions/types.ts index 5e133c676b..27778b0420 100644 --- a/plugins/scaffolder/src/extensions/types.ts +++ b/plugins/scaffolder-react/src/next/extensions/types.ts @@ -14,7 +14,6 @@ * limitations under the License. */ import { ApiHolder } from '@backstage/core-plugin-api'; -import { FieldValidation, FieldProps } from '@rjsf/core'; import { UIOptionsType, FieldProps as FieldPropsV5, @@ -22,62 +21,8 @@ import { FieldValidation as FieldValidationV5, } from '@rjsf/utils'; import { PropsWithChildren } from 'react'; -import { JSONSchema7 } from 'json-schema'; import { JsonObject } from '@backstage/types'; - -/** - * Field validation type for Custom Field Extensions. - * - * @public - */ -export type CustomFieldValidator = ( - data: TFieldReturnValue, - field: FieldValidation, - context: { apiHolder: ApiHolder }, -) => void | Promise; - -/** - * Type for the Custom Field Extension schema. - * - * @public - */ -export type CustomFieldExtensionSchema = { - returnValue: JSONSchema7; - uiOptions?: JSONSchema7; -}; - -/** - * Type for the Custom Field Extension with the - * name and components and validation function. - * - * @public - */ -export type FieldExtensionOptions< - TFieldReturnValue = unknown, - TInputProps = unknown, -> = { - name: string; - component: ( - props: FieldExtensionComponentProps, - ) => JSX.Element | null; - validation?: CustomFieldValidator; - schema?: CustomFieldExtensionSchema; -}; - -/** - * Type for field extensions and being able to type - * incoming props easier. - * - * @public - */ -export interface FieldExtensionComponentProps< - TFieldReturnValue, - TUiOptions extends {} = {}, -> extends FieldProps { - uiSchema: FieldProps['uiSchema'] & { - 'ui:options'?: TUiOptions; - }; -} +import { CustomFieldExtensionSchema } from '../../extensions'; /** * Type for Field Extension Props for RJSF v5 diff --git a/plugins/scaffolder-react/src/next/hooks/index.ts b/plugins/scaffolder-react/src/next/hooks/index.ts new file mode 100644 index 0000000000..78a4d2a777 --- /dev/null +++ b/plugins/scaffolder-react/src/next/hooks/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { useFormDataFromQuery } from './useFormDataFromQuery'; +export { + useTemplateSchema, + type ParsedTemplateSchema, +} from './useTemplateSchema'; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useFormData.ts b/plugins/scaffolder-react/src/next/hooks/useFormDataFromQuery.ts similarity index 76% rename from plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useFormData.ts rename to plugins/scaffolder-react/src/next/hooks/useFormDataFromQuery.ts index 1500216688..966a8fe8f4 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useFormData.ts +++ b/plugins/scaffolder-react/src/next/hooks/useFormDataFromQuery.ts @@ -14,11 +14,22 @@ * limitations under the License. */ +import { JsonValue } from '@backstage/types'; import qs from 'qs'; import { useState } from 'react'; -export const useFormData = () => { +/** + * This hook is used to get the formData from the query string. + * @alpha + */ +export const useFormDataFromQuery = ( + initialState?: Record, +) => { return useState>(() => { + if (initialState) { + return initialState; + } + const query = qs.parse(window.location.search, { ignoreQueryPrefix: true, }); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.test.tsx b/plugins/scaffolder-react/src/next/hooks/useTemplateSchema.test.tsx similarity index 99% rename from plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.test.tsx rename to plugins/scaffolder-react/src/next/hooks/useTemplateSchema.test.tsx index 9be6faa0fa..153c48468d 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.test.tsx +++ b/plugins/scaffolder-react/src/next/hooks/useTemplateSchema.test.tsx @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { TemplateParameterSchema } from '../../../types'; import { useTemplateSchema } from './useTemplateSchema'; import { renderHook } from '@testing-library/react-hooks'; import { TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { featureFlagsApiRef } from '@backstage/core-plugin-api'; +import { TemplateParameterSchema } from '../../types'; describe('useTemplateSchema', () => { it('should generate the correct schema', () => { diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts b/plugins/scaffolder-react/src/next/hooks/useTemplateSchema.ts similarity index 84% rename from plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts rename to plugins/scaffolder-react/src/next/hooks/useTemplateSchema.ts index 6aad6baa96..2b2e48cf68 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts +++ b/plugins/scaffolder-react/src/next/hooks/useTemplateSchema.ts @@ -16,9 +16,13 @@ import { featureFlagsApiRef, useApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { UiSchema } from '@rjsf/utils'; -import { TemplateParameterSchema } from '../../../types'; -import { extractSchemaFromStep } from './schema'; +import { TemplateParameterSchema } from '../../types'; +import { extractSchemaFromStep } from '../lib'; +/** + * This is the parsed template schema that is returned from the {@link useTemplateSchema} hook. + * @alpha + */ export interface ParsedTemplateSchema { uiSchema: UiSchema; mergedSchema: JsonObject; @@ -26,6 +30,13 @@ export interface ParsedTemplateSchema { title: string; description?: string; } + +/** + * This hook will parse the template schema and return the steps with the + * parsed schema and uiSchema. Filtering out any steps or properties that + * are not enabled with feature flags. + * @alpha + */ export const useTemplateSchema = ( manifest: TemplateParameterSchema, ): { steps: ParsedTemplateSchema[] } => { diff --git a/plugins/scaffolder-react/src/next/index.ts b/plugins/scaffolder-react/src/next/index.ts new file mode 100644 index 0000000000..a18649f506 --- /dev/null +++ b/plugins/scaffolder-react/src/next/index.ts @@ -0,0 +1,20 @@ +/* + * 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 * from './components'; +export * from './extensions'; +export * from './types'; +export * from './lib'; +export * from './hooks'; diff --git a/plugins/scaffolder-react/src/next/lib/index.ts b/plugins/scaffolder-react/src/next/lib/index.ts new file mode 100644 index 0000000000..f42c4c8ab3 --- /dev/null +++ b/plugins/scaffolder-react/src/next/lib/index.ts @@ -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 { extractSchemaFromStep, createFieldValidation } from './schema'; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts b/plugins/scaffolder-react/src/next/lib/schema.test.ts similarity index 100% rename from plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts rename to plugins/scaffolder-react/src/next/lib/schema.test.ts diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts b/plugins/scaffolder-react/src/next/lib/schema.ts similarity index 100% rename from plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts rename to plugins/scaffolder-react/src/next/lib/schema.ts index 4c647bc045..e8b819be4b 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts +++ b/plugins/scaffolder-react/src/next/lib/schema.ts @@ -99,8 +99,8 @@ function extractUiSchema(schema: JsonObject, uiSchema: JsonObject) { } /** - * @alpha * Takes a step from a Backstage Template Manifest and converts it to a JSON Schema and UI Schema for rjsf + * @alpha */ export const extractSchemaFromStep = ( inputStep: JsonObject, @@ -112,8 +112,8 @@ export const extractSchemaFromStep = ( }; /** - * @alpha * Creates a field validation object for use in react jsonschema form + * @alpha */ export const createFieldValidation = (): FieldValidation => { const fieldValidation: FieldValidation = { diff --git a/plugins/scaffolder-react/src/next/types.ts b/plugins/scaffolder-react/src/next/types.ts new file mode 100644 index 0000000000..32d0fe85f0 --- /dev/null +++ b/plugins/scaffolder-react/src/next/types.ts @@ -0,0 +1,26 @@ +/* + * 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 as SchemaFormProps } from '@rjsf/core-v5'; + +/** + * Any `@rjsf/core` form properties that are publicly exposed to the `NextScaffolderpage` + * + * @alpha + */ +export type FormProps = Pick< + SchemaFormProps, + 'transformErrors' | 'noHtml5Validate' +>; diff --git a/plugins/scaffolder/src/components/secrets/SecretsContext.test.tsx b/plugins/scaffolder-react/src/secrets/SecretsContext.test.tsx similarity index 77% rename from plugins/scaffolder/src/components/secrets/SecretsContext.test.tsx rename to plugins/scaffolder-react/src/secrets/SecretsContext.test.tsx index 3b84c074d7..593fbd908b 100644 --- a/plugins/scaffolder/src/components/secrets/SecretsContext.test.tsx +++ b/plugins/scaffolder-react/src/secrets/SecretsContext.test.tsx @@ -13,12 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useContext } from 'react'; -import { - useTemplateSecrets, - SecretsContextProvider, - SecretsContext, -} from './SecretsContext'; +import React from 'react'; +import { useTemplateSecrets, SecretsContextProvider } from './SecretsContext'; import { renderHook, act } from '@testing-library/react-hooks'; describe('SecretsContext', () => { @@ -26,7 +22,6 @@ describe('SecretsContext', () => { const { result } = renderHook( () => ({ hook: useTemplateSecrets(), - context: useContext(SecretsContext), }), { wrapper: ({ children }) => ( @@ -34,10 +29,10 @@ describe('SecretsContext', () => { ), }, ); - expect(result.current.context?.secrets.foo).toEqual(undefined); + expect(result.current.hook?.secrets.foo).toEqual(undefined); act(() => result.current.hook.setSecrets({ foo: 'bar' })); - expect(result.current.context?.secrets.foo).toEqual('bar'); + expect(result.current.hook?.secrets.foo).toEqual('bar'); }); }); diff --git a/plugins/scaffolder/src/components/secrets/SecretsContext.tsx b/plugins/scaffolder-react/src/secrets/SecretsContext.tsx similarity index 71% rename from plugins/scaffolder/src/components/secrets/SecretsContext.tsx rename to plugins/scaffolder-react/src/secrets/SecretsContext.tsx index b425a72233..af5c2393cd 100644 --- a/plugins/scaffolder/src/components/secrets/SecretsContext.tsx +++ b/plugins/scaffolder-react/src/secrets/SecretsContext.tsx @@ -13,36 +13,43 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { + createVersionedContext, + createVersionedValueMap, +} from '@backstage/version-bridge'; import React, { useState, useCallback, useContext, - createContext, PropsWithChildren, } from 'react'; +/** + * The contents of the `SecretsContext` + */ type SecretsContextContents = { secrets: Record; setSecrets: React.Dispatch>>; }; /** - * The actual context object. + * The context to hold the Secrets. */ -export const SecretsContext = createContext( - undefined, -); +const SecretsContext = createVersionedContext<{ + 1: SecretsContextContents; +}>('secrets-context'); /** * The Context Provider that holds the state for the secrets. - * * @public */ export const SecretsContextProvider = ({ children }: PropsWithChildren<{}>) => { const [secrets, setSecrets] = useState>({}); return ( - + {children} ); @@ -54,21 +61,24 @@ export const SecretsContextProvider = ({ children }: PropsWithChildren<{}>) => { */ export interface ScaffolderUseTemplateSecrets { setSecrets: (input: Record) => void; + secrets: Record; } /** - * Hook to access the secrets context. + * Hook to access the secrets context to be able to set secrets that are + * passed to the Scaffolder backend. * @public */ export const useTemplateSecrets = (): ScaffolderUseTemplateSecrets => { - const value = useContext(SecretsContext); + const value = useContext(SecretsContext)?.atVersion(1); + if (!value) { throw new Error( 'useTemplateSecrets must be used within a SecretsContextProvider', ); } - const { setSecrets: updateSecrets } = value; + const { setSecrets: updateSecrets, secrets = {} } = value; const setSecrets = useCallback( (input: Record) => { @@ -77,5 +87,5 @@ export const useTemplateSecrets = (): ScaffolderUseTemplateSecrets => { [updateSecrets], ); - return { setSecrets }; + return { setSecrets, secrets }; }; diff --git a/plugins/scaffolder-react/src/secrets/index.ts b/plugins/scaffolder-react/src/secrets/index.ts new file mode 100644 index 0000000000..0b8a09cd97 --- /dev/null +++ b/plugins/scaffolder-react/src/secrets/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { + useTemplateSecrets, + SecretsContextProvider, + type ScaffolderUseTemplateSecrets, +} from './SecretsContext'; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/index.ts b/plugins/scaffolder-react/src/setupTests.ts similarity index 94% rename from plugins/scaffolder/src/next/TemplateWizardPage/Stepper/index.ts rename to plugins/scaffolder-react/src/setupTests.ts index 5532563e28..326d2ed212 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/index.ts +++ b/plugins/scaffolder-react/src/setupTests.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { Stepper } from './Stepper'; +import '@testing-library/jest-dom'; diff --git a/plugins/scaffolder-react/src/types.ts b/plugins/scaffolder-react/src/types.ts new file mode 100644 index 0000000000..f644fbeb64 --- /dev/null +++ b/plugins/scaffolder-react/src/types.ts @@ -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 { JsonObject } from '@backstage/types'; + +/** + * The shape of each entry of parameters which gets rendered + * as a separate step in the wizard input + * + * @public + */ +export type TemplateParameterSchema = { + title: string; + description?: string; + steps: Array<{ + title: string; + description?: string; + schema: JsonObject; + }>; +}; diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 0a881b4519..82a8dfd8d3 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -9,96 +9,73 @@ import { ApiHolder } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; +import { createScaffolderFieldExtension as createScaffolderFieldExtension_2 } from '@backstage/plugin-scaffolder-react'; +import { CustomFieldExtensionSchema as CustomFieldExtensionSchema_2 } from '@backstage/plugin-scaffolder-react'; +import { CustomFieldValidator as CustomFieldValidator_2 } from '@backstage/plugin-scaffolder-react'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { Extension } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; -import { FieldProps } from '@rjsf/core'; -import { FieldProps as FieldProps_2 } from '@rjsf/utils'; +import { FieldExtensionComponent as FieldExtensionComponent_2 } from '@backstage/plugin-scaffolder-react'; +import { FieldExtensionComponentProps as FieldExtensionComponentProps_2 } from '@backstage/plugin-scaffolder-react'; +import { FieldExtensionOptions as FieldExtensionOptions_2 } from '@backstage/plugin-scaffolder-react'; import { FieldValidation } from '@rjsf/core'; -import { FieldValidation as FieldValidation_2 } from '@rjsf/utils'; import type { FormProps as FormProps_2 } from '@rjsf/core'; import type { FormProps as FormProps_3 } from '@rjsf/core-v5'; import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; -import { JSONSchema7 } from 'json-schema'; -import { JsonValue } from '@backstage/types'; +import { ListActionsResponse as ListActionsResponse_2 } from '@backstage/plugin-scaffolder-react'; +import { LogEvent as LogEvent_2 } from '@backstage/plugin-scaffolder-react'; import { Observable } from '@backstage/types'; import { PathParams } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; +import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; +import { ScaffolderApi as ScaffolderApi_2 } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderDryRunOptions as ScaffolderDryRunOptions_2 } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderDryRunResponse as ScaffolderDryRunResponse_2 } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderGetIntegrationsListOptions as ScaffolderGetIntegrationsListOptions_2 } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderGetIntegrationsListResponse as ScaffolderGetIntegrationsListResponse_2 } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderOutputLink } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderScaffoldOptions as ScaffolderScaffoldOptions_2 } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderScaffoldResponse as ScaffolderScaffoldResponse_2 } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderStreamLogsOptions as ScaffolderStreamLogsOptions_2 } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderTask as ScaffolderTask_2 } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderTaskOutput as ScaffolderTaskOutput_2 } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderTaskStatus as ScaffolderTaskStatus_2 } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderUseTemplateSecrets as ScaffolderUseTemplateSecrets_2 } from '@backstage/plugin-scaffolder-react'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { SubRouteRef } from '@backstage/core-plugin-api'; -import { TaskSpec } from '@backstage/plugin-scaffolder-common'; -import { TaskStep } from '@backstage/plugin-scaffolder-common'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { UIOptionsType } from '@rjsf/utils'; +import { TemplateParameterSchema as TemplateParameterSchema_2 } from '@backstage/plugin-scaffolder-react'; import { UiSchema } from '@rjsf/utils'; import { z } from 'zod'; -// @public -export type Action = { - id: string; - description?: string; - schema?: { - input?: JSONSchema7; - output?: JSONSchema7; - }; - examples?: ActionExample[]; -}; - -// @public -export type ActionExample = { - description: string; - example: string; -}; - -// @alpha -export function createNextScaffolderFieldExtension< - TReturnValue = unknown, - TInputProps extends UIOptionsType = {}, ->( - options: NextFieldExtensionOptions, -): Extension>; - -// @public -export function createScaffolderFieldExtension< - TReturnValue = unknown, - TInputProps = unknown, ->( - options: FieldExtensionOptions, -): Extension>; +// @public @deprecated (undocumented) +export const createScaffolderFieldExtension: typeof createScaffolderFieldExtension_2; // @public export function createScaffolderLayout( options: LayoutOptions, ): Extension>; -// @public -export type CustomFieldExtensionSchema = { - returnValue: JSONSchema7; - uiOptions?: JSONSchema7; -}; +// @public @deprecated (undocumented) +export type CustomFieldExtensionSchema = CustomFieldExtensionSchema_2; + +// @public @deprecated (undocumented) +export type CustomFieldValidator = + CustomFieldValidator_2; // @public -export type CustomFieldValidator = ( - data: TFieldReturnValue, - field: FieldValidation, - context: { - apiHolder: ApiHolder; - }, -) => void | Promise; - -// @public -export const EntityNamePickerFieldExtension: FieldExtensionComponent< +export const EntityNamePickerFieldExtension: FieldExtensionComponent_2< string, {} >; // @public -export const EntityPickerFieldExtension: FieldExtensionComponent< +export const EntityPickerFieldExtension: FieldExtensionComponent_2< string, { defaultKind?: string | undefined; @@ -132,7 +109,7 @@ export type EntityPickerUiOptions = typeof EntityPickerFieldSchema.uiOptionsType; // @public -export const EntityTagsPickerFieldExtension: FieldExtensionComponent< +export const EntityTagsPickerFieldExtension: FieldExtensionComponent_2< string[], { showCounts?: boolean | undefined; @@ -155,44 +132,30 @@ export const EntityTagsPickerFieldSchema: FieldSchema< export type EntityTagsPickerUiOptions = typeof EntityTagsPickerFieldSchema.uiOptionsType; -// @public -export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; +// @public @deprecated (undocumented) +export type FieldExtensionComponent<_TReturnValue, _TInputProps> = + FieldExtensionComponent_2<_TReturnValue, _TInputProps>; -// @public -export interface FieldExtensionComponentProps< +// @public @deprecated (undocumented) +export type FieldExtensionComponentProps< TFieldReturnValue, TUiOptions extends {} = {}, -> extends FieldProps { - // (undocumented) - uiSchema: FieldProps['uiSchema'] & { - 'ui:options'?: TUiOptions; - }; -} +> = FieldExtensionComponentProps_2; -// @public -export type FieldExtensionOptions< - TFieldReturnValue = unknown, - TInputProps = unknown, -> = { - name: string; - component: ( - props: FieldExtensionComponentProps, - ) => JSX.Element | null; - validation?: CustomFieldValidator; - schema?: CustomFieldExtensionSchema; -}; +// @public @deprecated (undocumented) +export type FieldExtensionOptions = FieldExtensionOptions_2; // @public export interface FieldSchema { // (undocumented) - readonly schema: CustomFieldExtensionSchema; + readonly schema: CustomFieldExtensionSchema_2; // (undocumented) - readonly type: FieldExtensionComponentProps; + readonly type: FieldExtensionComponentProps_2; // (undocumented) readonly uiOptionsType: TUiOptions; } -// @alpha +// @alpha @deprecated export type FormProps = Pick< FormProps_3, 'transformErrors' | 'noHtml5Validate' @@ -212,21 +175,11 @@ export interface LayoutOptions

{ // @public export type LayoutTemplate = FormProps_2['ObjectFieldTemplate']; -// @public -export type ListActionsResponse = Array; +// @public @deprecated (undocumented) +export type ListActionsResponse = ListActionsResponse_2; -// @public -export type LogEvent = { - type: 'log' | 'completion'; - body: { - message: string; - stepId?: string; - status?: ScaffolderTaskStatus; - }; - createdAt: string; - id: string; - taskId: string; -}; +// @public @deprecated (undocumented) +export type LogEvent = LogEvent_2; // @public export function makeFieldSchemaFromZod< @@ -242,40 +195,6 @@ export function makeFieldSchemaFromZod< : never >; -// @alpha -export type NextCustomFieldValidator = ( - data: TFieldReturnValue, - field: FieldValidation_2, - context: { - apiHolder: ApiHolder; - formData: JsonObject; - }, -) => void | Promise; - -// @alpha -export interface NextFieldExtensionComponentProps< - TFieldReturnValue, - TUiOptions = {}, -> extends PropsWithChildren> { - // (undocumented) - uiSchema?: UiSchema & { - 'ui:options'?: TUiOptions & UIOptionsType; - }; -} - -// @alpha -export type NextFieldExtensionOptions< - TFieldReturnValue = unknown, - TInputProps = unknown, -> = { - name: string; - component: ( - props: NextFieldExtensionComponentProps, - ) => JSX.Element | null; - validation?: NextCustomFieldValidator; - schema?: CustomFieldExtensionSchema; -}; - // @alpha (undocumented) export const nextRouteRef: RouteRef; @@ -296,13 +215,18 @@ export const NextScaffolderPage: ( props: PropsWithChildren, ) => JSX.Element; +// @alpha (undocumented) +export const nextScaffolderTaskRouteRef: SubRouteRef< + PathParams<'/tasks/:taskId'> +>; + // @alpha (undocumented) export const nextSelectedTemplateRouteRef: SubRouteRef< PathParams<'/templates/:namespace/:templateName'> >; // @public -export const OwnedEntityPickerFieldExtension: FieldExtensionComponent< +export const OwnedEntityPickerFieldExtension: FieldExtensionComponent_2< string, { defaultKind?: string | undefined; @@ -328,7 +252,7 @@ export type OwnedEntityPickerUiOptions = typeof OwnedEntityPickerFieldSchema.uiOptionsType; // @public -export const OwnerPickerFieldExtension: FieldExtensionComponent< +export const OwnerPickerFieldExtension: FieldExtensionComponent_2< string, { defaultNamespace?: string | false | undefined; @@ -368,14 +292,14 @@ export const repoPickerValidation: ( ) => void; // @public -export const RepoUrlPickerFieldExtension: FieldExtensionComponent< +export const RepoUrlPickerFieldExtension: FieldExtensionComponent_2< string, { - allowedOwners?: string[] | undefined; + allowedHosts?: string[] | undefined; allowedOrganizations?: string[] | undefined; + allowedOwners?: string[] | undefined; allowedProjects?: string[] | undefined; allowedRepos?: string[] | undefined; - allowedHosts?: string[] | undefined; requestUserCredentials?: | { additionalScopes?: @@ -397,11 +321,11 @@ export const RepoUrlPickerFieldExtension: FieldExtensionComponent< export const RepoUrlPickerFieldSchema: FieldSchema< string, { - allowedOwners?: string[] | undefined; + allowedHosts?: string[] | undefined; allowedOrganizations?: string[] | undefined; + allowedOwners?: string[] | undefined; allowedProjects?: string[] | undefined; allowedRepos?: string[] | undefined; - allowedHosts?: string[] | undefined; requestUserCredentials?: | { additionalScopes?: @@ -437,7 +361,7 @@ export type ReviewStepProps = { }[]; }; -// @public (undocumented) +// @public @deprecated (undocumented) export const rootRouteRef: RouteRef; // @public @@ -467,37 +391,14 @@ export type RouterProps = { }; }; -// @public -export interface ScaffolderApi { - // (undocumented) - dryRun?(options: ScaffolderDryRunOptions): Promise; - // (undocumented) - getIntegrationsList( - options: ScaffolderGetIntegrationsListOptions, - ): Promise; - // (undocumented) - getTask(taskId: string): Promise; - // (undocumented) - getTemplateParameterSchema( - templateRef: string, - ): Promise; - listActions(): Promise; - // (undocumented) - listTasks?(options: { filterByOwnership: 'owned' | 'all' }): Promise<{ - tasks: ScaffolderTask[]; - }>; - scaffold( - options: ScaffolderScaffoldOptions, - ): Promise; - // (undocumented) - streamLogs(options: ScaffolderStreamLogsOptions): Observable; -} +// @public @deprecated (undocumented) +export type ScaffolderApi = ScaffolderApi_2; + +// @public @deprecated (undocumented) +export const scaffolderApiRef: ApiRef; // @public -export const scaffolderApiRef: ApiRef; - -// @public -export class ScaffolderClient implements ScaffolderApi { +export class ScaffolderClient implements ScaffolderApi_2 { constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi; @@ -506,92 +407,57 @@ export class ScaffolderClient implements ScaffolderApi { useLongPollingLogs?: boolean; }); // (undocumented) - dryRun(options: ScaffolderDryRunOptions): Promise; + dryRun( + options: ScaffolderDryRunOptions_2, + ): Promise; // (undocumented) getIntegrationsList( - options: ScaffolderGetIntegrationsListOptions, - ): Promise; + options: ScaffolderGetIntegrationsListOptions_2, + ): Promise; // (undocumented) - getTask(taskId: string): Promise; + getTask(taskId: string): Promise; // (undocumented) getTemplateParameterSchema( templateRef: string, - ): Promise; + ): Promise; // (undocumented) - listActions(): Promise; + listActions(): Promise; // (undocumented) listTasks(options: { filterByOwnership: 'owned' | 'all' }): Promise<{ - tasks: ScaffolderTask[]; + tasks: ScaffolderTask_2[]; }>; + // (undocumented) scaffold( - options: ScaffolderScaffoldOptions, - ): Promise; + options: ScaffolderScaffoldOptions_2, + ): Promise; // (undocumented) - streamLogs(options: ScaffolderStreamLogsOptions): Observable; + streamLogs(options: ScaffolderStreamLogsOptions_2): Observable; } -// @public (undocumented) -export interface ScaffolderDryRunOptions { - // (undocumented) - directoryContents: { - path: string; - base64Content: string; - }[]; - // (undocumented) - secrets?: Record; - // (undocumented) - template: JsonValue; - // (undocumented) - values: JsonObject; -} +// @public @deprecated (undocumented) +export type ScaffolderDryRunOptions = ScaffolderDryRunOptions_2; -// @public (undocumented) -export interface ScaffolderDryRunResponse { - // (undocumented) - directoryContents: Array<{ - path: string; - base64Content: string; - executable: boolean; - }>; - // (undocumented) - log: Array>; - // (undocumented) - output: ScaffolderTaskOutput; - // (undocumented) - steps: TaskStep[]; -} +// @public @deprecated (undocumented) +export type ScaffolderDryRunResponse = ScaffolderDryRunResponse_2; -// @public -export const ScaffolderFieldExtensions: React_2.ComponentType< - React_2.PropsWithChildren<{}> ->; +// @public @deprecated (undocumented) +export const ScaffolderFieldExtensions: ComponentType<{ + children?: ReactNode; +}>; -// @public -export interface ScaffolderGetIntegrationsListOptions { - // (undocumented) - allowedHosts: string[]; -} +// @public @deprecated (undocumented) +export type ScaffolderGetIntegrationsListOptions = + ScaffolderGetIntegrationsListOptions_2; -// @public -export interface ScaffolderGetIntegrationsListResponse { - // (undocumented) - integrations: { - type: string; - title: string; - host: string; - }[]; -} +// @public @deprecated (undocumented) +export type ScaffolderGetIntegrationsListResponse = + ScaffolderGetIntegrationsListResponse_2; // @public export const ScaffolderLayouts: React.ComponentType; -// @public (undocumented) -export type ScaffolderOutputLink = { - title?: string; - icon?: string; - url?: string; - entityRef?: string; -}; +// @public @deprecated (undocumented) +export type ScaffolderOutputlink = ScaffolderOutputLink; // @public export const ScaffolderPage: (props: RouterProps) => JSX.Element; @@ -600,6 +466,10 @@ export const ScaffolderPage: (props: RouterProps) => JSX.Element; export const scaffolderPlugin: BackstagePlugin< { root: RouteRef; + selectedTemplate: SubRouteRef< + PathParams<'/templates/:namespace/:templateName'> + >; + ongoingTask: SubRouteRef>; }, { registerComponent: ExternalRouteRef; @@ -615,64 +485,26 @@ export const scaffolderPlugin: BackstagePlugin< {} >; -// @public -export interface ScaffolderScaffoldOptions { - // (undocumented) - secrets?: Record; - // (undocumented) - templateRef: string; - // (undocumented) - values: Record; -} +// @public @deprecated (undocumented) +export type ScaffolderScaffoldOptions = ScaffolderScaffoldOptions_2; -// @public -export interface ScaffolderScaffoldResponse { - // (undocumented) - taskId: string; -} +// @public @deprecated (undocumented) +export type ScaffolderScaffoldResponse = ScaffolderScaffoldResponse_2; -// @public -export interface ScaffolderStreamLogsOptions { - // (undocumented) - after?: number; - // (undocumented) - taskId: string; -} +// @public @deprecated (undocumented) +export type ScaffolderStreamLogsOptions = ScaffolderStreamLogsOptions_2; -// @public -export type ScaffolderTask = { - id: string; - spec: TaskSpec; - status: 'failed' | 'completed' | 'processing' | 'open' | 'cancelled'; - lastHeartbeatAt: string; - createdAt: string; -}; +// @public @deprecated (undocumented) +export type ScaffolderTask = ScaffolderTask_2; -// @public (undocumented) -export type ScaffolderTaskOutput = { - links?: ScaffolderOutputLink[]; -} & { - [key: string]: unknown; -}; +// @public @deprecated (undocumented) +export type ScaffolderTaskOutput = ScaffolderTaskOutput_2; -// @public -export type ScaffolderTaskStatus = - | 'open' - | 'processing' - | 'failed' - | 'completed' - | 'skipped'; +// @public @deprecated (undocumented) +export type ScaffolderTaskStatus = ScaffolderTaskStatus_2; -// @public -export interface ScaffolderUseTemplateSecrets { - // (undocumented) - setSecrets: (input: Record) => void; -} - -// @public (undocumented) -export const selectedTemplateRouteRef: SubRouteRef< - PathParams<'/templates/:namespace/:templateName'> ->; +// @public @deprecated (undocumented) +export type ScaffolderUseTemplateSecrets = ScaffolderUseTemplateSecrets_2; // @public export const TaskPage: ({ loadingText }: TaskPageProps) => JSX.Element; @@ -688,20 +520,12 @@ export type TemplateGroupFilter = { filter: (entity: Entity) => boolean; }; -// @public -export type TemplateParameterSchema = { - title: string; - description?: string; - steps: Array<{ - title: string; - description?: string; - schema: JsonObject; - }>; -}; +// @public @deprecated (undocumented) +export type TemplateParameterSchema = TemplateParameterSchema_2; // @public export const TemplateTypePicker: () => JSX.Element | null; -// @public -export const useTemplateSecrets: () => ScaffolderUseTemplateSecrets; +// @public @deprecated (undocumented) +export const useTemplateSecrets: () => ScaffolderUseTemplateSecrets_2; ``` diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index fd463c5d33..0a9f476f11 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -45,6 +45,7 @@ "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", + "@backstage/plugin-scaffolder-react": "workspace:^", "@backstage/theme": "workspace:^", "@backstage/types": "workspace:^", "@codemirror/language": "^6.0.0", @@ -57,10 +58,7 @@ "@rjsf/core": "^3.2.1", "@rjsf/core-v5": "npm:@rjsf/core@^5.0.0-beta.14", "@rjsf/material-ui": "^3.2.1", - "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@^5.0.0-beta.14", "@rjsf/utils": "^5.0.0-beta.14", - "@rjsf/validator-ajv6": "^5.0.0-beta.14", - "@types/json-schema": "^7.0.9", "@uiw/react-codemirror": "^4.9.3", "classnames": "^2.2.6", "git-url-parse": "^13.0.0", @@ -94,6 +92,7 @@ "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/humanize-duration": "^3.18.1", + "@types/json-schema": "^7.0.9", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", "event-source-polyfill": "1.0.25", diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 64141dd83d..15bd1925b3 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -16,7 +16,6 @@ import { parseEntityRef } from '@backstage/catalog-model'; import { - createApiRef, DiscoveryApi, FetchApi, IdentityApi, @@ -30,7 +29,6 @@ import { ListActionsResponse, LogEvent, ScaffolderApi, - TemplateParameterSchema, ScaffolderScaffoldOptions, ScaffolderScaffoldResponse, ScaffolderStreamLogsOptions, @@ -39,17 +37,10 @@ import { ScaffolderTask, ScaffolderDryRunOptions, ScaffolderDryRunResponse, -} from './types'; -import queryString from 'qs'; + TemplateParameterSchema, +} from '@backstage/plugin-scaffolder-react'; -/** - * Utility API reference for the {@link ScaffolderApi}. - * - * @public - */ -export const scaffolderApiRef = createApiRef({ - id: 'plugin.scaffolder.service', -}); +import queryString from 'qs'; /** * An API to interact with the scaffolder backend. @@ -149,12 +140,6 @@ export class ScaffolderClient implements ScaffolderApi { return schema; } - /** - * Executes the scaffolding of a component, given a template and its - * parameter values. - * - * @param options - The {@link ScaffolderScaffoldOptions} the scaffolding. - */ async scaffold( options: ScaffolderScaffoldOptions, ): Promise { diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx index 02f3c066ca..552d0cea6a 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx @@ -14,12 +14,14 @@ * limitations under the License. */ import React from 'react'; -import { scaffolderApiRef } from '../../api'; import { ActionsPage } from './ActionsPage'; -import { rootRouteRef } from '../../routes'; +import { + scaffolderApiRef, + ScaffolderApi, +} from '@backstage/plugin-scaffolder-react'; import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { ApiProvider } from '@backstage/core-app-api'; -import { ScaffolderApi } from '../../types'; +import { rootRouteRef } from '../../routes'; const scaffolderApiMock: jest.Mocked = { scaffold: jest.fn(), diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx index 0c16d0bd03..10a272b3cf 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx @@ -15,7 +15,10 @@ */ import React, { Fragment } from 'react'; import useAsync from 'react-use/lib/useAsync'; -import { scaffolderApiRef } from '../../api'; +import { + ActionExample, + scaffolderApiRef, +} from '@backstage/plugin-scaffolder-react'; import { Typography, Paper, @@ -47,7 +50,6 @@ import { CodeSnippet, MarkdownContent, } from '@backstage/core-components'; -import { ActionExample } from '../../types'; const useStyles = makeStyles(theme => ({ code: { @@ -146,7 +148,7 @@ export const ActionsPage = () => { <> {[props.type].flat().map(type => ( - + ))} diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx index 57dde13a43..b9c5a84da3 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx @@ -24,10 +24,12 @@ import { import React from 'react'; import { identityApiRef } from '@backstage/core-plugin-api'; import { ListTasksPage } from './ListTasksPage'; -import { ScaffolderApi } from '../../types'; -import { scaffolderApiRef } from '../../api'; -import { rootRouteRef } from '../../routes'; +import { + scaffolderApiRef, + ScaffolderApi, +} from '@backstage/plugin-scaffolder-react'; import { act, fireEvent } from '@testing-library/react'; +import { rootRouteRef } from '../../routes'; describe('', () => { const catalogApi: jest.Mocked = { diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx index 0b00879d43..1edac508d8 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx @@ -28,9 +28,10 @@ import { useApi, useRouteRef } from '@backstage/core-plugin-api'; import { CatalogFilterLayout } from '@backstage/plugin-catalog-react'; import useAsync from 'react-use/lib/useAsync'; import React, { useState } from 'react'; -import { scaffolderApiRef } from '../../api'; -import { rootRouteRef } from '../../routes'; -import { ScaffolderTask } from '../../types'; +import { + ScaffolderTask, + scaffolderApiRef, +} from '@backstage/plugin-scaffolder-react'; import { OwnerListPicker } from './OwnerListPicker'; import { CreatedAtColumn, @@ -38,6 +39,7 @@ import { TaskStatusColumn, TemplateTitleColumn, } from './columns'; +import { rootRouteRef } from '../../routes'; export interface MyTaskPageProps { initiallySelectedFilter?: 'owned' | 'all'; diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.test.tsx index 3dc20312aa..bf06c09436 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.test.tsx @@ -18,9 +18,11 @@ import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { TemplateTitleColumn } from './TemplateTitleColumn'; -import { scaffolderApiRef } from '../../../api'; -import { ScaffolderApi } from '../../../types'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; +import { + scaffolderApiRef, + ScaffolderApi, +} from '@backstage/plugin-scaffolder-react'; describe('', () => { const scaffolderApiMock: jest.Mocked = { diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.tsx index 0561b40392..cbac929a33 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/TemplateTitleColumn.tsx @@ -15,7 +15,7 @@ */ import { useApi } from '@backstage/core-plugin-api'; import React from 'react'; -import { scaffolderApiRef } from '../../../api'; +import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import useAsync from 'react-use/lib/useAsync'; import { parseEntityRef } from '@backstage/catalog-model'; import { EntityRefLink } from '@backstage/plugin-catalog-react'; diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index 1fc63b2b61..3a1196bc0e 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -30,7 +30,6 @@ import { useApi, } from '@backstage/core-plugin-api'; import { FormProps, IChangeEvent, withTheme } from '@rjsf/core'; -import { UiSchema } from '@rjsf/utils'; import { Theme as MuiTheme } from '@rjsf/material-ui'; import React, { ComponentType, useState } from 'react'; import { transformSchemaToProps } from './schema'; @@ -39,8 +38,8 @@ import * as fieldOverrides from './FieldOverrides'; import { LayoutOptions } from '../../layouts'; import { ReviewStepProps } from '../types'; import { ReviewStep } from './ReviewStep'; +import { extractSchemaFromStep } from '@backstage/plugin-scaffolder-react'; import { selectedTemplateRouteRef } from '../../routes'; -import { extractSchemaFromStep } from '../../next/TemplateWizardPage/Stepper/schema'; const Form = withTheme(MuiTheme); @@ -68,11 +67,7 @@ export type MultistepJsonFormProps = { ReviewStepComponent?: ComponentType; }; -export function getSchemasFromSteps(steps: Step[]): { - uiSchema: UiSchema; - mergedSchema: JsonObject; - schema: JsonObject; -}[] { +export function getSchemasFromSteps(steps: Step[]) { return steps.map(({ schema }) => ({ mergedSchema: schema, ...extractSchemaFromStep(schema), diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 644b26eeb8..6a2da406e0 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -22,20 +22,21 @@ import { ScaffolderPage } from './ScaffolderPage'; import { TemplatePage } from './TemplatePage'; import { TaskPage } from './TaskPage'; import { ActionsPage } from './ActionsPage'; -import { SecretsContextProvider } from './secrets/SecretsContext'; import { TemplateEditorPage } from './TemplateEditorPage'; - -import { - DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS, - FIELD_EXTENSION_KEY, - FIELD_EXTENSION_WRAPPER_KEY, - FieldExtensionOptions, -} from '../extensions'; +import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../extensions/default'; import { useElementFilter, useRouteRef, useRouteRefParams, } from '@backstage/core-plugin-api'; +import { + FieldExtensionOptions, + SecretsContextProvider, + useCustomFieldExtensions, +} from '@backstage/plugin-scaffolder-react'; +import { ListTasksPage } from './ListTasksPage'; +import { LayoutOptions, LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from '../layouts'; +import { ReviewStepProps } from './types'; import { actionsRouteRef, editRouteRef, @@ -44,9 +45,6 @@ import { scaffolderTaskRouteRef, selectedTemplateRouteRef, } from '../routes'; -import { ListTasksPage } from './ListTasksPage'; -import { LayoutOptions, LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from '../layouts'; -import { ReviewStepProps } from './types'; /** * The props for the entrypoint `ScaffolderPage` component the plugin. @@ -95,16 +93,7 @@ export const Router = (props: RouterProps) => { const outlet = useOutlet(); const TaskPageElement = TaskPageComponent ?? TaskPage; - const customFieldExtensions = useElementFilter(outlet, elements => - elements - .selectByComponentData({ - key: FIELD_EXTENSION_WRAPPER_KEY, - }) - .findComponentData({ - key: FIELD_EXTENSION_KEY, - }), - ); - + const customFieldExtensions = useCustomFieldExtensions(outlet); const fieldExtensions = [ ...customFieldExtensions, ...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter( @@ -113,8 +102,9 @@ export const Router = (props: RouterProps) => { customFieldExtension => customFieldExtension.name === name, ), ), - ]; + ] as FieldExtensionOptions[]; + // todo(blam): this should also be moved to a hook in -react const customLayouts = useElementFilter(outlet, elements => elements .selectByComponentData({ diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 9c13db72b1..50221f3a83 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -34,12 +34,12 @@ import { UserListPicker, } from '@backstage/plugin-catalog-react'; import React, { ComponentType } from 'react'; -import { registerComponentRouteRef } from '../../routes'; import { TemplateList } from '../TemplateList'; import { TemplateTypePicker } from '../TemplateTypePicker'; import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; import { usePermission } from '@backstage/plugin-permission-react'; import { ScaffolderPageContextMenu } from './ScaffolderPageContextMenu'; +import { registerComponentRouteRef } from '../../routes'; export type ScaffolderPageProps = { TemplateCardComponent?: diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.test.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.test.tsx index 02869bb6da..66513591d0 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.test.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.test.tsx @@ -18,8 +18,8 @@ import userEvent from '@testing-library/user-event'; import { renderInTestApp } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; -import { rootRouteRef } from '../../routes'; import { ScaffolderPageContextMenu } from './ScaffolderPageContextMenu'; +import { rootRouteRef } from '../../routes'; describe('ScaffolderPageContextMenu', () => { it('does not render anything if fully disabled', async () => { diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index 699ce119fb..393bd495bf 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -47,15 +47,18 @@ import qs from 'qs'; import React, { memo, useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import useInterval from 'react-use/lib/useInterval'; +import { + ScaffolderTaskStatus, + ScaffolderTaskOutput, +} from '@backstage/plugin-scaffolder-react'; +import { useTaskEventStream } from '../hooks/useEventStream'; +import { TaskErrors } from './TaskErrors'; +import { TaskPageLinks } from './TaskPageLinks'; import { rootRouteRef, scaffolderTaskRouteRef, selectedTemplateRouteRef, } from '../../routes'; -import { ScaffolderTaskStatus, ScaffolderTaskOutput } from '../../types'; -import { useTaskEventStream } from '../hooks/useEventStream'; -import { TaskErrors } from './TaskErrors'; -import { TaskPageLinks } from './TaskPageLinks'; // typings are wrong for this library, so fallback to not parsing types. const humanizeDuration = require('humanize-duration'); diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx index 78d6a452ea..59c1515b79 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx @@ -19,7 +19,7 @@ import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { Box } from '@material-ui/core'; import LanguageIcon from '@material-ui/icons/Language'; import React from 'react'; -import { ScaffolderTaskOutput } from '../../types'; +import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; import { IconLink } from './IconLink'; import { IconComponent, useApp, useRouteRef } from '@backstage/core-plugin-api'; diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/CustomFieldExplorer.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/CustomFieldExplorer.tsx index 3510968acd..c7138efb97 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/CustomFieldExplorer.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/CustomFieldExplorer.tsx @@ -33,7 +33,7 @@ import { Theme as MuiTheme } from '@rjsf/material-ui'; import CodeMirror from '@uiw/react-codemirror'; import React, { useCallback, useMemo, useState } from 'react'; import yaml from 'yaml'; -import { FieldExtensionOptions } from '../../extensions'; +import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; import * as fieldOverrides from '../MultistepJsonForm/FieldOverrides'; import { TemplateEditorForm } from './TemplateEditorForm'; diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/DryRunContext.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/DryRunContext.tsx index 3534b62c39..a617eebe1e 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/DryRunContext.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/DryRunContext.tsx @@ -26,8 +26,10 @@ import React, { useRef, useState, } from 'react'; -import { scaffolderApiRef } from '../../api'; -import { ScaffolderDryRunResponse } from '../../types'; +import { + scaffolderApiRef, + ScaffolderDryRunResponse, +} from '@backstage/plugin-scaffolder-react'; const MAX_CONTENT_SIZE = 64 * 1024; const CHUNK_SIZE = 32 * 1024; diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResults.test.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResults.test.tsx index bc3354cc46..0267093ee6 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResults.test.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResults.test.tsx @@ -18,7 +18,7 @@ import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { act, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React, { useEffect } from 'react'; -import { scaffolderApiRef } from '../../../api'; +import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import { DryRunProvider, useDryRun } from '../DryRunContext'; import { DryRunResults } from './DryRunResults'; diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResultsList.test.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResultsList.test.tsx index 417c23c483..706f284db1 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResultsList.test.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResultsList.test.tsx @@ -18,7 +18,7 @@ import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React, { useEffect } from 'react'; -import { scaffolderApiRef } from '../../../api'; +import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import { DryRunProvider, useDryRun } from '../DryRunContext'; import { DryRunResultsList } from './DryRunResultsList'; diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResultsView.test.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResultsView.test.tsx index c709adfd68..c2d8d3a2a4 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResultsView.test.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResultsView.test.tsx @@ -19,7 +19,7 @@ import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React, { ReactNode, useEffect } from 'react'; -import { scaffolderApiRef } from '../../../api'; +import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import { DryRunProvider, useDryRun } from '../DryRunContext'; import { DryRunResultsView } from './DryRunResultsView'; diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor.tsx index 7055e42c84..dc67f687bb 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor.tsx @@ -15,7 +15,7 @@ */ import { makeStyles } from '@material-ui/core'; import React, { useState } from 'react'; -import { FieldExtensionOptions } from '../../extensions'; +import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; import type { LayoutOptions } from '../../layouts'; import { TemplateDirectoryAccess } from '../../lib/filesystem'; import { DirectoryEditorProvider } from './DirectoryEditorContext'; diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorForm.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorForm.tsx index 192a9696cf..c62112a89c 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorForm.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorForm.tsx @@ -19,9 +19,11 @@ import { makeStyles } from '@material-ui/core/styles'; import React, { Component, ReactNode, useMemo, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import yaml from 'yaml'; -import { FieldExtensionOptions } from '../../extensions'; +import { + FieldExtensionOptions, + TemplateParameterSchema, +} from '@backstage/plugin-scaffolder-react'; import { LayoutOptions } from '../../layouts'; -import { TemplateParameterSchema } from '../../types'; import { MultistepJsonForm } from '../MultistepJsonForm'; import { createValidator } from '../TemplatePage'; import { useDirectoryEditor } from './DirectoryEditorContext'; diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx index 8847b85373..20691a8e85 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx @@ -23,7 +23,7 @@ import { CustomFieldExplorer } from './CustomFieldExplorer'; import { TemplateEditorIntro } from './TemplateEditorIntro'; import { TemplateEditor } from './TemplateEditor'; import { TemplateFormPreviewer } from './TemplateFormPreviewer'; -import { FieldExtensionOptions } from '../../extensions'; +import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; import type { LayoutOptions } from '../../layouts'; type Selection = diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateFormPreviewer.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateFormPreviewer.tsx index 5bc32ed581..884305bf64 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateFormPreviewer.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateFormPreviewer.tsx @@ -32,7 +32,7 @@ import CloseIcon from '@material-ui/icons/Close'; import React, { useCallback, useState } from 'react'; import useAsync from 'react-use/lib/useAsync'; import yaml from 'yaml'; -import { FieldExtensionOptions } from '../../extensions'; +import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; import { LayoutOptions } from '../../layouts'; import { TemplateEditorForm } from './TemplateEditorForm'; import { TemplateEditorTextArea } from './TemplateEditorTextArea'; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx index 0535df5ef4..b4358146fd 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -22,9 +22,11 @@ import { import { act, fireEvent, screen, within } from '@testing-library/react'; import React from 'react'; import { Route, Routes } from 'react-router-dom'; -import { scaffolderApiRef } from '../../api'; -import { ScaffolderApi } from '../../types'; -import { rootRouteRef } from '../../routes'; +import { + scaffolderApiRef, + ScaffolderApi, + SecretsContextProvider, +} from '@backstage/plugin-scaffolder-react'; import { TemplatePage } from './TemplatePage'; import { featureFlagsApiRef, @@ -33,6 +35,7 @@ import { } from '@backstage/core-plugin-api'; import { ApiProvider } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; +import { rootRouteRef } from '../../routes'; jest.mock('react-router-dom', () => { return { @@ -125,7 +128,9 @@ describe('TemplatePage', () => { }); const rendered = await renderInTestApp( - + + + , { mountedRoutes: { @@ -146,7 +151,9 @@ describe('TemplatePage', () => { scaffolderApiMock.getTemplateParameterSchema.mockReturnValueOnce(promise); const rendered = await renderInTestApp( - + + + , { mountedRoutes: { @@ -187,7 +194,9 @@ describe('TemplatePage', () => { }); const { findByLabelText, findByText } = await renderInTestApp( - + + + , { mountedRoutes: { @@ -234,7 +243,14 @@ describe('TemplatePage', () => { const rendered = await renderInTestApp( - } /> + + + + } + /> This is root} /> , @@ -287,7 +303,9 @@ describe('TemplatePage', () => { const { findByText, findByLabelText, findAllByRole, findByRole } = await renderInTestApp( - + + + , { mountedRoutes: { @@ -331,7 +349,9 @@ describe('TemplatePage', () => { await renderInTestApp( - + + + , { mountedRoutes: { diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index e5dcc5d9bf..0f484ff404 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -16,17 +16,14 @@ import { LinearProgress } from '@material-ui/core'; import { IChangeEvent } from '@rjsf/core'; import qs from 'qs'; -import React, { ComponentType, useCallback, useContext, useState } from 'react'; +import React, { ComponentType, useCallback, useState } from 'react'; import { Navigate, useNavigate } from 'react-router-dom'; import useAsync from 'react-use/lib/useAsync'; -import { scaffolderApiRef } from '../../api'; -import { FieldExtensionOptions } from '../../extensions'; -import { SecretsContext } from '../secrets/SecretsContext'; import { - rootRouteRef, - scaffolderTaskRouteRef, - selectedTemplateRouteRef, -} from '../../routes'; + FieldExtensionOptions, + scaffolderApiRef, + useTemplateSecrets, +} from '@backstage/plugin-scaffolder-react'; import { MultistepJsonForm } from '../MultistepJsonForm'; import { createValidator } from './createValidator'; @@ -42,6 +39,11 @@ import { import { stringifyEntityRef } from '@backstage/catalog-model'; import { LayoutOptions } from '../../layouts'; import { ReviewStepProps } from '../types'; +import { + rootRouteRef, + scaffolderTaskRouteRef, + selectedTemplateRouteRef, +} from '../../routes'; const useTemplateParameterSchema = (templateRef: string) => { const scaffolderApi = useApi(scaffolderApiRef); @@ -70,7 +72,7 @@ export const TemplatePage = ({ headerOptions, }: Props) => { const apiHolder = useApiHolder(); - const secretsContext = useContext(SecretsContext); + const secretsContext = useTemplateSecrets(); const errorApi = useApi(errorApiRef); const scaffolderApi = useApi(scaffolderApiRef); const { templateName, namespace } = useRouteRefParams( diff --git a/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts b/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts index f549c9e7f2..177697eda0 100644 --- a/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts +++ b/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts @@ -15,7 +15,7 @@ */ import { createValidator } from './createValidator'; -import { CustomFieldValidator } from '../../extensions'; +import { CustomFieldValidator } from '@backstage/plugin-scaffolder-react'; import { ApiHolder } from '@backstage/core-plugin-api'; import { FormValidation } from '@rjsf/core'; diff --git a/plugins/scaffolder/src/components/TemplatePage/createValidator.ts b/plugins/scaffolder/src/components/TemplatePage/createValidator.ts index 9cd1489a02..4a64481fc3 100644 --- a/plugins/scaffolder/src/components/TemplatePage/createValidator.ts +++ b/plugins/scaffolder/src/components/TemplatePage/createValidator.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { CustomFieldValidator } from '../../extensions'; +import { CustomFieldValidator } from '@backstage/plugin-scaffolder-react'; import { FormValidation } from '@rjsf/core'; import { JsonObject, JsonValue } from '@backstage/types'; import { ApiHolder } from '@backstage/core-plugin-api'; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx index 7fac9f7840..1edb4fb940 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useContext } from 'react'; +import React from 'react'; import { RepoUrlPicker } from './RepoUrlPicker'; import Form from '@rjsf/core'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; @@ -23,13 +23,13 @@ import { scmAuthApiRef, ScmAuthApi, } from '@backstage/integration-react'; -import { scaffolderApiRef } from '../../../api'; -import { ScaffolderApi } from '../../../types'; import { SecretsContextProvider, - SecretsContext, -} from '../../secrets/SecretsContext'; + scaffolderApiRef, + ScaffolderApi, + useTemplateSecrets, +} from '@backstage/plugin-scaffolder-react'; import { act, fireEvent } from '@testing-library/react'; describe('RepoUrlPicker', () => { @@ -119,8 +119,10 @@ describe('RepoUrlPicker', () => { describe('requestUserCredentials', () => { it('should call the scmAuthApi with the correct params', async () => { const SecretsComponent = () => { - const value = useContext(SecretsContext); - return

{JSON.stringify(value)}
; + const { secrets } = useTemplateSecrets(); + return ( +
{JSON.stringify({ secrets })}
+ ); }; const { getAllByRole, getByTestId } = await renderInTestApp( { diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx index b9762cd69d..5a78cf6195 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx @@ -18,7 +18,7 @@ import { Progress, Select, SelectItem } from '@backstage/core-components'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; import { useApi } from '@backstage/core-plugin-api'; -import { scaffolderApiRef } from '../../../api'; +import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import useAsync from 'react-use/lib/useAsync'; export const RepoUrlPickerHost = (props: { diff --git a/plugins/scaffolder/src/components/fields/utils.ts b/plugins/scaffolder/src/components/fields/utils.ts index 2d3e2aab1e..7b59f1da90 100644 --- a/plugins/scaffolder/src/components/fields/utils.ts +++ b/plugins/scaffolder/src/components/fields/utils.ts @@ -19,7 +19,7 @@ import zodToJsonSchema from 'zod-to-json-schema'; import { CustomFieldExtensionSchema, FieldExtensionComponentProps, -} from '../../extensions'; +} from '@backstage/plugin-scaffolder-react'; /** * @public diff --git a/plugins/scaffolder/src/components/hooks/useEventStream.ts b/plugins/scaffolder/src/components/hooks/useEventStream.ts index bb24c0d612..0b36c85ad8 100644 --- a/plugins/scaffolder/src/components/hooks/useEventStream.ts +++ b/plugins/scaffolder/src/components/hooks/useEventStream.ts @@ -15,13 +15,13 @@ */ import { useImmerReducer } from 'use-immer'; import { useEffect } from 'react'; -import { scaffolderApiRef } from '../../api'; import { ScaffolderTask, ScaffolderTaskStatus, ScaffolderTaskOutput, LogEvent, -} from '../../types'; + scaffolderApiRef, +} from '@backstage/plugin-scaffolder-react'; import { useApi } from '@backstage/core-plugin-api'; import { Subscription } from '@backstage/types'; diff --git a/plugins/scaffolder/src/components/index.ts b/plugins/scaffolder/src/components/index.ts index 23f49ea748..a5c17b8ab0 100644 --- a/plugins/scaffolder/src/components/index.ts +++ b/plugins/scaffolder/src/components/index.ts @@ -16,7 +16,6 @@ export * from './fields'; export type { RepoUrlPickerUiOptions } from './fields'; export { TemplateTypePicker } from './TemplateTypePicker'; -export * from './secrets'; -export { TaskPage } from './TaskPage'; +export { TaskPage, type TaskPageProps } from './TaskPage'; export type { RouterProps } from './Router'; export type { ReviewStepProps } from './types'; diff --git a/plugins/scaffolder/src/deprecated.ts b/plugins/scaffolder/src/deprecated.ts new file mode 100644 index 0000000000..201f7ac7d7 --- /dev/null +++ b/plugins/scaffolder/src/deprecated.ts @@ -0,0 +1,190 @@ +/* + * 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. + */ + +/* + * This file is here to re-export exports from the `@backage/plugin-scaffolder-react` package to keep backwards compatibility. + * But mark them as deprecated so that any import that is using these exports will be notified that they should use the `-react` package instead. + * It's a little awkward to get the deprecated notice to show up in the API report, which is why it's all been extracted out to this file. + */ + +import { + createScaffolderFieldExtension as createScaffolderFieldExtensionTemp, + ScaffolderFieldExtensions as ScaffolderFieldExtensionsTemp, + useTemplateSecrets as useTemplateSecretsTemp, + scaffolderApiRef as scaffolderApiRefTemp, + type ScaffolderApi as ScaffolderApiTemp, + type ScaffolderUseTemplateSecrets as ScaffolderUseTemplateSecretsTemp, + type TemplateParameterSchema as TemplateParameterSchemaTemp, + type CustomFieldExtensionSchema as CustomFieldExtensionSchemaTemp, + type CustomFieldValidator as CustomFieldValidatorTemp, + type FieldExtensionOptions as FieldExtensionOptionsTemp, + type FieldExtensionComponentProps as FieldExtensionComponentPropsTemp, + type FieldExtensionComponent as FieldExtensionComponentTemp, + type ListActionsResponse as ListActionsResponseTemp, + type LogEvent as LogEventTemp, + type ScaffolderDryRunOptions as ScaffolderDryRunOptionsTemp, + type ScaffolderDryRunResponse as ScaffolderDryRunResponseTemp, + type ScaffolderGetIntegrationsListOptions as ScaffolderGetIntegrationsListOptionsTemp, + type ScaffolderGetIntegrationsListResponse as ScaffolderGetIntegrationsListResponseTemp, + type ScaffolderOutputLink as ScaffolderOutputLinkTemp, + type ScaffolderScaffoldOptions as ScaffolderScaffoldOptionsTemp, + type ScaffolderScaffoldResponse as ScaffolderScaffoldResponseTemp, + type ScaffolderStreamLogsOptions as ScaffolderStreamLogsOptionsTemp, + type ScaffolderTask as ScaffolderTaskTemp, + type ScaffolderTaskOutput as ScaffolderTaskOutputTemp, + type ScaffolderTaskStatus as ScaffolderTaskStatusTemp, +} from '@backstage/plugin-scaffolder-react'; + +import { rootRouteRef as rootRouteRefTemp } from './routes'; + +/** + * @public + * @deprecated use import from `{@link @backstage/plugin-scaffolder#scaffolderPlugin}.routes.root` instead. + */ +export const rootRouteRef = rootRouteRefTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#createScaffolderFieldExtension} instead as this has now been moved. + */ +export const createScaffolderFieldExtension = + createScaffolderFieldExtensionTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderFieldExtensions} instead as this has now been moved. + */ +export const ScaffolderFieldExtensions = ScaffolderFieldExtensionsTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#useTemplateSecrets} instead as this has now been moved. + */ +export const useTemplateSecrets = useTemplateSecretsTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#scaffolderApiRef} instead as this has now been moved. + */ +export const scaffolderApiRef = scaffolderApiRefTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderApi} instead as this has now been moved. + */ +export type ScaffolderApi = ScaffolderApiTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderUseTemplateSecrets} instead as this has now been moved. + */ +export type ScaffolderUseTemplateSecrets = ScaffolderUseTemplateSecretsTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#TemplateParameterSchema} instead as this has now been moved. + */ +export type TemplateParameterSchema = TemplateParameterSchemaTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#CustomFieldExtensionSchema} instead as this has now been moved. + */ +export type CustomFieldExtensionSchema = CustomFieldExtensionSchemaTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#CustomFieldValidator} instead as this has now been moved. + */ +export type CustomFieldValidator = + CustomFieldValidatorTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#FieldExtensionOptions} instead as this has now been moved. + */ +export type FieldExtensionOptions = FieldExtensionOptionsTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#FieldExtensionComponentProps} instead as this has now been moved. + */ +export type FieldExtensionComponentProps< + TFieldReturnValue, + TUiOptions extends {} = {}, +> = FieldExtensionComponentPropsTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#FieldExtensionComponent} instead as this has now been moved. + */ +export type FieldExtensionComponent<_TReturnValue, _TInputProps> = + FieldExtensionComponentTemp<_TReturnValue, _TInputProps>; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ListActionsResponse} instead as this has now been moved. + */ +export type ListActionsResponse = ListActionsResponseTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#LogEvent} instead as this has now been moved. + */ +export type LogEvent = LogEventTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderDryRunOptions} instead as this has now been moved. + */ +export type ScaffolderDryRunOptions = ScaffolderDryRunOptionsTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderDryRunResponse} instead as this has now been moved. + */ +export type ScaffolderDryRunResponse = ScaffolderDryRunResponseTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderGetIntegrationsListOptions} instead as this has now been moved. + */ +export type ScaffolderGetIntegrationsListOptions = + ScaffolderGetIntegrationsListOptionsTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderGetIntegrationsListResponse} instead as this has now been moved. + */ +export type ScaffolderGetIntegrationsListResponse = + ScaffolderGetIntegrationsListResponseTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderOutputlink} instead as this has now been moved. + */ +export type ScaffolderOutputlink = ScaffolderOutputLinkTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderScaffoldOptions} instead as this has now been moved. + */ +export type ScaffolderScaffoldOptions = ScaffolderScaffoldOptionsTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderScaffoldResponse} instead as this has now been moved. + */ +export type ScaffolderScaffoldResponse = ScaffolderScaffoldResponseTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderStreamLogsOptions} instead as this has now been moved. + */ +export type ScaffolderStreamLogsOptions = ScaffolderStreamLogsOptionsTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderTask} instead as this has now been moved. + */ +export type ScaffolderTask = ScaffolderTaskTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderTaskOutput} instead as this has now been moved. + */ +export type ScaffolderTaskOutput = ScaffolderTaskOutputTemp; +/** + * @public + * @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderTaskStatus} instead as this has now been moved. + */ +export type ScaffolderTaskStatus = ScaffolderTaskStatusTemp; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index ae994191ce..0e2b293982 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -19,40 +19,16 @@ * * @packageDocumentation */ +export { ScaffolderClient } from './api'; -export { scaffolderApiRef, ScaffolderClient } from './api'; -export type { - Action, - ActionExample, - ListActionsResponse, - LogEvent, - ScaffolderApi, - ScaffolderDryRunOptions, - ScaffolderDryRunResponse, - ScaffolderGetIntegrationsListOptions, - ScaffolderGetIntegrationsListResponse, - ScaffolderOutputLink, - ScaffolderScaffoldOptions, - ScaffolderScaffoldResponse, - ScaffolderStreamLogsOptions, - ScaffolderTask, - ScaffolderTaskOutput, - ScaffolderTaskStatus, - TemplateParameterSchema, -} from './types'; export { - createScaffolderFieldExtension, - ScaffolderFieldExtensions, -} from './extensions'; -export type { - CustomFieldExtensionSchema, - CustomFieldValidator, - FieldExtensionOptions, - FieldExtensionComponentProps, - FieldExtensionComponent, -} from './extensions'; -export { createScaffolderLayout, ScaffolderLayouts } from './layouts'; -export type { LayoutOptions, LayoutTemplate, LayoutComponent } from './layouts'; + createScaffolderLayout, + ScaffolderLayouts, + type LayoutOptions, + type LayoutTemplate, + type LayoutComponent, +} from './layouts'; + export { EntityPickerFieldExtension, EntityNamePickerFieldExtension, @@ -63,23 +39,17 @@ export { ScaffolderPage, scaffolderPlugin, } from './plugin'; + export * from './components'; -export { - rootRouteRef, - nextRouteRef, - selectedTemplateRouteRef, - nextSelectedTemplateRouteRef, -} from './routes'; -export type { TaskPageProps } from './components/TaskPage'; +export * from './deprecated'; /** next exports */ export { NextScaffolderPage } from './plugin'; -export type { NextRouterProps } from './next'; -export type { TemplateGroupFilter } from './next'; -export type { FormProps } from './next'; export { - createNextScaffolderFieldExtension, - type NextCustomFieldValidator, - type NextFieldExtensionOptions, - type NextFieldExtensionComponentProps, -} from './extensions'; + nextRouteRef, + nextScaffolderTaskRouteRef, + nextSelectedTemplateRouteRef, + type TemplateGroupFilter, + type NextRouterProps, + type FormProps, +} from './next'; diff --git a/plugins/scaffolder/src/next/Router/Router.test.tsx b/plugins/scaffolder/src/next/Router/Router.test.tsx index 89f8f6ecd3..4e5535be6c 100644 --- a/plugins/scaffolder/src/next/Router/Router.test.tsx +++ b/plugins/scaffolder/src/next/Router/Router.test.tsx @@ -21,7 +21,7 @@ import { renderInTestApp } from '@backstage/test-utils'; import { createScaffolderFieldExtension, ScaffolderFieldExtensions, -} from '../../extensions'; +} from '@backstage/plugin-scaffolder-react'; import { scaffolderPlugin } from '../../plugin'; jest.mock('../TemplateListPage', () => ({ diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx index 7a2d2826e7..784e17dd55 100644 --- a/plugins/scaffolder/src/next/Router/Router.tsx +++ b/plugins/scaffolder/src/next/Router/Router.tsx @@ -18,19 +18,16 @@ import { Routes, Route, useOutlet } from 'react-router-dom'; import { TemplateListPage } from '../TemplateListPage'; import { TemplateWizardPage } from '../TemplateWizardPage'; import { - FIELD_EXTENSION_WRAPPER_KEY, - FIELD_EXTENSION_KEY, - DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS, NextFieldExtensionOptions, - FieldExtensionOptions, -} from '../../extensions'; + SecretsContextProvider, + useCustomFieldExtensions, +} from '@backstage/plugin-scaffolder-react'; -import { useElementFilter } from '@backstage/core-plugin-api'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups'; -import { nextSelectedTemplateRouteRef } from '../../routes'; -import { SecretsContextProvider } from '../../components/secrets/SecretsContext'; -import type { FormProps } from '../types'; +import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../../extensions/default'; +import { FormProps } from '../types'; +import { nextSelectedTemplateRouteRef } from '../routes'; /** * The Props for the Scaffolder Router @@ -55,19 +52,9 @@ export type NextRouterProps = { */ export const Router = (props: PropsWithChildren) => { const { components: { TemplateCardComponent } = {} } = props; - const outlet = useOutlet() || props.children; - - const customFieldExtensions = useElementFilter(outlet, elements => - elements - .selectByComponentData({ - key: FIELD_EXTENSION_WRAPPER_KEY, - }) - .findComponentData({ - key: FIELD_EXTENSION_KEY, - }), - ); - + const customFieldExtensions = + useCustomFieldExtensions(outlet); const fieldExtensions = [ ...customFieldExtensions, ...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter( diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx deleted file mode 100644 index 59c6989c0d..0000000000 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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. - */ -jest.mock('./TemplateCard', () => ({ TemplateCard: jest.fn(() => null) })); - -import React from 'react'; -import { TemplateGroup } from './TemplateGroup'; -import { render } from '@testing-library/react'; -import { TemplateCard } from './TemplateCard'; -import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; - -describe('TemplateGroup', () => { - it('should return a message when no templates are passed in', async () => { - const { getByText } = render(); - - expect( - getByText(/No templates found that match your filter/), - ).toBeInTheDocument(); - }); - - it('should render a card for each template with the template being passed as a prop', () => { - const mockTemplates: TemplateEntityV1beta3[] = [ - { - apiVersion: 'scaffolder.backstage.io/v1beta3', - kind: 'Template', - metadata: { name: 'test' }, - spec: { - parameters: [], - steps: [], - type: 'website', - }, - }, - { - apiVersion: 'scaffolder.backstage.io/v1beta3', - kind: 'Template', - metadata: { name: 'test2' }, - spec: { - parameters: [], - steps: [], - type: 'service', - }, - }, - ]; - - render(); - - expect(TemplateCard).toHaveBeenCalledTimes(2); - - for (const template of mockTemplates) { - expect(TemplateCard).toHaveBeenCalledWith( - expect.objectContaining({ template }), - {}, - ); - } - }); - - it('should use the passed in TemplateCard prop to render the template card', () => { - const mockTemplateCardComponent = jest.fn(() => null); - - const mockTemplates: TemplateEntityV1beta3[] = [ - { - apiVersion: 'scaffolder.backstage.io/v1beta3', - kind: 'Template', - metadata: { name: 'test' }, - spec: { - parameters: [], - steps: [], - type: 'website', - }, - }, - { - apiVersion: 'scaffolder.backstage.io/v1beta3', - kind: 'Template', - metadata: { name: 'test2' }, - spec: { - parameters: [], - steps: [], - type: 'service', - }, - }, - ]; - - render( - , - ); - - expect(mockTemplateCardComponent).toHaveBeenCalledTimes(2); - - for (const template of mockTemplates) { - expect(mockTemplateCardComponent).toHaveBeenCalledWith( - expect.objectContaining({ template }), - {}, - ); - } - }); - - it('should render the title when no templates passed', () => { - const { getByText } = render(); - expect(getByText('Test')).toBeInTheDocument(); - }); - - it('should render the title when there are templates in the list', () => { - const mockTemplates: TemplateEntityV1beta3[] = [ - { - apiVersion: 'scaffolder.backstage.io/v1beta3', - kind: 'Template', - metadata: { name: 'test' }, - spec: { parameters: [], steps: [], type: 'website' }, - }, - ]; - - const { getByText } = render( - , - ); - - expect(getByText('Test')).toBeInTheDocument(); - }); - - it('should allow for passing through a user given title component', () => { - const TitleComponent =

Im a custom header

; - const { getByText } = render( - , - ); - - expect(getByText('Im a custom header')).toBeInTheDocument(); - }); -}); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx index 2d8cdc3150..f76c1a4ab6 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx @@ -18,26 +18,33 @@ jest.mock('@backstage/plugin-catalog-react', () => ({ useEntityList: jest.fn(), })); -jest.mock('./TemplateGroup', () => ({ +jest.mock('@backstage/plugin-scaffolder-react', () => ({ TemplateGroup: jest.fn(() => null), })); import React from 'react'; -import { render } from '@testing-library/react'; import { useEntityList } from '@backstage/plugin-catalog-react'; import { TemplateGroups } from './TemplateGroups'; -import { TestApiProvider } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { errorApiRef } from '@backstage/core-plugin-api'; -import { TemplateGroup } from './TemplateGroup'; +import { TemplateGroup } from '@backstage/plugin-scaffolder-react'; +import { nextRouteRef } from '../routes'; describe('TemplateGroups', () => { + beforeEach(() => jest.clearAllMocks()); + it('should return progress if the hook is loading', async () => { (useEntityList as jest.Mock).mockReturnValue({ loading: true }); - const { findByTestId } = render( + const { findByTestId } = await renderInTestApp( , + { + mountedRoutes: { + '/next': nextRouteRef, + }, + }, ); expect(await findByTestId('progress')).toBeInTheDocument(); @@ -51,10 +58,15 @@ describe('TemplateGroups', () => { const errorApi = { post: jest.fn(), }; - render( + await renderInTestApp( , + { + mountedRoutes: { + '/next': nextRouteRef, + }, + }, ); expect(errorApi.post).toHaveBeenCalledWith(mockError); @@ -67,10 +79,15 @@ describe('TemplateGroups', () => { error: null, }); - const { findByText } = render( + const { findByText } = await renderInTestApp( , + { + mountedRoutes: { + '/next': nextRouteRef, + }, + }, ); expect(await findByText(/No templates found/)).toBeInTheDocument(); @@ -83,10 +100,15 @@ describe('TemplateGroups', () => { error: null, }); - const { findByText } = render( + const { findByText } = await renderInTestApp( , + { + mountedRoutes: { + '/next': nextRouteRef, + }, + }, ); expect(await findByText(/No templates found/)).toBeInTheDocument(); @@ -118,14 +140,23 @@ describe('TemplateGroups', () => { error: null, }); - render( + await renderInTestApp( true }]} /> , + { + mountedRoutes: { + '/next': nextRouteRef, + }, + }, ); expect(TemplateGroup).toHaveBeenCalledWith( - expect.objectContaining({ templates: mockEntities }), + expect.objectContaining({ + templates: mockEntities.map(template => + expect.objectContaining({ template }), + ), + }), {}, ); }); @@ -156,16 +187,23 @@ describe('TemplateGroups', () => { error: null, }); - render( + await renderInTestApp( e.metadata.name === 't1' }]} /> , + { + mountedRoutes: { + '/next': nextRouteRef, + }, + }, ); expect(TemplateGroup).toHaveBeenCalledWith( - expect.objectContaining({ templates: [mockEntities[0]] }), + expect.objectContaining({ + templates: [expect.objectContaining({ template: mockEntities[0] })], + }), {}, ); }); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx index f402c0ad19..485b0e40cd 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx @@ -13,14 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { TemplateGroup } from './TemplateGroup'; -import { Entity } from '@backstage/catalog-model'; +import React, { useCallback } from 'react'; + +import { + Entity, + parseEntityRef, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { useEntityList } from '@backstage/plugin-catalog-react'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { Progress, Link } from '@backstage/core-components'; +import { Progress, Link, DocsIcon } from '@backstage/core-components'; import { Typography } from '@material-ui/core'; -import { errorApiRef, useApi } from '@backstage/core-plugin-api'; +import { + errorApiRef, + useApi, + useApp, + useRouteRef, +} from '@backstage/core-plugin-api'; +import { TemplateGroup } from '@backstage/plugin-scaffolder-react'; +import { viewTechDocRouteRef } from '../../routes'; +import { nextSelectedTemplateRouteRef } from '../routes'; +import { useNavigate } from 'react-router-dom'; /** * @alpha @@ -41,6 +54,17 @@ export const TemplateGroups = (props: TemplateGroupsProps) => { const { loading, error, entities } = useEntityList(); const { groups, TemplateCardComponent } = props; const errorApi = useApi(errorApiRef); + const app = useApp(); + const viewTechDocsLink = useRouteRef(viewTechDocRouteRef); + const templateRoute = useRouteRef(nextSelectedTemplateRouteRef); + const navigate = useNavigate(); + const onSelected = useCallback( + (template: TemplateEntityV1beta3) => { + const { namespace, name } = parseEntityRef(stringifyEntityRef(template)); + navigate(templateRoute({ namespace, templateName: name })); + }, + [navigate, templateRoute], + ); if (loading) { return ; @@ -65,16 +89,41 @@ export const TemplateGroups = (props: TemplateGroupsProps) => { return ( <> - {groups.map(({ title, filter }, index) => ( - - filter(e), - )} - title={title} - components={{ CardComponent: TemplateCardComponent }} - /> - ))} + {groups.map(({ title, filter }, index) => { + const templates = entities + .filter((e): e is TemplateEntityV1beta3 => filter(e)) + .map(template => { + const { kind, namespace, name } = parseEntityRef( + stringifyEntityRef(template), + ); + const additionalLinks = + template.metadata.annotations?.['backstage.io/techdocs-ref'] && + viewTechDocsLink + ? [ + { + icon: app.getSystemIcon('docs') ?? DocsIcon, + text: 'View TechDocs', + url: viewTechDocsLink({ kind, namespace, name }), + }, + ] + : []; + + return { + template, + additionalLinks, + }; + }); + + return ( + + ); + })} ); }; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx index 26e2994c8a..41e7897819 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx @@ -25,7 +25,7 @@ import { TestApiProvider, } from '@backstage/test-utils'; import React from 'react'; -import { nextRouteRef } from '../../routes'; +import { nextRouteRef } from '../routes'; import { TemplateListPage } from './TemplateListPage'; describe('TemplateListPage', () => { diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx index 003c3aa98b..4eae84b866 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx @@ -35,8 +35,8 @@ import { import { CategoryPicker } from './CategoryPicker'; import { RegisterExistingButton } from './RegisterExistingButton'; import { useRouteRef } from '@backstage/core-plugin-api'; -import { registerComponentRouteRef } from '../../routes'; import { TemplateGroupFilter, TemplateGroups } from './TemplateGroups'; +import { registerComponentRouteRef } from '../../routes'; export type TemplateListPageProps = { TemplateCardComponent?: React.ComponentType<{ diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx index f36fae9aa3..c4f9c75ceb 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx @@ -22,10 +22,14 @@ import { } from '@backstage/test-utils'; import { act, fireEvent } from '@testing-library/react'; import React from 'react'; -import { scaffolderApiRef } from '../../api'; -import { nextRouteRef, rootRouteRef } from '../../routes'; -import { ScaffolderApi } from '../../types'; +import { + ScaffolderApi, + scaffolderApiRef, + SecretsContextProvider, +} from '@backstage/plugin-scaffolder-react'; import { TemplateWizardPage } from './TemplateWizardPage'; +import { rootRouteRef } from '../../routes'; +import { nextRouteRef } from '../routes'; jest.mock('react-router-dom', () => { return { @@ -73,7 +77,9 @@ describe('TemplateWizardPage', () => { const { findByRole, getByRole } = await renderInTestApp( - , + + , + , { mountedRoutes: { diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx index 92b6b005ea..c4e8a51e1f 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useContext, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { Page, Header, @@ -22,7 +22,6 @@ import { InfoCard, MarkdownContent, } from '@backstage/core-components'; -import { NextFieldExtensionOptions } from '../../extensions'; import { Navigate, useNavigate } from 'react-router-dom'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { @@ -32,21 +31,23 @@ import { useRouteRef, useRouteRefParams, } from '@backstage/core-plugin-api'; -import { scaffolderApiRef } from '../../api'; +import { + scaffolderApiRef, + useTemplateSecrets, +} from '@backstage/plugin-scaffolder-react'; import useAsync from 'react-use/lib/useAsync'; import { makeStyles } from '@material-ui/core'; -import { Stepper } from './Stepper'; import { BackstageTheme } from '@backstage/theme'; import { - nextRouteRef, - scaffolderTaskRouteRef, - selectedTemplateRouteRef, -} from '../../routes'; -import { SecretsContext } from '../../components/secrets/SecretsContext'; + Stepper, + NextFieldExtensionOptions, +} from '@backstage/plugin-scaffolder-react'; import { JsonValue } from '@backstage/types'; -import type { FormProps } from '../types'; +import { FormProps } from '../types'; +import { nextRouteRef } from '../routes'; +import { scaffolderTaskRouteRef, selectedTemplateRouteRef } from '../../routes'; -export type TemplateWizardPageProps = { +type TemplateWizardPageProps = { customFieldExtensions: NextFieldExtensionOptions[]; FormProps?: FormProps; }; @@ -77,7 +78,7 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => { const styles = useStyles(); const rootRef = useRouteRef(nextRouteRef); const taskRoute = useRouteRef(scaffolderTaskRouteRef); - const { secrets } = useContext(SecretsContext) ?? {}; + const { secrets } = useTemplateSecrets(); const scaffolderApi = useApi(scaffolderApiRef); const navigate = useNavigate(); const { templateName, namespace } = useRouteRefParams( diff --git a/plugins/scaffolder/src/next/index.ts b/plugins/scaffolder/src/next/index.ts index 3a874e3b41..1ca9096459 100644 --- a/plugins/scaffolder/src/next/index.ts +++ b/plugins/scaffolder/src/next/index.ts @@ -16,5 +16,5 @@ export * from './Router'; export * from './TemplateListPage'; export * from './TemplateWizardPage'; - -export type { FormProps } from './types'; +export * from './types'; +export * from './routes'; diff --git a/plugins/scaffolder/src/next/routes.ts b/plugins/scaffolder/src/next/routes.ts new file mode 100644 index 0000000000..620122ee60 --- /dev/null +++ b/plugins/scaffolder/src/next/routes.ts @@ -0,0 +1,36 @@ +/* + * 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 { createRouteRef, createSubRouteRef } from '@backstage/core-plugin-api'; + +/** @alpha */ +export const nextRouteRef = createRouteRef({ + id: 'scaffolder/next', +}); + +/** @alpha */ +export const nextSelectedTemplateRouteRef = createSubRouteRef({ + id: 'scaffolder/next/selected-template', + parent: nextRouteRef, + path: '/templates/:namespace/:templateName', +}); + +/** @alpha */ +export const nextScaffolderTaskRouteRef = createSubRouteRef({ + id: 'scaffolder/next/task', + parent: nextRouteRef, + path: '/tasks/:taskId', +}); diff --git a/plugins/scaffolder/src/next/types.ts b/plugins/scaffolder/src/next/types.ts index 32d0fe85f0..e9c77aa00e 100644 --- a/plugins/scaffolder/src/next/types.ts +++ b/plugins/scaffolder/src/next/types.ts @@ -13,12 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +/** + * These types will be replaced eventually by the one in the scaffolder-react plugin. + * It is a temporary solution to avoid the `/alpha` types being re-exported and that not being supported right now. + * It exists already in the `scaffolder-react` plugin, so you may have to update both files. + */ + import type { FormProps as SchemaFormProps } from '@rjsf/core-v5'; /** * Any `@rjsf/core` form properties that are publicly exposed to the `NextScaffolderpage` * * @alpha + * @deprecated use the import from {@link @backstage/plugin-scaffolder-react/alpha#FormProps} instead */ export type FormProps = Pick< SchemaFormProps, diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index cfa7af8128..f6801c6371 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -15,7 +15,11 @@ */ import { scmIntegrationsApiRef } from '@backstage/integration-react'; -import { scaffolderApiRef, ScaffolderClient } from './api'; +import { + createScaffolderFieldExtension, + scaffolderApiRef, +} from '@backstage/plugin-scaffolder-react'; +import { ScaffolderClient } from './api'; import { EntityPicker, EntityPickerSchema, @@ -34,13 +38,6 @@ import { RepoUrlPicker, RepoUrlPickerSchema, } from './components/fields/RepoUrlPicker/RepoUrlPicker'; -import { createScaffolderFieldExtension } from './extensions'; -import { - nextRouteRef, - registerComponentRouteRef, - rootRouteRef, - viewTechDocRouteRef, -} from './routes'; import { createApiFactory, createPlugin, @@ -57,6 +54,14 @@ import { EntityTagsPicker, EntityTagsPickerSchema, } from './components/fields/EntityTagsPicker/EntityTagsPicker'; +import { + registerComponentRouteRef, + rootRouteRef, + viewTechDocRouteRef, + selectedTemplateRouteRef, + scaffolderTaskRouteRef, +} from './routes'; +import { nextRouteRef } from './next'; /** * The main plugin export for the scaffolder. @@ -84,6 +89,8 @@ export const scaffolderPlugin = createPlugin({ ], routes: { root: rootRouteRef, + selectedTemplate: selectedTemplateRouteRef, + ongoingTask: scaffolderTaskRouteRef, }, externalRoutes: { registerComponent: registerComponentRouteRef, diff --git a/plugins/scaffolder/src/routes.ts b/plugins/scaffolder/src/routes.ts index 7451a3a943..54b7605eb1 100644 --- a/plugins/scaffolder/src/routes.ts +++ b/plugins/scaffolder/src/routes.ts @@ -30,13 +30,16 @@ export const viewTechDocRouteRef = createExternalRouteRef({ params: ['namespace', 'kind', 'name'], }); -/** @public */ +/** + * @public + */ export const rootRouteRef = createRouteRef({ id: 'scaffolder', }); /** * @deprecated This is the old template route, can be deleted before next major release + * @public */ export const legacySelectedTemplateRouteRef = createSubRouteRef({ id: 'scaffolder/legacy/selected-template', @@ -44,37 +47,18 @@ export const legacySelectedTemplateRouteRef = createSubRouteRef({ path: '/templates/:templateName', }); -/** @alpha */ -export const nextRouteRef = createRouteRef({ - id: 'scaffolder/next', -}); - -/** @public */ export const selectedTemplateRouteRef = createSubRouteRef({ id: 'scaffolder/selected-template', parent: rootRouteRef, path: '/templates/:namespace/:templateName', }); -/** @alpha */ -export const nextSelectedTemplateRouteRef = createSubRouteRef({ - id: 'scaffolder/next/selected-template', - parent: nextRouteRef, - path: '/templates/:namespace/:templateName', -}); - export const scaffolderTaskRouteRef = createSubRouteRef({ id: 'scaffolder/task', parent: rootRouteRef, path: '/tasks/:taskId', }); -export const nextScaffolderTaskRouteRef = createSubRouteRef({ - id: 'scaffolder/next/task', - parent: nextRouteRef, - path: '/tasks/:taskId', -}); - export const scaffolderListTaskRouteRef = createSubRouteRef({ id: 'scaffolder/list-tasks', parent: rootRouteRef, diff --git a/yarn.lock b/yarn.lock index 052d0cbec3..a549cab5a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7470,6 +7470,54 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-scaffolder-react@workspace:^, @backstage/plugin-scaffolder-react@workspace:plugins/scaffolder-react": + version: 0.0.0-use.local + resolution: "@backstage/plugin-scaffolder-react@workspace:plugins/scaffolder-react" + dependencies: + "@backstage/catalog-client": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-catalog": "workspace:^" + "@backstage/plugin-catalog-common": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-scaffolder-common": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@backstage/types": "workspace:^" + "@backstage/version-bridge": "workspace:^" + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.57 + "@rjsf/core": ^3.2.1 + "@rjsf/core-v5": "npm:@rjsf/core@^5.0.0-beta.14" + "@rjsf/material-ui": ^3.2.1 + "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@^5.0.0-beta.14" + "@rjsf/utils": ^5.0.0-beta.14 + "@rjsf/validator-ajv6": ^5.0.0-beta.14 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/react-hooks": ^8.0.0 + "@testing-library/user-event": ^14.0.0 + "@types/json-schema": ^7.0.9 + classnames: ^2.2.6 + json-schema: ^0.4.0 + json-schema-library: ^7.3.9 + lodash: ^4.17.21 + qs: ^6.9.4 + zen-observable: ^0.10.0 + zod: ~3.18.0 + zod-to-json-schema: ~3.18.0 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + languageName: unknown + linkType: soft + "@backstage/plugin-scaffolder@workspace:^, @backstage/plugin-scaffolder@workspace:plugins/scaffolder": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder@workspace:plugins/scaffolder" @@ -7490,6 +7538,7 @@ __metadata: "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" + "@backstage/plugin-scaffolder-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@backstage/types": "workspace:^" @@ -7503,9 +7552,7 @@ __metadata: "@rjsf/core": ^3.2.1 "@rjsf/core-v5": "npm:@rjsf/core@^5.0.0-beta.14" "@rjsf/material-ui": ^3.2.1 - "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@^5.0.0-beta.14" "@rjsf/utils": ^5.0.0-beta.14 - "@rjsf/validator-ajv6": ^5.0.0-beta.14 "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 @@ -22399,6 +22446,7 @@ __metadata: "@backstage/plugin-playlist": "workspace:^" "@backstage/plugin-rollbar": "workspace:^" "@backstage/plugin-scaffolder": "workspace:^" + "@backstage/plugin-scaffolder-react": "workspace:^" "@backstage/plugin-search": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/plugin-search-react": "workspace:^"