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:
@@ -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`.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
+2
-2
@@ -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';
|
||||
@@ -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.
|
||||
*
|
||||
+1
-39
@@ -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,
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
+1
-1
@@ -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', () => {
|
||||
+11
-3
@@ -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]) => {
|
||||
+1
-2
@@ -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';
|
||||
+1
-1
@@ -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 () => {
|
||||
+20
-14
@@ -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',
|
||||
);
|
||||
}}
|
||||
>
|
||||
+1
-1
@@ -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', () => {
|
||||
+2
-2
@@ -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';
|
||||
+8
-13
@@ -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);
|
||||
});
|
||||
});
|
||||
+23
-46
@@ -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();
|
||||
});
|
||||
});
|
||||
+32
-5
@@ -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,
|
||||
};
|
||||
+1
-56
@@ -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';
|
||||
+12
-1
@@ -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,
|
||||
});
|
||||
+1
-1
@@ -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', () => {
|
||||
+13
-2
@@ -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';
|
||||
+2
-2
@@ -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'
|
||||
>;
|
||||
+4
-9
@@ -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');
|
||||
});
|
||||
});
|
||||
+21
-11
@@ -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';
|
||||
+1
-1
@@ -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';
|
||||
@@ -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
@@ -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;
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
+4
-2
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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', () => ({
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
export * from './Router';
|
||||
export * from './TemplateListPage';
|
||||
export * from './TemplateWizardPage';
|
||||
|
||||
export type { FormProps } from './types';
|
||||
export * from './types';
|
||||
export * from './routes';
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user