Merge pull request #15399 from backstage/blam/break-up-scaffolder

scaffolder: Introduce the `@backstage/plugin-scaffolder-react` and move re-usable components and types there
This commit is contained in:
Ben Lambert
2023-01-10 15:05:31 +01:00
committed by GitHub
103 changed files with 2097 additions and 936 deletions
+5
View File
@@ -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`.
+45
View File
@@ -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
```
+1
View File
@@ -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:^",
@@ -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<string>) => {
const {
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+12
View File
@@ -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)
+430
View File
@@ -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
/// <reference types="react" />
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<TReturnValue, TInputProps>,
): Extension<FieldExtensionComponent<TReturnValue, TInputProps>>;
// @public
export function createScaffolderFieldExtension<
TReturnValue = unknown,
TInputProps = unknown,
>(
options: FieldExtensionOptions<TReturnValue, TInputProps>,
): Extension<FieldExtensionComponent<TReturnValue, TInputProps>>;
// @public
export type CustomFieldExtensionSchema = {
returnValue: JSONSchema7;
uiOptions?: JSONSchema7;
};
// @public
export type CustomFieldValidator<TFieldReturnValue> = (
data: TFieldReturnValue,
field: FieldValidation,
context: {
apiHolder: ApiHolder;
},
) => void | Promise<void>;
// @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<TFieldReturnValue> {
// (undocumented)
uiSchema: FieldProps['uiSchema'] & {
'ui:options'?: TUiOptions;
};
}
// @public
export type FieldExtensionOptions<
TFieldReturnValue = unknown,
TInputProps = unknown,
> = {
name: string;
component: (
props: FieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: CustomFieldValidator<TFieldReturnValue>;
schema?: CustomFieldExtensionSchema;
};
// @alpha
export type FormProps = Pick<
FormProps_2,
'transformErrors' | 'noHtml5Validate'
>;
// @public
export type ListActionsResponse = Array<Action>;
// @public
export type LogEvent = {
type: 'log' | 'completion';
body: {
message: string;
stepId?: string;
status?: ScaffolderTaskStatus;
};
createdAt: string;
id: string;
taskId: string;
};
// @alpha
export type NextCustomFieldValidator<TFieldReturnValue> = (
data: TFieldReturnValue,
field: FieldValidation_2,
context: {
apiHolder: ApiHolder;
formData: JsonObject;
},
) => void | Promise<void>;
// @alpha
export interface NextFieldExtensionComponentProps<
TFieldReturnValue,
TUiOptions = {},
> extends PropsWithChildren<FieldProps_2<TFieldReturnValue>> {
// (undocumented)
uiSchema?: UiSchema<TFieldReturnValue> & {
'ui:options'?: TUiOptions & UIOptionsType;
};
}
// @alpha
export type NextFieldExtensionOptions<
TFieldReturnValue = unknown,
TInputProps = unknown,
> = {
name: string;
component: (
props: NextFieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: NextCustomFieldValidator<TFieldReturnValue>;
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<ScaffolderDryRunResponse>;
// (undocumented)
getIntegrationsList(
options: ScaffolderGetIntegrationsListOptions,
): Promise<ScaffolderGetIntegrationsListResponse>;
// (undocumented)
getTask(taskId: string): Promise<ScaffolderTask>;
// (undocumented)
getTemplateParameterSchema(
templateRef: string,
): Promise<TemplateParameterSchema>;
listActions(): Promise<ListActionsResponse>;
// (undocumented)
listTasks?(options: { filterByOwnership: 'owned' | 'all' }): Promise<{
tasks: ScaffolderTask[];
}>;
scaffold(
options: ScaffolderScaffoldOptions,
): Promise<ScaffolderScaffoldResponse>;
// (undocumented)
streamLogs(options: ScaffolderStreamLogsOptions): Observable<LogEvent>;
}
// @public (undocumented)
export const scaffolderApiRef: ApiRef<ScaffolderApi>;
// @public (undocumented)
export interface ScaffolderDryRunOptions {
// (undocumented)
directoryContents: {
path: string;
base64Content: string;
}[];
// (undocumented)
secrets?: Record<string, string>;
// (undocumented)
template: JsonValue;
// (undocumented)
values: JsonObject;
}
// @public (undocumented)
export interface ScaffolderDryRunResponse {
// (undocumented)
directoryContents: Array<{
path: string;
base64Content: string;
executable: boolean;
}>;
// (undocumented)
log: Array<Pick<LogEvent, 'body'>>;
// (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<string, string>;
// (undocumented)
templateRef: string;
// (undocumented)
values: Record<string, JsonValue>;
}
// @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<string, string>;
// (undocumented)
setSecrets: (input: Record<string, string>) => 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<any, any>[];
templateName?: string;
FormProps?: FormProps;
initialState?: Record<string, JsonValue>;
onComplete: (values: Record<string, JsonValue>) => Promise<void>;
};
// @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<TemplateCardProps>;
};
// (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<unknown, unknown>,
>(
outlet: React.ReactNode,
) => TComponentDataType[];
// @alpha
export const useFormDataFromQuery: (
initialState?: Record<string, JsonValue>,
) => [Record<string, any>, Dispatch<SetStateAction<Record<string, any>>>];
// @alpha
export const useTemplateSchema: (manifest: TemplateParameterSchema) => {
steps: ParsedTemplateSchema[];
};
// @public
export const useTemplateSecrets: () => ScaffolderUseTemplateSecrets;
// (No @packageDocumentation comment for this package)
```
+85
View File
@@ -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"
]
}
@@ -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';
+28
View File
@@ -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<ScaffolderApi>({
id: 'plugin.scaffolder.service',
}),
);
@@ -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.
*
@@ -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<TReturnValue, TInputProps>,
): Extension<FieldExtensionComponent<TReturnValue, TInputProps>> {
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';
@@ -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 `<FieldExtension />` component
*/
export const FIELD_EXTENSION_KEY = 'scaffolder.extensions.field.v1';
@@ -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<TFieldReturnValue> = (
data: TFieldReturnValue,
field: FieldValidation,
context: { apiHolder: ApiHolder },
) => void | Promise<void>;
/**
* 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<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: CustomFieldValidator<TFieldReturnValue>;
schema?: CustomFieldExtensionSchema;
};
/**
* Type for field extensions and being able to type
* incoming props easier.
*
* @public
*/
export interface FieldExtensionComponentProps<
TFieldReturnValue,
TUiOptions extends {} = {},
> extends FieldProps<TFieldReturnValue> {
uiSchema: FieldProps['uiSchema'] & {
'ui:options'?: TUiOptions;
};
}
@@ -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';
@@ -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<TComponentDataType>({
key: FIELD_EXTENSION_KEY,
}),
);
};
+23
View File
@@ -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';
@@ -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', () => {
@@ -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]) => {
@@ -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';
@@ -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 () => {
@@ -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<any, any>[];
onComplete: (values: Record<string, JsonValue>) => Promise<void>;
templateName?: string;
FormProps?: FormProps;
initialState?: Record<string, JsonValue>;
onComplete: (values: Record<string, JsonValue>) => Promise<void>;
};
// 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<string, FieldValidation>
@@ -196,7 +202,7 @@ export const Stepper = (props: StepperProps) => {
: undefined;
analytics.captureEvent(
'create',
name || `new ${templateName}`,
name ?? props.templateName ?? 'unknown',
);
}}
>
@@ -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', () => {
@@ -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,
@@ -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';
@@ -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', () => {
>
<TemplateCard template={mockTemplate} />
</TestApiProvider>,
{ mountedRoutes: { '/': nextRouteRef } },
);
expect(getByText('bob')).toBeInTheDocument();
@@ -84,7 +83,6 @@ describe('TemplateCard', () => {
>
<TemplateCard template={mockTemplate} />
</TestApiProvider>,
{ mountedRoutes: { '/': nextRouteRef } },
);
const description = getByText('hello');
@@ -115,7 +113,6 @@ describe('TemplateCard', () => {
>
<TemplateCard template={mockTemplate} />
</TestApiProvider>,
{ mountedRoutes: { '/': nextRouteRef } },
);
expect(getByText('No description')).toBeInTheDocument();
@@ -145,7 +142,6 @@ describe('TemplateCard', () => {
>
<TemplateCard template={mockTemplate} />
</TestApiProvider>,
{ mountedRoutes: { '/': nextRouteRef } },
);
for (const tag of mockTemplate.metadata.tags!) {
@@ -185,7 +181,6 @@ describe('TemplateCard', () => {
</TestApiProvider>,
{
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(
<TestApiProvider
@@ -220,20 +216,19 @@ describe('TemplateCard', () => {
],
]}
>
<TemplateCard template={mockTemplate} />
<TemplateCard template={mockTemplate} onSelected={mockOnSelected} />
</TestApiProvider>,
{
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);
});
});
@@ -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<BackstageTheme>(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 (
<Card>
<CardHeader template={template} />
@@ -159,22 +135,18 @@ export const TemplateCard = (props: TemplateCardProps) => {
</Grid>
</>
)}
{(!!viewTechDocsLink || template.metadata.links?.length) && (
{(props.additionalLinks || template.metadata.links?.length) && (
<>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Grid container spacing={2}>
{viewTechDocsLink && (
{props.additionalLinks?.map(({ icon, text, url }) => (
<Grid className={styles.linkText} item xs={6}>
<CardLink
icon={iconResolver('docs')}
text="View TechDocs"
url={viewTechDocsLink}
/>
<CardLink icon={icon} text={text} url={url} />
</Grid>
)}
))}
{template.metadata.links?.map(({ url, icon, title }) => (
<Grid className={styles.linkText} item xs={6}>
<CardLink
@@ -204,7 +176,12 @@ export const TemplateCard = (props: TemplateCardProps) => {
</>
)}
</div>
<Button size="small" variant="outlined" color="primary" to={href}>
<Button
size="small"
variant="outlined"
color="primary"
onClick={() => props.onSelected?.(template)}
>
Choose
</Button>
</div>
@@ -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';
@@ -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(
<TemplateGroup onSelected={jest.fn()} title="Test" templates={[]} />,
);
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(
<TemplateGroup
onSelected={mockOnSelected}
title="Test"
templates={mockTemplates}
/>,
);
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(
<TemplateGroup
onSelected={mockOnSelected}
title="Test"
templates={mockTemplates}
components={{ CardComponent: mockTemplateCardComponent }}
/>,
);
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(
<TemplateGroup onSelected={jest.fn()} title="Test" templates={[]} />,
);
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(
<TemplateGroup
onSelected={jest.fn()}
title="Test"
templates={mockTemplates}
/>,
);
expect(getByText('Test')).toBeInTheDocument();
});
it('should allow for passing through a user given title component', () => {
const TitleComponent = <p>Im a custom header</p>;
const { getByText } = render(
<TemplateGroup
onSelected={jest.fn()}
templates={[]}
title={TitleComponent}
/>,
);
expect(getByText('Im a custom header')).toBeInTheDocument();
});
});
@@ -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<TemplateCardProps>;
};
}
/**
* 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' ? <ContentHeader title={title} /> : title;
@@ -59,8 +81,13 @@ export const TemplateGroup = (props: TemplateGroupProps) => {
<Content>
{titleComponent}
<ItemCardGrid>
{templates.map(template => (
<Card key={stringifyEntityRef(template)} template={template} />
{templates.map(({ template, additionalLinks }) => (
<Card
key={stringifyEntityRef(template)}
additionalLinks={additionalLinks}
template={template}
onSelected={onSelected}
/>
))}
</ItemCardGrid>
</Content>
@@ -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';
@@ -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';
@@ -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<TReturnValue, TInputProps>,
): Extension<FieldExtensionComponent<TReturnValue, TInputProps>> {
return {
expose() {
const FieldExtensionDataHolder: any = () => null;
attachComponentData(
FieldExtensionDataHolder,
FIELD_EXTENSION_KEY,
options,
);
return FieldExtensionDataHolder;
},
};
}
export type {
NextCustomFieldValidator,
NextFieldExtensionOptions,
NextFieldExtensionComponentProps,
};
@@ -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<TFieldReturnValue> = (
data: TFieldReturnValue,
field: FieldValidation,
context: { apiHolder: ApiHolder },
) => void | Promise<void>;
/**
* 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<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: CustomFieldValidator<TFieldReturnValue>;
schema?: CustomFieldExtensionSchema;
};
/**
* Type for field extensions and being able to type
* incoming props easier.
*
* @public
*/
export interface FieldExtensionComponentProps<
TFieldReturnValue,
TUiOptions extends {} = {},
> extends FieldProps<TFieldReturnValue> {
uiSchema: FieldProps['uiSchema'] & {
'ui:options'?: TUiOptions;
};
}
import { CustomFieldExtensionSchema } from '../../extensions';
/**
* Type for Field Extension Props for RJSF v5
@@ -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';
@@ -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<string, JsonValue>,
) => {
return useState<Record<string, any>>(() => {
if (initialState) {
return initialState;
}
const query = qs.parse(window.location.search, {
ignoreQueryPrefix: true,
});
@@ -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', () => {
@@ -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[] } => {
@@ -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';
@@ -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';
@@ -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 = {
@@ -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'
>;
@@ -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');
});
});
@@ -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<string, string>;
setSecrets: React.Dispatch<React.SetStateAction<Record<string, string>>>;
};
/**
* The actual context object.
* The context to hold the Secrets.
*/
export const SecretsContext = createContext<SecretsContextContents | undefined>(
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<Record<string, string>>({});
return (
<SecretsContext.Provider value={{ secrets, setSecrets }}>
<SecretsContext.Provider
value={createVersionedValueMap({ 1: { secrets, setSecrets } })}
>
{children}
</SecretsContext.Provider>
);
@@ -54,21 +61,24 @@ export const SecretsContextProvider = ({ children }: PropsWithChildren<{}>) => {
*/
export interface ScaffolderUseTemplateSecrets {
setSecrets: (input: Record<string, string>) => void;
secrets: Record<string, string>;
}
/**
* 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<string, string>) => {
@@ -77,5 +87,5 @@ export const useTemplateSecrets = (): ScaffolderUseTemplateSecrets => {
[updateSecrets],
);
return { setSecrets };
return { setSecrets, secrets };
};
@@ -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';
@@ -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';
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { 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;
}>;
};
+119 -295
View File
@@ -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<TReturnValue, TInputProps>,
): Extension<FieldExtensionComponent<TReturnValue, TInputProps>>;
// @public
export function createScaffolderFieldExtension<
TReturnValue = unknown,
TInputProps = unknown,
>(
options: FieldExtensionOptions<TReturnValue, TInputProps>,
): Extension<FieldExtensionComponent<TReturnValue, TInputProps>>;
// @public @deprecated (undocumented)
export const createScaffolderFieldExtension: typeof createScaffolderFieldExtension_2;
// @public
export function createScaffolderLayout<TInputProps = unknown>(
options: LayoutOptions,
): Extension<LayoutComponent<TInputProps>>;
// @public
export type CustomFieldExtensionSchema = {
returnValue: JSONSchema7;
uiOptions?: JSONSchema7;
};
// @public @deprecated (undocumented)
export type CustomFieldExtensionSchema = CustomFieldExtensionSchema_2;
// @public @deprecated (undocumented)
export type CustomFieldValidator<TReturnFieldData> =
CustomFieldValidator_2<TReturnFieldData>;
// @public
export type CustomFieldValidator<TFieldReturnValue> = (
data: TFieldReturnValue,
field: FieldValidation,
context: {
apiHolder: ApiHolder;
},
) => void | Promise<void>;
// @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<TFieldReturnValue> {
// (undocumented)
uiSchema: FieldProps['uiSchema'] & {
'ui:options'?: TUiOptions;
};
}
> = FieldExtensionComponentProps_2<TFieldReturnValue, TUiOptions>;
// @public
export type FieldExtensionOptions<
TFieldReturnValue = unknown,
TInputProps = unknown,
> = {
name: string;
component: (
props: FieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: CustomFieldValidator<TFieldReturnValue>;
schema?: CustomFieldExtensionSchema;
};
// @public @deprecated (undocumented)
export type FieldExtensionOptions = FieldExtensionOptions_2;
// @public
export interface FieldSchema<TReturn, TUiOptions> {
// (undocumented)
readonly schema: CustomFieldExtensionSchema;
readonly schema: CustomFieldExtensionSchema_2;
// (undocumented)
readonly type: FieldExtensionComponentProps<TReturn, TUiOptions>;
readonly type: FieldExtensionComponentProps_2<TReturn, TUiOptions>;
// (undocumented)
readonly uiOptionsType: TUiOptions;
}
// @alpha
// @alpha @deprecated
export type FormProps = Pick<
FormProps_3,
'transformErrors' | 'noHtml5Validate'
@@ -212,21 +175,11 @@ export interface LayoutOptions<P = any> {
// @public
export type LayoutTemplate<T = any> = FormProps_2<T>['ObjectFieldTemplate'];
// @public
export type ListActionsResponse = Array<Action>;
// @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<TFieldReturnValue> = (
data: TFieldReturnValue,
field: FieldValidation_2,
context: {
apiHolder: ApiHolder;
formData: JsonObject;
},
) => void | Promise<void>;
// @alpha
export interface NextFieldExtensionComponentProps<
TFieldReturnValue,
TUiOptions = {},
> extends PropsWithChildren<FieldProps_2<TFieldReturnValue>> {
// (undocumented)
uiSchema?: UiSchema<TFieldReturnValue> & {
'ui:options'?: TUiOptions & UIOptionsType;
};
}
// @alpha
export type NextFieldExtensionOptions<
TFieldReturnValue = unknown,
TInputProps = unknown,
> = {
name: string;
component: (
props: NextFieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: NextCustomFieldValidator<TFieldReturnValue>;
schema?: CustomFieldExtensionSchema;
};
// @alpha (undocumented)
export const nextRouteRef: RouteRef<undefined>;
@@ -296,13 +215,18 @@ export const NextScaffolderPage: (
props: PropsWithChildren<NextRouterProps>,
) => 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<undefined>;
// @public
@@ -467,37 +391,14 @@ export type RouterProps = {
};
};
// @public
export interface ScaffolderApi {
// (undocumented)
dryRun?(options: ScaffolderDryRunOptions): Promise<ScaffolderDryRunResponse>;
// (undocumented)
getIntegrationsList(
options: ScaffolderGetIntegrationsListOptions,
): Promise<ScaffolderGetIntegrationsListResponse>;
// (undocumented)
getTask(taskId: string): Promise<ScaffolderTask>;
// (undocumented)
getTemplateParameterSchema(
templateRef: string,
): Promise<TemplateParameterSchema>;
listActions(): Promise<ListActionsResponse>;
// (undocumented)
listTasks?(options: { filterByOwnership: 'owned' | 'all' }): Promise<{
tasks: ScaffolderTask[];
}>;
scaffold(
options: ScaffolderScaffoldOptions,
): Promise<ScaffolderScaffoldResponse>;
// (undocumented)
streamLogs(options: ScaffolderStreamLogsOptions): Observable<LogEvent>;
}
// @public @deprecated (undocumented)
export type ScaffolderApi = ScaffolderApi_2;
// @public @deprecated (undocumented)
export const scaffolderApiRef: ApiRef<ScaffolderApi_2>;
// @public
export const scaffolderApiRef: ApiRef<ScaffolderApi>;
// @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<ScaffolderDryRunResponse>;
dryRun(
options: ScaffolderDryRunOptions_2,
): Promise<ScaffolderDryRunResponse_2>;
// (undocumented)
getIntegrationsList(
options: ScaffolderGetIntegrationsListOptions,
): Promise<ScaffolderGetIntegrationsListResponse>;
options: ScaffolderGetIntegrationsListOptions_2,
): Promise<ScaffolderGetIntegrationsListResponse_2>;
// (undocumented)
getTask(taskId: string): Promise<ScaffolderTask>;
getTask(taskId: string): Promise<ScaffolderTask_2>;
// (undocumented)
getTemplateParameterSchema(
templateRef: string,
): Promise<TemplateParameterSchema>;
): Promise<TemplateParameterSchema_2>;
// (undocumented)
listActions(): Promise<ListActionsResponse>;
listActions(): Promise<ListActionsResponse_2>;
// (undocumented)
listTasks(options: { filterByOwnership: 'owned' | 'all' }): Promise<{
tasks: ScaffolderTask[];
tasks: ScaffolderTask_2[];
}>;
// (undocumented)
scaffold(
options: ScaffolderScaffoldOptions,
): Promise<ScaffolderScaffoldResponse>;
options: ScaffolderScaffoldOptions_2,
): Promise<ScaffolderScaffoldResponse_2>;
// (undocumented)
streamLogs(options: ScaffolderStreamLogsOptions): Observable<LogEvent>;
streamLogs(options: ScaffolderStreamLogsOptions_2): Observable<LogEvent_2>;
}
// @public (undocumented)
export interface ScaffolderDryRunOptions {
// (undocumented)
directoryContents: {
path: string;
base64Content: string;
}[];
// (undocumented)
secrets?: Record<string, string>;
// (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<Pick<LogEvent, 'body'>>;
// (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<undefined>;
selectedTemplate: SubRouteRef<
PathParams<'/templates/:namespace/:templateName'>
>;
ongoingTask: SubRouteRef<PathParams<'/tasks/:taskId'>>;
},
{
registerComponent: ExternalRouteRef<undefined, true>;
@@ -615,64 +485,26 @@ export const scaffolderPlugin: BackstagePlugin<
{}
>;
// @public
export interface ScaffolderScaffoldOptions {
// (undocumented)
secrets?: Record<string, string>;
// (undocumented)
templateRef: string;
// (undocumented)
values: Record<string, JsonValue>;
}
// @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<string, string>) => 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;
```
+2 -3
View File
@@ -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",
+3 -18
View File
@@ -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<ScaffolderApi>({
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<ScaffolderScaffoldResponse> {
@@ -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<ScaffolderApi> = {
scaffold: jest.fn(),
@@ -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 = () => {
<TableCell>
<>
{[props.type].flat().map(type => (
<Chip label={type} />
<Chip label={type} key={type} />
))}
</>
</TableCell>
@@ -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('<ListTasksPage />', () => {
const catalogApi: jest.Mocked<CatalogApi> = {
@@ -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';
@@ -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('<TemplateTitleColumn />', () => {
const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
@@ -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';
@@ -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<ReviewStepProps>;
};
export function getSchemasFromSteps(steps: Step[]): {
uiSchema: UiSchema;
mergedSchema: JsonObject;
schema: JsonObject;
}[] {
export function getSchemasFromSteps(steps: Step[]) {
return steps.map(({ schema }) => ({
mergedSchema: schema,
...extractSchemaFromStep(schema),
+12 -22
View File
@@ -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<FieldExtensionOptions>({
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({
@@ -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?:
@@ -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 () => {
@@ -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');
@@ -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';
@@ -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';
@@ -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;
@@ -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';
@@ -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';
@@ -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';
@@ -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';
@@ -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';
@@ -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 =
@@ -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';
@@ -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(
<ApiProvider apis={apis}>
<TemplatePage />
<SecretsContextProvider>
<TemplatePage />
</SecretsContextProvider>
</ApiProvider>,
{
mountedRoutes: {
@@ -146,7 +151,9 @@ describe('TemplatePage', () => {
scaffolderApiMock.getTemplateParameterSchema.mockReturnValueOnce(promise);
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<TemplatePage />
<SecretsContextProvider>
<TemplatePage />
</SecretsContextProvider>
</ApiProvider>,
{
mountedRoutes: {
@@ -187,7 +194,9 @@ describe('TemplatePage', () => {
});
const { findByLabelText, findByText } = await renderInTestApp(
<ApiProvider apis={apis}>
<TemplatePage />
<SecretsContextProvider>
<TemplatePage />
</SecretsContextProvider>
</ApiProvider>,
{
mountedRoutes: {
@@ -234,7 +243,14 @@ describe('TemplatePage', () => {
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<Routes>
<Route path="/create/test" element={<TemplatePage />} />
<Route
path="/create/test"
element={
<SecretsContextProvider>
<TemplatePage />
</SecretsContextProvider>
}
/>
<Route path="/create" element={<>This is root</>} />
</Routes>
</ApiProvider>,
@@ -287,7 +303,9 @@ describe('TemplatePage', () => {
const { findByText, findByLabelText, findAllByRole, findByRole } =
await renderInTestApp(
<ApiProvider apis={apis}>
<TemplatePage />
<SecretsContextProvider>
<TemplatePage />
</SecretsContextProvider>
</ApiProvider>,
{
mountedRoutes: {
@@ -331,7 +349,9 @@ describe('TemplatePage', () => {
await renderInTestApp(
<ApiProvider apis={apis}>
<TemplatePage />
<SecretsContextProvider>
<TemplatePage />
</SecretsContextProvider>
</ApiProvider>,
{
mountedRoutes: {
@@ -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(
@@ -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';
@@ -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';
@@ -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 <div data-testid="current-secrets">{JSON.stringify(value)}</div>;
const { secrets } = useTemplateSecrets();
return (
<div data-testid="current-secrets">{JSON.stringify({ secrets })}</div>
);
};
const { getAllByRole, getByTestId } = await renderInTestApp(
<TestApiProvider
@@ -30,7 +30,7 @@ import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils';
import { RepoUrlPickerProps } from './schema';
import { RepoUrlPickerState } from './types';
import useDebounce from 'react-use/lib/useDebounce';
import { useTemplateSecrets } from '../../secrets';
import { useTemplateSecrets } from '@backstage/plugin-scaffolder-react';
export { RepoUrlPickerSchema } from './schema';
@@ -16,7 +16,7 @@
import React from 'react';
import { RepoUrlPickerHost } from './RepoUrlPickerHost';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { scaffolderApiRef } from '../../../api';
import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
import { fireEvent, within } from '@testing-library/react';
describe('RepoUrlPickerHostField', () => {
@@ -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: {
@@ -19,7 +19,7 @@ import zodToJsonSchema from 'zod-to-json-schema';
import {
CustomFieldExtensionSchema,
FieldExtensionComponentProps,
} from '../../extensions';
} from '@backstage/plugin-scaffolder-react';
/**
* @public
@@ -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';
+1 -2
View File
@@ -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';
+190
View File
@@ -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<TReturnFieldData> =
CustomFieldValidatorTemp<TReturnFieldData>;
/**
* @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<TFieldReturnValue, TUiOptions>;
/**
* @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;
+17 -47
View File
@@ -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';
@@ -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', () => ({
+8 -21
View File
@@ -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<NextRouterProps>) => {
const { components: { TemplateCardComponent } = {} } = props;
const outlet = useOutlet() || props.children;
const customFieldExtensions = useElementFilter(outlet, elements =>
elements
.selectByComponentData({
key: FIELD_EXTENSION_WRAPPER_KEY,
})
.findComponentData<FieldExtensionOptions | NextFieldExtensionOptions>({
key: FIELD_EXTENSION_KEY,
}),
);
const customFieldExtensions =
useCustomFieldExtensions<NextFieldExtensionOptions>(outlet);
const fieldExtensions = [
...customFieldExtensions,
...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter(
@@ -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(<TemplateGroup title="Test" templates={[]} />);
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(<TemplateGroup title="Test" templates={mockTemplates} />);
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(
<TemplateGroup
title="Test"
templates={mockTemplates}
components={{ CardComponent: mockTemplateCardComponent }}
/>,
);
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(<TemplateGroup title="Test" templates={[]} />);
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(
<TemplateGroup title="Test" templates={mockTemplates} />,
);
expect(getByText('Test')).toBeInTheDocument();
});
it('should allow for passing through a user given title component', () => {
const TitleComponent = <p>Im a custom header</p>;
const { getByText } = render(
<TemplateGroup templates={[]} title={TitleComponent} />,
);
expect(getByText('Im a custom header')).toBeInTheDocument();
});
});
@@ -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(
<TestApiProvider apis={[[errorApiRef, {}]]}>
<TemplateGroups groups={[]} />
</TestApiProvider>,
{
mountedRoutes: {
'/next': nextRouteRef,
},
},
);
expect(await findByTestId('progress')).toBeInTheDocument();
@@ -51,10 +58,15 @@ describe('TemplateGroups', () => {
const errorApi = {
post: jest.fn(),
};
render(
await renderInTestApp(
<TestApiProvider apis={[[errorApiRef, errorApi]]}>
<TemplateGroups groups={[]} />
</TestApiProvider>,
{
mountedRoutes: {
'/next': nextRouteRef,
},
},
);
expect(errorApi.post).toHaveBeenCalledWith(mockError);
@@ -67,10 +79,15 @@ describe('TemplateGroups', () => {
error: null,
});
const { findByText } = render(
const { findByText } = await renderInTestApp(
<TestApiProvider apis={[[errorApiRef, {}]]}>
<TemplateGroups groups={[]} />
</TestApiProvider>,
{
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(
<TestApiProvider apis={[[errorApiRef, {}]]}>
<TemplateGroups groups={[]} />
</TestApiProvider>,
{
mountedRoutes: {
'/next': nextRouteRef,
},
},
);
expect(await findByText(/No templates found/)).toBeInTheDocument();
@@ -118,14 +140,23 @@ describe('TemplateGroups', () => {
error: null,
});
render(
await renderInTestApp(
<TestApiProvider apis={[[errorApiRef, {}]]}>
<TemplateGroups groups={[{ title: 'all', filter: () => true }]} />
</TestApiProvider>,
{
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(
<TestApiProvider apis={[[errorApiRef, {}]]}>
<TemplateGroups
groups={[{ title: 'all', filter: e => e.metadata.name === 't1' }]}
/>
</TestApiProvider>,
{
mountedRoutes: {
'/next': nextRouteRef,
},
},
);
expect(TemplateGroup).toHaveBeenCalledWith(
expect.objectContaining({ templates: [mockEntities[0]] }),
expect.objectContaining({
templates: [expect.objectContaining({ template: mockEntities[0] })],
}),
{},
);
});
@@ -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 <Progress />;
@@ -65,16 +89,41 @@ export const TemplateGroups = (props: TemplateGroupsProps) => {
return (
<>
{groups.map(({ title, filter }, index) => (
<TemplateGroup
key={index}
templates={entities.filter((e): e is TemplateEntityV1beta3 =>
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 (
<TemplateGroup
key={index}
templates={templates}
title={title}
components={{ CardComponent: TemplateCardComponent }}
onSelected={onSelected}
/>
);
})}
</>
);
};
@@ -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', () => {
@@ -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<{
@@ -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(
<ApiProvider apis={apis}>
<TemplateWizardPage customFieldExtensions={[]} />,
<SecretsContextProvider>
<TemplateWizardPage customFieldExtensions={[]} />,
</SecretsContextProvider>
</ApiProvider>,
{
mountedRoutes: {
@@ -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<any, any>[];
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(
+2 -2
View File
@@ -16,5 +16,5 @@
export * from './Router';
export * from './TemplateListPage';
export * from './TemplateWizardPage';
export type { FormProps } from './types';
export * from './types';
export * from './routes';
+36
View File
@@ -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',
});
+8
View File
@@ -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,

Some files were not shown because too many files have changed in this diff Show More