diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index c2296d01e7..d2dde95f68 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -31,7 +31,6 @@ import { AppRouter, ConfigReader, defaultConfigLoader, - FeatureFlagged, FlatRoutes, } from '@backstage/core-app-api'; import { @@ -64,7 +63,6 @@ import { GcpProjectsPage } from '@backstage/plugin-gcp-projects'; import { HomepageCompositionRoot, VisitListener } from '@backstage/plugin-home'; import { LighthousePage } from '@backstage/plugin-lighthouse'; import { NewRelicPage } from '@backstage/plugin-newrelic'; -import { LegacyScaffolderPage } from '@backstage/plugin-scaffolder/alpha'; import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder'; import { ScaffolderFieldExtensions, @@ -96,10 +94,7 @@ import { apis } from './apis'; import { entityPage } from './components/catalog/EntityPage'; import { homePage } from './components/home/HomePage'; import { Root } from './components/Root'; -import { - DelayingComponentFieldExtension, - LowerCaseValuePickerFieldExtension, -} from './components/scaffolder/customScaffolderExtensions'; +import { DelayingComponentFieldExtension } from './components/scaffolder/customScaffolderExtensions'; import { defaultPreviewTemplate } from './components/scaffolder/defaultPreviewTemplate'; import { searchPage } from './components/search/SearchPage'; import { providers } from './identityProviders'; @@ -236,53 +231,28 @@ const routes = ( - - - entity?.metadata?.tags?.includes('recommended') ?? false, - }, - ]} - /> - } - > - - - - - - - - - - - entity?.metadata?.tags?.includes('recommended') ?? false, - }, - ]} - /> - } - > - - - - - - - - + + entity?.metadata?.tags?.includes('recommended') ?? false, + }, + ]} + /> + } + > + + + + + + + } /> ( - options: LegacyFieldExtensionOptions, -): Extension> { - return { - expose() { - const FieldExtensionDataHolder: any = () => null; - - attachComponentData( - FieldExtensionDataHolder, - FIELD_EXTENSION_KEY, - options, - ); - - return FieldExtensionDataHolder; - }, - }; -} - -export type { - LegacyCustomFieldValidator, - LegacyFieldExtensionOptions, - LegacyFieldExtensionComponentProps, -}; diff --git a/plugins/scaffolder-react/src/legacy/extensions/types.ts b/plugins/scaffolder-react/src/legacy/extensions/types.ts deleted file mode 100644 index 54c38ad2cd..0000000000 --- a/plugins/scaffolder-react/src/legacy/extensions/types.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 } from '@rjsf/utils'; -import { - CustomFieldExtensionSchema, - ScaffolderRJSFFieldProps, -} from '@backstage/plugin-scaffolder-react'; - -/** - * Field validation type for Custom Field Extensions. - * - * @alpha - */ -export type LegacyCustomFieldValidator = ( - data: TFieldReturnValue, - field: FieldValidation, - context: { apiHolder: ApiHolder }, -) => void | Promise; - -/** - * Type for the Custom Field Extension with the - * name and components and validation function. - * - * @alpha - */ -export type LegacyFieldExtensionOptions< - TFieldReturnValue = unknown, - TInputProps = unknown, -> = { - name: string; - component: ( - props: LegacyFieldExtensionComponentProps, - ) => JSX.Element | null; - validation?: LegacyCustomFieldValidator; - schema?: CustomFieldExtensionSchema; -}; - -/** - * Type for field extensions and being able to type - * incoming props easier. - * - * @alpha - */ -export interface LegacyFieldExtensionComponentProps< - TFieldReturnValue, - TUiOptions = unknown, -> extends ScaffolderRJSFFieldProps { - uiSchema: ScaffolderRJSFFieldProps['uiSchema'] & { - 'ui:options'?: TUiOptions; - }; -} diff --git a/plugins/scaffolder-react/src/legacy/index.ts b/plugins/scaffolder-react/src/legacy/index.ts deleted file mode 100644 index c539a8ba60..0000000000 --- a/plugins/scaffolder-react/src/legacy/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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 * from './extensions'; diff --git a/plugins/scaffolder/dev/index.tsx b/plugins/scaffolder/dev/index.tsx index b683345ff5..e4425b04bf 100644 --- a/plugins/scaffolder/dev/index.tsx +++ b/plugins/scaffolder/dev/index.tsx @@ -24,7 +24,7 @@ import { } from '@backstage/plugin-catalog-react'; import React from 'react'; import { scaffolderApiRef, ScaffolderClient } from '../src'; -import { ScaffolderPage, LegacyScaffolderPage } from '../src/plugin'; +import { ScaffolderPage } from '../src/plugin'; import { discoveryApiRef, fetchApiRef, @@ -68,11 +68,6 @@ createDevApp() title: 'Create', element: , }) - .addPage({ - path: '/legacy-create', - title: 'Create (next)', - element: , - }) .addPage({ path: '/create-groups', title: 'Groups', @@ -91,22 +86,4 @@ createDevApp() /> ), }) - .addPage({ - path: '/legacy-create-groups', - title: 'Groups (next)', - element: ( - e.metadata.tags?.includes('techdocs') || false, - title: 'Techdocs', - }, - { - filter: e => e.metadata.tags?.includes('react') || false, - title: 'React', - }, - ]} - /> - ), - }) .render(); diff --git a/plugins/scaffolder/src/components/index.ts b/plugins/scaffolder/src/components/index.ts index fd8c11e2da..552a75c99b 100644 --- a/plugins/scaffolder/src/components/index.ts +++ b/plugins/scaffolder/src/components/index.ts @@ -22,5 +22,3 @@ export type { RouterProps } from './Router'; export { OngoingTask as TaskPage } from './OngoingTask'; export type { ReviewStepProps } from '@backstage/plugin-scaffolder-react'; - -export type { TaskPageProps } from '../legacy/TaskPage'; diff --git a/plugins/scaffolder/src/components/types.ts b/plugins/scaffolder/src/components/types.ts deleted file mode 100644 index b1e99a84e6..0000000000 --- a/plugins/scaffolder/src/components/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2020 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. - */ diff --git a/plugins/scaffolder/src/deprecated.ts b/plugins/scaffolder/src/deprecated.ts index f08a83c594..ffe923058b 100644 --- a/plugins/scaffolder/src/deprecated.ts +++ b/plugins/scaffolder/src/deprecated.ts @@ -212,3 +212,13 @@ export type LayoutTemplate = LayoutTemplateTemp; * @deprecated use import from {@link @backstage/plugin-scaffolder-react#LayoutOptions} instead as this has now been moved. */ export type LayoutOptions = LayoutOptionsTemp; + +/** + * TaskPageProps for constructing a TaskPage + * @param loadingText - Optional loading text shown before a task begins executing. + * @public + * @deprecated - this is a useless type that is no longer used. + */ +export type TaskPageProps = { + loadingText?: string; +}; diff --git a/plugins/scaffolder/src/legacy/MultistepJsonForm/FieldOverrides/DescriptionField.tsx b/plugins/scaffolder/src/legacy/MultistepJsonForm/FieldOverrides/DescriptionField.tsx deleted file mode 100644 index eded38beee..0000000000 --- a/plugins/scaffolder/src/legacy/MultistepJsonForm/FieldOverrides/DescriptionField.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 React from 'react'; -import { MarkdownContent } from '@backstage/core-components'; -import { FieldProps } from '@rjsf/utils'; - -export const DescriptionField = ({ description }: FieldProps) => - description && ; diff --git a/plugins/scaffolder/src/legacy/MultistepJsonForm/FieldOverrides/index.ts b/plugins/scaffolder/src/legacy/MultistepJsonForm/FieldOverrides/index.ts deleted file mode 100644 index d6d0bf3499..0000000000 --- a/plugins/scaffolder/src/legacy/MultistepJsonForm/FieldOverrides/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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. - */ -export { DescriptionField } from './DescriptionField'; diff --git a/plugins/scaffolder/src/legacy/MultistepJsonForm/MultistepJsonForm.test.tsx b/plugins/scaffolder/src/legacy/MultistepJsonForm/MultistepJsonForm.test.tsx deleted file mode 100644 index 3f8e153b65..0000000000 --- a/plugins/scaffolder/src/legacy/MultistepJsonForm/MultistepJsonForm.test.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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 { getReviewData, getUiSchemasFromSteps } from './ReviewStep'; - -describe('MultistepJsonForm', () => { - const formDataMock = { - password: 'password', - masked: 'Some info to mask', - open: 'Some open info', - hidden: 'Some info to hide', - 'other-open': 'Other open info', - }; - - const stepsMock = [ - { - title: 'The test template', - schema: { - title: 'The test template', - properties: { - password: { - title: 'Password', - type: 'string', - 'ui:widget': 'password', - }, - masked: { - title: 'Masked', - type: 'string', - 'ui:backstage': { - review: { - mask: '******', - }, - }, - }, - open: { - title: 'Open info', - type: 'string', - }, - }, - }, - }, - { - title: 'Other fields', - schema: { - title: 'Other fields', - properties: { - hidden: { - title: 'Hidden', - type: 'string', - 'ui:backstage': { - review: { - show: false, - }, - }, - }, - 'other-open': { - title: 'Other Open Info', - type: 'string', - }, - }, - }, - }, - ]; - - test('Fields are defined to be hidden or masked', () => { - const schemas = getUiSchemasFromSteps(stepsMock); - const reviewData = getReviewData(formDataMock, schemas); - - expect(reviewData.password).toBe('******'); - expect(reviewData.masked).toBe('******'); - expect(reviewData.open).toBe('Some open info'); - expect(reviewData.hidden).toBeUndefined(); - expect(reviewData['other-open']).toBe('Other open info'); - }); -}); diff --git a/plugins/scaffolder/src/legacy/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/legacy/MultistepJsonForm/MultistepJsonForm.tsx deleted file mode 100644 index 68b25d97d2..0000000000 --- a/plugins/scaffolder/src/legacy/MultistepJsonForm/MultistepJsonForm.tsx +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright 2020 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 { - Button, - Step as StepUI, - StepContent, - StepLabel, - Stepper, - Typography, -} from '@material-ui/core'; -import { JsonObject } from '@backstage/types'; -import { - errorApiRef, - featureFlagsApiRef, - useAnalytics, - useRouteRefParams, - useApi, -} from '@backstage/core-plugin-api'; -import { FormProps, IChangeEvent, withTheme } from '@rjsf/core'; -import { Theme as MuiTheme } from '@rjsf/material-ui'; -import React, { ComponentType, useState } from 'react'; -import { transformSchemaToProps } from './schema'; -import cloneDeep from 'lodash/cloneDeep'; -import * as fieldOverrides from './FieldOverrides'; -import { ReviewStep } from './ReviewStep'; -import validator from '@rjsf/validator-ajv8'; -import { extractSchemaFromStep } from '@backstage/plugin-scaffolder-react/alpha'; -import { selectedTemplateRouteRef } from '../../routes'; -import { - LayoutOptions, - ReviewStepProps, -} from '@backstage/plugin-scaffolder-react'; - -const Form = withTheme(MuiTheme); - -type Step = { - schema: JsonObject; - title: string; -} & Partial, 'schema'>>; - -/** - * The props for a dynamic form of a scaffolder template. - */ -export type MultistepJsonFormProps = { - /** - * Steps for the form, each contains title and form schema - */ - steps: Step[]; - formData: Record; - onChange: (e: IChangeEvent) => void; - onReset: () => void; - onFinish?: () => Promise; - widgets?: FormProps['widgets']; - fields?: FormProps['fields']; - finishButtonLabel?: string; - layouts: LayoutOptions[]; - ReviewStepComponent?: ComponentType; -}; - -export function getSchemasFromSteps(steps: Step[]) { - return steps.map(({ schema }) => ({ - mergedSchema: schema, - ...extractSchemaFromStep(schema), - })); -} - -/** - * Creates the dynamic form for a scaffolder template. - */ -export const MultistepJsonForm = (props: MultistepJsonFormProps) => { - const { - formData, - onChange, - onReset, - onFinish, - fields, - widgets, - layouts, - ReviewStepComponent, - } = props; - const { templateName } = useRouteRefParams(selectedTemplateRouteRef); - const analytics = useAnalytics(); - const [activeStep, setActiveStep] = useState(0); - const [disableButtons, setDisableButtons] = useState(false); - const errorApi = useApi(errorApiRef); - const featureFlagApi = useApi(featureFlagsApiRef); - const featureFlagKey = 'backstage:featureFlag'; - const filterOutProperties = (step: Step): Step => { - const filteredStep = cloneDeep(step); - const removedPropertyKeys: Array = []; - if (filteredStep.schema.properties) { - filteredStep.schema.properties = Object.fromEntries( - Object.entries(filteredStep.schema.properties).filter( - ([key, value]) => { - if (value[featureFlagKey]) { - if (featureFlagApi.isActive(value[featureFlagKey])) { - return true; - } - removedPropertyKeys.push(key); - return false; - } - return true; - }, - ), - ); - - // remove the feature flag property key from required if they are not active - filteredStep.schema.required = Array.isArray(filteredStep.schema.required) - ? filteredStep.schema.required?.filter( - r => !removedPropertyKeys.includes(r as string), - ) - : filteredStep.schema.required; - } - return filteredStep; - }; - - const steps = props.steps - .filter(step => { - const featureFlag = step.schema[featureFlagKey]; - return ( - typeof featureFlag !== 'string' || featureFlagApi.isActive(featureFlag) - ); - }) - .map(filterOutProperties); - - const handleReset = () => { - setActiveStep(0); - onReset(); - }; - const handleNext = () => { - const stepNum = Math.min(activeStep + 1, steps.length); - setActiveStep(stepNum); - analytics.captureEvent('click', `Next Step (${stepNum})`); - }; - const handleBack = () => setActiveStep(Math.max(activeStep - 1, 0)); - const handleCreate = async () => { - if (!onFinish) { - return; - } - - setDisableButtons(true); - try { - await onFinish(); - analytics.captureEvent('create', formData.name || `new ${templateName}`); - } catch (err) { - errorApi.post(err); - } finally { - setDisableButtons(false); - } - }; - - const ReviewStepElement = ReviewStepComponent ?? ReviewStep; - - return ( - <> - - {steps.map(({ title, schema, ...formProps }, index) => { - return ( - - - - {title} - - - -
) => { - if (e.errors.length === 0) handleNext(); - }} - experimental_defaultFormStateBehavior={{ - allOf: 'populateDefaults', - }} - {...formProps} - {...transformSchemaToProps(schema, layouts)} - > - - -
-
-
- ); - })} -
- {activeStep === steps.length && ( - - )} - - ); -}; diff --git a/plugins/scaffolder/src/legacy/MultistepJsonForm/ReviewStep.tsx b/plugins/scaffolder/src/legacy/MultistepJsonForm/ReviewStep.tsx deleted file mode 100644 index c3b244e921..0000000000 --- a/plugins/scaffolder/src/legacy/MultistepJsonForm/ReviewStep.tsx +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2020 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 { Box, Button, Paper, Typography } from '@material-ui/core'; -import React from 'react'; -import { Content, StructuredMetadataTable } from '@backstage/core-components'; -import { UiSchema } from '@rjsf/utils'; -import { JsonObject } from '@backstage/types'; -import { ReviewStepProps } from '@backstage/plugin-scaffolder-react'; - -export function getReviewData( - formData: Record, - uiSchemas: UiSchema[], -) { - const reviewData: Record = {}; - const orderedReviewProperties = new Set( - uiSchemas.map(us => us.name).concat(Object.getOwnPropertyNames(formData)), - ); - - for (const key of orderedReviewProperties) { - const uiSchema = uiSchemas.find(us => us.name === key); - - if (!uiSchema) { - reviewData[key] = formData[key]; - continue; - } - - if (uiSchema['ui:widget'] === 'password') { - reviewData[key] = '******'; - continue; - } - - if (!uiSchema['ui:backstage'] || !uiSchema['ui:backstage'].review) { - reviewData[key] = formData[key]; - continue; - } - - const review = uiSchema['ui:backstage'].review as JsonObject; - if (review.mask) { - reviewData[key] = review.mask; - continue; - } - - if (!review.show) { - continue; - } - reviewData[key] = formData[key]; - } - - return reviewData; -} - -export function getUiSchemasFromSteps( - steps: { - schema: JsonObject; - }[], -): UiSchema[] { - const uiSchemas: Array = []; - steps.forEach(step => { - const schemaProps = step.schema.properties as JsonObject; - for (const key in schemaProps) { - if (schemaProps.hasOwnProperty(key)) { - const uiSchema = schemaProps[key] as UiSchema; - uiSchema.name = key; - uiSchemas.push(uiSchema); - } - } - }); - return uiSchemas; -} - -/** - * The component displaying the Last Step in scaffolder template form. - * Which represents the summary of the input provided by the end user. - */ -export const ReviewStep = (props: ReviewStepProps) => { - const { - disableButtons, - formData, - handleBack, - handleCreate, - handleReset, - steps, - } = props; - return ( - - - Review and create - ({ schema: mergedSchema })), - ), - )} - /> - - - - - - - ); -}; diff --git a/plugins/scaffolder/src/legacy/MultistepJsonForm/index.ts b/plugins/scaffolder/src/legacy/MultistepJsonForm/index.ts deleted file mode 100644 index b125e7c4c2..0000000000 --- a/plugins/scaffolder/src/legacy/MultistepJsonForm/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2020 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 { MultistepJsonForm } from './MultistepJsonForm'; diff --git a/plugins/scaffolder/src/legacy/MultistepJsonForm/schema.test.ts b/plugins/scaffolder/src/legacy/MultistepJsonForm/schema.test.ts deleted file mode 100644 index af5bb7fac5..0000000000 --- a/plugins/scaffolder/src/legacy/MultistepJsonForm/schema.test.ts +++ /dev/null @@ -1,535 +0,0 @@ -/* - * Copyright 2020 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 { transformSchemaToProps } from './schema'; - -describe('transformSchemaToProps', () => { - it('transforms deep schema', () => { - const inputSchema = { - type: 'object', - 'ui:welp': 'warp', - properties: { - field1: { - type: 'string', - 'ui:derp': 'herp', - }, - field2: { - type: 'object', - properties: { - fieldX: { - type: 'string', - 'ui:derp': 'xerp', - }, - }, - }, - }, - }; - const expectedSchema = { - type: 'object', - properties: { - field1: { - type: 'string', - }, - field2: { - type: 'object', - properties: { - fieldX: { - type: 'string', - }, - }, - }, - }, - }; - const expectedUiSchema = { - 'ui:welp': 'warp', - field1: { - 'ui:derp': 'herp', - }, - field2: { - fieldX: { - 'ui:derp': 'xerp', - }, - }, - }; - - expect(transformSchemaToProps(inputSchema)).toEqual({ - schema: expectedSchema, - uiSchema: expectedUiSchema, - }); - }); - - it('transforms schema with anyOf fields', () => { - const inputSchema = { - type: 'object', - anyOf: [ - { - properties: { - field3: { - type: 'string', - default: 'Value 1', - 'ui:readonly': true, - }, - }, - }, - { - properties: { - field3: { - type: 'string', - default: 'Value 2', - 'ui:readonly': true, - }, - }, - }, - ], - oneOf: [ - { - properties: { - field4: { - type: 'string', - default: 'Value 1', - 'ui:readonly': true, - }, - }, - }, - ], - allOf: [ - { - properties: { - field5: { - type: 'string', - default: 'Value 1', - 'ui:readonly': true, - }, - }, - }, - ], - properties: { - field1: { - type: 'object', - anyOf: [ - { - properties: { - field3: { - type: 'string', - default: 'Value 1', - 'ui:readonly': true, - }, - }, - }, - { - properties: { - field3: { - type: 'string', - default: 'Value 2', - 'ui:readonly': true, - }, - }, - }, - ], - oneOf: [ - { - properties: { - field4: { - type: 'string', - default: 'Value 1', - 'ui:readonly': true, - }, - }, - }, - ], - allOf: [ - { - properties: { - field5: { - type: 'string', - default: 'Value 1', - 'ui:readonly': true, - }, - }, - }, - ], - }, - field2: { - type: 'string', - 'ui:derp': 'xerp', - }, - }, - }; - const expectedSchema = { - type: 'object', - anyOf: [ - { - properties: { - field3: { - type: 'string', - default: 'Value 1', - }, - }, - }, - { - properties: { - field3: { - type: 'string', - default: 'Value 2', - }, - }, - }, - ], - oneOf: [ - { - properties: { - field4: { - type: 'string', - default: 'Value 1', - }, - }, - }, - ], - allOf: [ - { - properties: { - field5: { - type: 'string', - default: 'Value 1', - }, - }, - }, - ], - properties: { - field1: { - type: 'object', - anyOf: [ - { - properties: { - field3: { - type: 'string', - default: 'Value 1', - }, - }, - }, - { - properties: { - field3: { - type: 'string', - default: 'Value 2', - }, - }, - }, - ], - oneOf: [ - { - properties: { - field4: { - type: 'string', - default: 'Value 1', - }, - }, - }, - ], - allOf: [ - { - properties: { - field5: { - type: 'string', - default: 'Value 1', - }, - }, - }, - ], - }, - field2: { - type: 'string', - }, - }, - }; - const expectedUiSchema = { - field3: { - 'ui:readonly': true, - }, - field4: { - 'ui:readonly': true, - }, - field5: { - 'ui:readonly': true, - }, - field1: { - field3: { - 'ui:readonly': true, - }, - field4: { - 'ui:readonly': true, - }, - field5: { - 'ui:readonly': true, - }, - }, - field2: { - 'ui:derp': 'xerp', - }, - }; - - expect(transformSchemaToProps(inputSchema)).toEqual({ - schema: expectedSchema, - uiSchema: expectedUiSchema, - }); - }); - - it('transforms schema with dependencies', () => { - const inputSchema = { - type: 'object', - properties: { - name: { - type: 'string', - }, - credit_card: { - type: 'number', - }, - }, - required: ['name'], - dependencies: { - credit_card: { - properties: { - billing_address: { - type: 'string', - 'ui:widget': 'textarea', - }, - }, - required: ['billing_address'], - }, - }, - }; - const expectedSchema = { - type: 'object', - properties: { - name: { - type: 'string', - }, - credit_card: { - type: 'number', - }, - }, - required: ['name'], - dependencies: { - credit_card: { - properties: { - billing_address: { - type: 'string', - }, - }, - required: ['billing_address'], - }, - }, - }; - const expectedUiSchema = { - billing_address: { - 'ui:widget': 'textarea', - }, - credit_card: {}, - name: {}, - }; - - expect(transformSchemaToProps(inputSchema)).toEqual({ - schema: expectedSchema, - uiSchema: expectedUiSchema, - }); - }); - - it('transforms schema with complex dependencies', () => { - const inputSchema = { - type: 'object', - properties: { - conditional: { - title: 'Person', - type: 'object', - properties: { - 'Do you have any pets?': { - type: 'string', - enum: ['No', 'Yes: One', 'Yes: More than one'], - default: 'No', - 'ui:widget': 'radio', - }, - }, - required: ['Do you have any pets?'], - dependencies: { - 'Do you have any pets?': { - oneOf: [ - { - properties: { - 'Do you have any pets?': { - enum: ['No'], - }, - }, - }, - { - properties: { - 'Do you have any pets?': { - enum: ['Yes: One'], - }, - 'How old is your pet?': { - type: 'number', - }, - }, - required: ['How old is your pet?'], - }, - { - properties: { - 'Do you have any pets?': { - enum: ['Yes: More than one'], - }, - 'Do you want to get rid of any?': { - type: 'boolean', - }, - }, - required: ['Do you want to get rid of any?'], - }, - ], - }, - }, - }, - }, - }; - const expectedSchema = { - type: 'object', - properties: { - conditional: { - title: 'Person', - type: 'object', - properties: { - 'Do you have any pets?': { - type: 'string', - enum: ['No', 'Yes: One', 'Yes: More than one'], - default: 'No', - }, - }, - required: ['Do you have any pets?'], - dependencies: { - 'Do you have any pets?': { - oneOf: [ - { - properties: { - 'Do you have any pets?': { - enum: ['No'], - }, - }, - }, - { - properties: { - 'Do you have any pets?': { - enum: ['Yes: One'], - }, - 'How old is your pet?': { - type: 'number', - }, - }, - required: ['How old is your pet?'], - }, - { - properties: { - 'Do you have any pets?': { - enum: ['Yes: More than one'], - }, - 'Do you want to get rid of any?': { - type: 'boolean', - }, - }, - required: ['Do you want to get rid of any?'], - }, - ], - }, - }, - }, - }, - }; - const expectedUiSchema = { - conditional: { - 'Do you have any pets?': { - 'ui:widget': 'radio', - }, - 'Do you want to get rid of any?': {}, - 'How old is your pet?': {}, - }, - }; - - expect(transformSchemaToProps(inputSchema)).toEqual({ - schema: expectedSchema, - uiSchema: expectedUiSchema, - }); - }); - - it('transforms schema with array items', () => { - const inputSchema = { - type: 'object', - properties: { - person: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - }, - address: { - type: 'string', - 'ui:widget': 'textarea', - }, - }, - }, - }, - accountNumber: { - type: 'number', - }, - }, - }; - const expectedSchema = { - type: 'object', - properties: { - person: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - }, - address: { - type: 'string', - }, - }, - }, - }, - accountNumber: { - type: 'number', - }, - }, - }; - const expectedUiSchema = { - accountNumber: {}, - person: { - items: { - name: {}, - address: { - 'ui:widget': 'textarea', - }, - }, - }, - }; - - expect(transformSchemaToProps(inputSchema)).toEqual({ - schema: expectedSchema, - uiSchema: expectedUiSchema, - }); - }); -}); diff --git a/plugins/scaffolder/src/legacy/MultistepJsonForm/schema.ts b/plugins/scaffolder/src/legacy/MultistepJsonForm/schema.ts deleted file mode 100644 index e6221cb8d5..0000000000 --- a/plugins/scaffolder/src/legacy/MultistepJsonForm/schema.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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 { JsonObject } from '@backstage/types'; -import { UiSchema } from '@rjsf/utils'; -import type { LayoutOptions } from '@backstage/plugin-scaffolder-react'; -import { FormProps } from '@rjsf/core'; - -function isObject(value: unknown): value is JsonObject { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - -function extractUiSchema(schema: JsonObject, uiSchema: JsonObject) { - if (!isObject(schema)) { - return; - } - - const { properties, items, anyOf, oneOf, allOf, dependencies } = schema; - - for (const propName in schema) { - if (!schema.hasOwnProperty(propName)) { - continue; - } - - if (propName.startsWith('ui:')) { - uiSchema[propName] = schema[propName]; - delete schema[propName]; - } - } - - if (isObject(properties)) { - for (const propName in properties) { - if (!properties.hasOwnProperty(propName)) { - continue; - } - - const schemaNode = properties[propName]; - if (!isObject(schemaNode)) { - continue; - } - const innerUiSchema = {}; - uiSchema[propName] = uiSchema[propName] || innerUiSchema; - extractUiSchema(schemaNode, innerUiSchema); - } - } - - if (isObject(items)) { - const innerUiSchema = {}; - uiSchema.items = innerUiSchema; - extractUiSchema(items, innerUiSchema); - } - - if (Array.isArray(anyOf)) { - for (const schemaNode of anyOf) { - if (!isObject(schemaNode)) { - continue; - } - extractUiSchema(schemaNode, uiSchema); - } - } - - if (Array.isArray(oneOf)) { - for (const schemaNode of oneOf) { - if (!isObject(schemaNode)) { - continue; - } - extractUiSchema(schemaNode, uiSchema); - } - } - - if (Array.isArray(allOf)) { - for (const schemaNode of allOf) { - if (!isObject(schemaNode)) { - continue; - } - extractUiSchema(schemaNode, uiSchema); - } - } - - if (isObject(dependencies)) { - for (const depName of Object.keys(dependencies)) { - const schemaNode = dependencies[depName]; - if (!isObject(schemaNode)) { - continue; - } - extractUiSchema(schemaNode, uiSchema); - } - } -} - -export function transformSchemaToProps( - inputSchema: JsonObject, - layouts: LayoutOptions[] = [], -): { - schema: FormProps['schema']; - uiSchema: FormProps['uiSchema']; -} { - const customLayoutName = inputSchema['ui:ObjectFieldTemplate']; - inputSchema.type = inputSchema.type || 'object'; - const schema = JSON.parse(JSON.stringify(inputSchema)); - delete schema.title; // Rendered separately - const uiSchema: UiSchema = {}; - extractUiSchema(schema, uiSchema); - - if (customLayoutName) { - const Layout = layouts.find( - layout => layout.name === customLayoutName, - )?.component; - - if (Layout) { - uiSchema['ui:ObjectFieldTemplate'] = Layout; - } - } - - return { schema, uiSchema }; -} diff --git a/plugins/scaffolder/src/legacy/Router.tsx b/plugins/scaffolder/src/legacy/Router.tsx deleted file mode 100644 index ac183b9470..0000000000 --- a/plugins/scaffolder/src/legacy/Router.tsx +++ /dev/null @@ -1,183 +0,0 @@ -/* - * 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 React, { ComponentType, useEffect, PropsWithChildren } from 'react'; -import { Navigate, Route, Routes, useOutlet } from 'react-router-dom'; -import { Entity } from '@backstage/catalog-model'; -import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { ScaffolderPage } from './ScaffolderPage'; -import { TemplatePage } from './TemplatePage'; -import { TaskPage } from './TaskPage'; -import { ActionsPage } from '../components/ActionsPage'; -import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../extensions/default'; -import { useRouteRef, useRouteRefParams } from '@backstage/core-plugin-api'; -import { LegacyFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha'; -import { - ReviewStepProps, - SecretsContextProvider, - useCustomFieldExtensions, - useCustomLayouts, -} from '@backstage/plugin-scaffolder-react'; -import { ListTasksPage } from '../components/ListTasksPage'; -import { - actionsRouteRef, - editRouteRef, - legacySelectedTemplateRouteRef, - scaffolderListTaskRouteRef, - scaffolderTaskRouteRef, - selectedTemplateRouteRef, -} from '../routes'; -import { TemplateEditorPage } from './TemplateEditorPage'; - -/** - * The props for the entrypoint `ScaffolderPage` component the plugin. - * @alpha - */ -export type LegacyRouterProps = { - components?: { - ReviewStepComponent?: ComponentType; - TemplateCardComponent?: - | ComponentType<{ template: TemplateEntityV1beta3 }> - | undefined; - TaskPageComponent?: ComponentType>; - }; - groups?: Array<{ - title?: React.ReactNode; - filter: (entity: Entity) => boolean; - }>; - templateFilter?: (entity: TemplateEntityV1beta3) => boolean; - defaultPreviewTemplate?: string; - headerOptions?: { - pageTitleOverride?: string; - title?: string; - subtitle?: string; - }; - /** - * Options for the context menu on the scaffolder page. - */ - contextMenu?: { - /** Whether to show a link to the template editor */ - editor?: boolean; - /** Whether to show a link to the actions documentation */ - actions?: boolean; - }; -}; - -/** - * The legacy router - * - * @alpha - */ -export const LegacyRouter = (props: LegacyRouterProps) => { - const { - groups, - templateFilter, - components = {}, - defaultPreviewTemplate, - } = props; - - const { ReviewStepComponent, TemplateCardComponent, TaskPageComponent } = - components; - - const outlet = useOutlet(); - const TaskPageElement = TaskPageComponent ?? TaskPage; - - const customFieldExtensions = - useCustomFieldExtensions(outlet); - - const fieldExtensions = [ - ...customFieldExtensions, - ...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter( - ({ name }) => - !customFieldExtensions.some( - customFieldExtension => customFieldExtension.name === name, - ), - ), - ] as LegacyFieldExtensionOptions[]; - - const customLayouts = useCustomLayouts(outlet); - - /** - * This component can be deleted once the older routes have been deprecated. - */ - const RedirectingComponent = () => { - const { templateName } = useRouteRefParams(legacySelectedTemplateRouteRef); - const newLink = useRouteRef(selectedTemplateRouteRef); - useEffect( - () => - // eslint-disable-next-line no-console - console.warn( - 'The route /template/:templateName is deprecated, please use the new /template/:namespace/:templateName route instead', - ), - [], - ); - return ; - }; - - return ( - - - } - /> - } - /> - - - - } - /> - } - /> - } /> - } /> - - - - } - /> - - } /> - - ); -}; diff --git a/plugins/scaffolder/src/legacy/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/legacy/ScaffolderPage/ScaffolderPage.tsx deleted file mode 100644 index 54122eddf0..0000000000 --- a/plugins/scaffolder/src/legacy/ScaffolderPage/ScaffolderPage.tsx +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2020 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 { - Content, - ContentHeader, - CreateButton, - Header, - Page, - SupportButton, -} from '@backstage/core-components'; -import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { useRouteRef } from '@backstage/core-plugin-api'; -import { - CatalogFilterLayout, - EntityKindPicker, - EntityListProvider, - EntitySearchBar, - EntityTagPicker, - UserListPicker, -} from '@backstage/plugin-catalog-react'; -import React, { ComponentType } from 'react'; -import { TemplateList } from '../TemplateList'; -import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; -import { usePermission } from '@backstage/plugin-permission-react'; -import { ScaffolderPageContextMenu } from './ScaffolderPageContextMenu'; -import { registerComponentRouteRef } from '../../routes'; -import { TemplateTypePicker } from '../../components'; - -export type ScaffolderPageProps = { - TemplateCardComponent?: - | ComponentType<{ template: TemplateEntityV1beta3 }> - | undefined; - groups?: Array<{ - title?: React.ReactNode; - filter: (entity: TemplateEntityV1beta3) => boolean; - }>; - templateFilter?: (entity: TemplateEntityV1beta3) => boolean; - contextMenu?: { - editor?: boolean; - actions?: boolean; - tasks?: boolean; - }; - headerOptions?: { - pageTitleOverride?: string; - title?: string; - subtitle?: string; - }; -}; - -export const ScaffolderPageContents = ({ - TemplateCardComponent, - groups, - templateFilter, - contextMenu, - headerOptions, -}: ScaffolderPageProps) => { - const registerComponentLink = useRouteRef(registerComponentRouteRef); - const otherTemplatesGroup = { - title: groups ? 'Other Templates' : 'Templates', - filter: (entity: TemplateEntityV1beta3) => { - const filtered = (groups ?? []).map(group => group.filter(entity)); - return !filtered.some(result => result === true); - }, - }; - - const { allowed } = usePermission({ - permission: catalogEntityCreatePermission, - }); - - return ( - -
- -
- - - {allowed && ( - - )} - - Create new software components using standard templates. Different - templates create different kinds of components (services, websites, - documentation, ...). - - - - - - - - - {groups && - groups.map((group, index) => ( - - ))} - - - - -
- ); -}; - -export const ScaffolderPage = ({ - TemplateCardComponent, - groups, - templateFilter, - contextMenu, - headerOptions, -}: ScaffolderPageProps) => ( - - - -); diff --git a/plugins/scaffolder/src/legacy/ScaffolderPage/ScaffolderPageContextMenu.test.tsx b/plugins/scaffolder/src/legacy/ScaffolderPage/ScaffolderPageContextMenu.test.tsx deleted file mode 100644 index 66513591d0..0000000000 --- a/plugins/scaffolder/src/legacy/ScaffolderPage/ScaffolderPageContextMenu.test.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2020 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 userEvent from '@testing-library/user-event'; -import { renderInTestApp } from '@backstage/test-utils'; -import { screen } from '@testing-library/react'; -import React from 'react'; -import { ScaffolderPageContextMenu } from './ScaffolderPageContextMenu'; -import { rootRouteRef } from '../../routes'; - -describe('ScaffolderPageContextMenu', () => { - it('does not render anything if fully disabled', async () => { - await renderInTestApp( -
- -
, - { mountedRoutes: { '/': rootRouteRef } }, - ); - - expect(screen.getByTestId('container')).toBeEmptyDOMElement(); - }); - - it('renders the editor option', async () => { - await renderInTestApp( -
- -
, - { - mountedRoutes: { '/': rootRouteRef }, - }, - ); - - await userEvent.click(screen.getByTestId('container').firstElementChild!); - - expect(screen.getByText('Template Editor')).toBeInTheDocument(); - expect(screen.queryByText('Installed Actions')).not.toBeInTheDocument(); - }); - - it('renders the actions option', async () => { - await renderInTestApp( -
- -
, - { - mountedRoutes: { '/': rootRouteRef }, - }, - ); - - await userEvent.click(screen.getByTestId('container').firstElementChild!); - - expect(screen.queryByText('Template Editor')).not.toBeInTheDocument(); - expect(screen.getByText('Installed Actions')).toBeInTheDocument(); - }); - - it('renders all options', async () => { - await renderInTestApp( -
- -
, - { - mountedRoutes: { '/': rootRouteRef }, - }, - ); - - await userEvent.click(screen.getByTestId('container').firstElementChild!); - - expect(screen.getByText('Template Editor')).toBeInTheDocument(); - expect(screen.getByText('Installed Actions')).toBeInTheDocument(); - }); -}); diff --git a/plugins/scaffolder/src/legacy/ScaffolderPage/ScaffolderPageContextMenu.tsx b/plugins/scaffolder/src/legacy/ScaffolderPage/ScaffolderPageContextMenu.tsx deleted file mode 100644 index f6151c227b..0000000000 --- a/plugins/scaffolder/src/legacy/ScaffolderPage/ScaffolderPageContextMenu.tsx +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2020 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 { useRouteRef } from '@backstage/core-plugin-api'; -import IconButton from '@material-ui/core/IconButton'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import ListItemText from '@material-ui/core/ListItemText'; -import MenuItem from '@material-ui/core/MenuItem'; -import MenuList from '@material-ui/core/MenuList'; -import Popover from '@material-ui/core/Popover'; -import { makeStyles } from '@material-ui/core/styles'; -import Description from '@material-ui/icons/Description'; -import Edit from '@material-ui/icons/Edit'; -import List from '@material-ui/icons/List'; -import MoreVert from '@material-ui/icons/MoreVert'; -import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - actionsRouteRef, - editRouteRef, - scaffolderListTaskRouteRef, -} from '../../routes'; - -const useStyles = makeStyles(theme => ({ - button: { - color: theme.page.fontColor, - }, -})); - -export type ScaffolderPageContextMenuProps = { - editor?: boolean; - actions?: boolean; - tasks?: boolean; -}; - -export function ScaffolderPageContextMenu( - props: ScaffolderPageContextMenuProps, -) { - const classes = useStyles(); - const [anchorEl, setAnchorEl] = useState(); - const editLink = useRouteRef(editRouteRef); - const actionsLink = useRouteRef(actionsRouteRef); - const tasksLink = useRouteRef(scaffolderListTaskRouteRef); - - const navigate = useNavigate(); - - const showEditor = props.editor !== false; - const showActions = props.actions !== false; - const showTasks = props.tasks !== false; - - if (!showEditor && !showActions) { - return null; - } - - const onOpen = (event: React.SyntheticEvent) => { - setAnchorEl(event.currentTarget); - }; - - const onClose = () => { - setAnchorEl(undefined); - }; - - return ( - <> - - - - - - {showEditor && ( - navigate(editLink())}> - - - - - - )} - {showActions && ( - navigate(actionsLink())}> - - - - - - )} - {showTasks && ( - navigate(tasksLink())}> - - - - - - )} - - - - ); -} diff --git a/plugins/scaffolder/src/legacy/ScaffolderPage/index.ts b/plugins/scaffolder/src/legacy/ScaffolderPage/index.ts deleted file mode 100644 index 433e39723f..0000000000 --- a/plugins/scaffolder/src/legacy/ScaffolderPage/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2020 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 { ScaffolderPage } from './ScaffolderPage'; diff --git a/plugins/scaffolder/src/legacy/TaskPage/IconLink.test.tsx b/plugins/scaffolder/src/legacy/TaskPage/IconLink.test.tsx deleted file mode 100644 index 0b6092c37d..0000000000 --- a/plugins/scaffolder/src/legacy/TaskPage/IconLink.test.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2020 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 { renderInTestApp } from '@backstage/test-utils'; -import CloudIcon from '@material-ui/icons/Cloud'; -import React from 'react'; -import { IconLink } from './IconLink'; - -describe('IconLink', () => { - it('should render an icon link', async () => { - const rendered = await renderInTestApp( - , - ); - - expect(rendered.getByText('I am Link')).toBeInTheDocument(); - }); -}); diff --git a/plugins/scaffolder/src/legacy/TaskPage/TaskErrors.tsx b/plugins/scaffolder/src/legacy/TaskPage/TaskErrors.tsx deleted file mode 100644 index 4f8ce974c8..0000000000 --- a/plugins/scaffolder/src/legacy/TaskPage/TaskErrors.tsx +++ /dev/null @@ -1,39 +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. - */ -import { Box } from '@material-ui/core'; -import React, { useEffect, useRef } from 'react'; -import { DismissableBanner } from '@backstage/core-components'; - -type TaskErrorsProps = { - error?: Error; -}; - -export const TaskErrors = ({ error }: TaskErrorsProps) => { - const id = useRef(''); - - useEffect(() => { - id.current = String(Math.random()); - }, [error]); - return error ? ( - - - - ) : null; -}; diff --git a/plugins/scaffolder/src/legacy/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/legacy/TaskPage/TaskPage.tsx deleted file mode 100644 index affb9a31b7..0000000000 --- a/plugins/scaffolder/src/legacy/TaskPage/TaskPage.tsx +++ /dev/null @@ -1,393 +0,0 @@ -/* - * 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 { parseEntityRef } from '@backstage/catalog-model'; -import { - Content, - ErrorPage, - Header, - LogViewer, - Page, - Progress, -} from '@backstage/core-components'; -import { - useApi, - useRouteRef, - useRouteRefParams, -} from '@backstage/core-plugin-api'; -import { - Button, - CircularProgress, - Paper, - StepButton, - StepIconProps, -} from '@material-ui/core'; -import Grid from '@material-ui/core/Grid'; -import Step from '@material-ui/core/Step'; -import StepLabel from '@material-ui/core/StepLabel'; -import Stepper from '@material-ui/core/Stepper'; -import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; -import Cancel from '@material-ui/icons/Cancel'; -import Check from '@material-ui/icons/Check'; -import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; -import classNames from 'classnames'; -import { DateTime, Interval } from 'luxon'; -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, - useTaskEventStream, -} from '@backstage/plugin-scaffolder-react'; -import { TaskErrors } from './TaskErrors'; -import { TaskPageLinks } from './TaskPageLinks'; -import { - rootRouteRef, - scaffolderTaskRouteRef, - selectedTemplateRouteRef, -} from '../../routes'; -import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; -import humanizeDuration from 'humanize-duration'; - -const useStyles = makeStyles((theme: Theme) => - createStyles({ - root: { - width: '100%', - }, - button: { - marginBottom: theme.spacing(2), - marginLeft: theme.spacing(2), - }, - actionsContainer: { - marginBottom: theme.spacing(2), - }, - resetContainer: { - padding: theme.spacing(3), - }, - labelWrapper: { - display: 'flex', - flex: 1, - flexDirection: 'row', - justifyContent: 'space-between', - }, - stepWrapper: { - width: '100%', - }, - }), -); - -type TaskStep = { - id: string; - name: string; - status: ScaffolderTaskStatus; - startedAt?: string; - endedAt?: string; -}; - -const StepTimeTicker = ({ step }: { step: TaskStep }) => { - const [time, setTime] = useState(''); - - useInterval(() => { - if (!step.startedAt) { - setTime(''); - return; - } - - const end = step.endedAt - ? DateTime.fromISO(step.endedAt) - : DateTime.local(); - - const startedAt = DateTime.fromISO(step.startedAt); - const formatted = Interval.fromDateTimes(startedAt, end) - .toDuration() - .valueOf(); - - setTime(humanizeDuration(formatted, { round: true })); - }, 1000); - - return {time}; -}; - -const useStepIconStyles = makeStyles(theme => - createStyles({ - root: { - color: theme.palette.text.disabled, - display: 'flex', - height: 22, - alignItems: 'center', - }, - completed: { - color: theme.palette.status.ok, - }, - error: { - color: theme.palette.status.error, - }, - }), -); - -function TaskStepIconComponent(props: StepIconProps) { - const classes = useStepIconStyles(); - const { active, completed, error } = props; - - const getMiddle = () => { - if (active) { - return ; - } - if (completed) { - return ; - } - if (error) { - return ; - } - return ; - }; - - return ( -
- {getMiddle()} -
- ); -} - -export const TaskStatusStepper = memo( - (props: { - steps: TaskStep[]; - currentStepId: string | undefined; - onUserStepChange: (id: string) => void; - classes?: { - root?: string; - }; - }) => { - const { steps, currentStepId, onUserStepChange } = props; - const classes = useStyles(props); - - return ( -
- s.id === currentStepId)} - orientation="vertical" - nonLinear - > - {steps.map((step, index) => { - const isCancelled = step.status === 'cancelled'; - const isActive = step.status === 'processing'; - const isCompleted = step.status === 'completed'; - const isFailed = step.status === 'failed'; - const isSkipped = step.status === 'skipped'; - - return ( - - onUserStepChange(step.id)}> - -
- {step.name} - {isSkipped ? ( - Skipped - ) : ( - - )} -
-
-
-
- ); - })} -
-
- ); - }, -); - -const hasLinks = ({ links = [] }: ScaffolderTaskOutput): boolean => - links.length > 0; - -/** - * TaskPageProps for constructing a TaskPage - * @param loadingText - Optional loading text shown before a task begins executing. - * @public - * @deprecated - this is a useless type that is no longer used. - */ -export type TaskPageProps = { - loadingText?: string; -}; - -/** - * TaskPage for showing the status of the taskId provided as a param - * - * @alpha - */ -export const TaskPage = (props: TaskPageProps) => { - const { loadingText } = props; - const classes = useStyles(); - const navigate = useNavigate(); - const rootPath = useRouteRef(rootRouteRef); - const scaffolderApi = useApi(scaffolderApiRef); - const templateRoute = useRouteRef(selectedTemplateRouteRef); - const [userSelectedStepId, setUserSelectedStepId] = useState< - string | undefined - >(undefined); - const [clickedToCancel, setClickedToCancel] = useState(false); - const [lastActiveStepId, setLastActiveStepId] = useState( - undefined, - ); - const { taskId } = useRouteRefParams(scaffolderTaskRouteRef); - const taskStream = useTaskEventStream(taskId); - const completed = taskStream.completed; - const taskCancelled = taskStream.cancelled; - const steps = useMemo( - () => - taskStream.task?.spec.steps.map(step => ({ - ...step, - ...taskStream?.steps?.[step.id], - })) ?? [], - [taskStream], - ); - - useEffect(() => { - const mostRecentFailedOrActiveStep = steps.find(step => - ['failed', 'processing'].includes(step.status), - ); - if (completed && !mostRecentFailedOrActiveStep) { - setLastActiveStepId(steps[steps.length - 1]?.id); - return; - } - - setLastActiveStepId(mostRecentFailedOrActiveStep?.id); - }, [steps, completed]); - - const currentStepId = userSelectedStepId ?? lastActiveStepId; - - const logAsString = useMemo(() => { - if (!currentStepId) { - return loadingText ? loadingText : 'Loading...'; - } - const log = taskStream.stepLogs[currentStepId]; - - if (!log?.length) { - return 'Waiting for logs...'; - } - return log.join('\n'); - }, [taskStream.stepLogs, currentStepId, loadingText]); - - const taskNotFound = - taskStream.completed && !taskStream.loading && !taskStream.task; - - const { output } = taskStream; - - const handleStartOver = () => { - if (!taskStream.task || !taskStream.task?.spec.templateInfo?.entityRef) { - navigate(rootPath()); - return; - } - - const formData = taskStream.task!.spec.parameters; - - const { name, namespace } = parseEntityRef( - taskStream.task!.spec.templateInfo?.entityRef, - ); - - navigate( - `${templateRoute({ templateName: name, namespace })}?${qs.stringify({ - formData: JSON.stringify(formData), - })}`, - ); - }; - - const handleCancel = async () => { - setClickedToCancel(true); - await scaffolderApi.cancelTask(taskId); - }; - - return ( - -
- - {taskNotFound ? ( - - ) : ( -
- - - - - {output && hasLinks(output) && ( - - )} - - - - - - {!currentStepId && } - -
- - -
-
-
-
- )} -
- - ); -}; diff --git a/plugins/scaffolder/src/legacy/TaskPage/TaskPageLinks.test.tsx b/plugins/scaffolder/src/legacy/TaskPage/TaskPageLinks.test.tsx deleted file mode 100644 index 2f5d015f8d..0000000000 --- a/plugins/scaffolder/src/legacy/TaskPage/TaskPageLinks.test.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2020 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 { entityRouteRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; -import React from 'react'; -import { TaskPageLinks } from './TaskPageLinks'; - -describe('TaskPageLinks', () => { - beforeEach(() => {}); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('renders further links', async () => { - const output = { - links: [ - { url: 'https://first.url', title: 'Cool link 1' }, - { url: 'https://second.url', title: 'Cool link 2' }, - { entityRef: 'Component:default/my-app', title: 'Open in catalog' }, - { title: 'Skipped' }, - ], - }; - const { findByText, queryByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name/*': entityRouteRef, - }, - }, - ); - - let element = await findByText('Cool link 1'); - - expect(element).toBeInTheDocument(); - expect(element).toHaveAttribute('href', 'https://first.url'); - - element = await findByText('Cool link 2'); - - expect(element).toBeInTheDocument(); - expect(element).toHaveAttribute('href', 'https://second.url'); - - element = await findByText('Open in catalog'); - - expect(element).toBeInTheDocument(); - expect(element).toHaveAttribute( - 'href', - '/catalog/default/Component/my-app', - ); - - expect(queryByText('Skipped')).not.toBeInTheDocument(); - }); -}); diff --git a/plugins/scaffolder/src/legacy/TaskPage/index.ts b/plugins/scaffolder/src/legacy/TaskPage/index.ts deleted file mode 100644 index f7876888f9..0000000000 --- a/plugins/scaffolder/src/legacy/TaskPage/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ -export { TaskPage } from './TaskPage'; -export type { TaskPageProps } from './TaskPage'; diff --git a/plugins/scaffolder/src/legacy/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/legacy/TemplateCard/TemplateCard.tsx deleted file mode 100644 index 80f719bd36..0000000000 --- a/plugins/scaffolder/src/legacy/TemplateCard/TemplateCard.tsx +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Copyright 2020 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 { - DEFAULT_NAMESPACE, - Entity, - EntityLink, - parseEntityRef, - RELATION_OWNED_BY, - stringifyEntityRef, -} from '@backstage/catalog-model'; -import { - ItemCardHeader, - Link, - LinkButton, - MarkdownContent, -} from '@backstage/core-components'; -import { - IconComponent, - useApi, - useApp, - useRouteRef, -} from '@backstage/core-plugin-api'; -import { - ScmIntegrationIcon, - scmIntegrationsApiRef, -} from '@backstage/integration-react'; -import { - EntityRefLinks, - FavoriteEntity, - getEntityRelations, - getEntitySourceLocation, -} from '@backstage/plugin-catalog-react'; -import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { - Box, - Card, - CardActions, - CardContent, - CardMedia, - Chip, - IconButton, - makeStyles, - Theme, - Tooltip, - Typography, - useTheme, -} from '@material-ui/core'; -import LanguageIcon from '@material-ui/icons/Language'; -import WarningIcon from '@material-ui/icons/Warning'; -import React from 'react'; -import { selectedTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; - -const useStyles = makeStyles< - Theme, - { fontColor: string; backgroundImage: string } ->(theme => ({ - cardHeader: { - position: 'relative', - }, - title: { - backgroundImage: ({ backgroundImage }) => backgroundImage, - color: ({ fontColor }) => fontColor, - }, - box: { - overflow: 'hidden', - textOverflow: 'ellipsis', - display: '-webkit-box', - '-webkit-line-clamp': 10, - '-webkit-box-orient': 'vertical', - }, - label: { - color: theme.palette.text.secondary, - textTransform: 'uppercase', - fontSize: '0.65rem', - fontWeight: 'bold', - letterSpacing: 0.5, - lineHeight: 1, - paddingBottom: '0.2rem', - }, - linksLabel: { - padding: '0 16px', - }, - description: { - '& p': { - margin: '0px', - }, - }, - leftButton: { - marginRight: 'auto', - }, - starButton: { - position: 'absolute', - top: theme.spacing(0.5), - right: theme.spacing(0.5), - padding: '0.25rem', - color: ({ fontColor }) => fontColor, - }, -})); - -const MuiIcon = ({ icon: Icon }: { icon: IconComponent }) => ; - -const useDeprecationStyles = makeStyles(theme => ({ - deprecationIcon: { - position: 'absolute', - top: theme.spacing(0.5), - right: theme.spacing(3.5), - padding: '0.25rem', - }, - link: { - color: theme.palette.warning.light, - }, -})); - -export type TemplateCardProps = { - template: TemplateEntityV1beta3; - deprecated?: boolean; -}; - -type TemplateProps = { - description: string; - tags: string[]; - title: string; - type: string; - name: string; - links: EntityLink[]; -}; - -const getTemplateCardProps = ( - template: TemplateEntityV1beta3, -): TemplateProps & { key: string } => { - return { - key: template.metadata.uid!, - name: template.metadata.name, - title: `${(template.metadata.title || template.metadata.name) ?? ''}`, - type: template.spec.type ?? '', - description: template.metadata.description ?? '-', - tags: (template.metadata?.tags as string[]) ?? [], - links: template.metadata.links ?? [], - }; -}; - -const DeprecationWarning = () => { - const styles = useDeprecationStyles(); - - const Title = ( - - This template uses a syntax that has been deprecated, and should be - migrated to a newer syntax. Click for more info. - - ); - - return ( -
- - - - - -
- ); -}; - -export const TemplateCard = ({ template, deprecated }: TemplateCardProps) => { - const app = useApp(); - const backstageTheme = useTheme(); - const templateRoute = useRouteRef(selectedTemplateRouteRef); - const templateProps = getTemplateCardProps(template); - const ownedByRelations = getEntityRelations( - template as Entity, - RELATION_OWNED_BY, - ); - const themeId = backstageTheme.getPageTheme({ themeId: templateProps.type }) - ? templateProps.type - : 'other'; - const theme = backstageTheme.getPageTheme({ themeId }); - const classes = useStyles({ - fontColor: theme.fontColor, - backgroundImage: theme.backgroundImage, - }); - const { name, namespace } = parseEntityRef(stringifyEntityRef(template)); - const href = templateRoute({ templateName: name, namespace }); - - // 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, - }); - - const iconResolver = (key?: string): IconComponent => - key ? app.getSystemIcon(key) ?? LanguageIcon : LanguageIcon; - - const scmIntegrationsApi = useApi(scmIntegrationsApiRef); - const sourceLocation = getEntitySourceLocation(template, scmIntegrationsApi); - - return ( - - - - {deprecated && } - - - - - - Description - - - - - - Owner - - - - - - Tags - - {templateProps.tags?.map(tag => ( - - ))} - - - - Links - - -
- {sourceLocation && ( - - - - - - )} - {viewTechDocsLink && ( - - - - - - )} - {templateProps.links?.map((link, i) => ( - - - - - - ))} -
- - Choose - -
-
- ); -}; diff --git a/plugins/scaffolder/src/legacy/TemplateCard/index.ts b/plugins/scaffolder/src/legacy/TemplateCard/index.ts deleted file mode 100644 index 291a06ed69..0000000000 --- a/plugins/scaffolder/src/legacy/TemplateCard/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 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 } from './TemplateCard'; -export type { TemplateCardProps } from './TemplateCard'; diff --git a/plugins/scaffolder/src/legacy/TemplateEditorPage/CustomFieldExplorer.tsx b/plugins/scaffolder/src/legacy/TemplateEditorPage/CustomFieldExplorer.tsx deleted file mode 100644 index def3bf1796..0000000000 --- a/plugins/scaffolder/src/legacy/TemplateEditorPage/CustomFieldExplorer.tsx +++ /dev/null @@ -1,205 +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. - */ -import { StreamLanguage } from '@codemirror/language'; -import { yaml as yamlSupport } from '@codemirror/legacy-modes/mode/yaml'; -import { - Button, - Card, - CardContent, - CardHeader, - FormControl, - IconButton, - InputLabel, - makeStyles, - MenuItem, - Select, -} from '@material-ui/core'; -import CloseIcon from '@material-ui/icons/Close'; -import CodeMirror from '@uiw/react-codemirror'; -import React, { useCallback, useMemo, useState } from 'react'; -import yaml from 'yaml'; -import { Form } from '@backstage/plugin-scaffolder-react/alpha'; -import { TemplateEditorForm } from './TemplateEditorForm'; -import validator from '@rjsf/validator-ajv8'; -import { LegacyFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha'; - -const useStyles = makeStyles(theme => ({ - root: { - gridArea: 'pageContent', - display: 'grid', - gridTemplateAreas: ` - "controls controls" - "fieldForm preview" - `, - gridTemplateRows: 'auto 1fr', - gridTemplateColumns: '1fr 1fr', - }, - controls: { - gridArea: 'controls', - display: 'flex', - flexFlow: 'row nowrap', - alignItems: 'center', - margin: theme.spacing(1), - }, - fieldForm: { - gridArea: 'fieldForm', - }, - preview: { - gridArea: 'preview', - }, -})); - -export const CustomFieldExplorer = ({ - customFieldExtensions = [], - onClose, -}: { - customFieldExtensions?: LegacyFieldExtensionOptions[]; - onClose?: () => void; -}) => { - const classes = useStyles(); - const fieldOptions = customFieldExtensions.filter(field => !!field.schema); - const [selectedField, setSelectedField] = useState(fieldOptions[0]); - const [fieldFormState, setFieldFormState] = useState({}); - const [refreshKey, setRefreshKey] = useState(Date.now()); - const [formState, setFormState] = useState({}); - const sampleFieldTemplate = useMemo( - () => - yaml.stringify({ - parameters: [ - { - title: `${selectedField.name} Example`, - properties: { - [selectedField.name]: { - type: selectedField.schema?.returnValue?.type, - 'ui:field': selectedField.name, - 'ui:options': fieldFormState, - }, - }, - }, - ], - }), - [fieldFormState, selectedField], - ); - - const fieldComponents = useMemo(() => { - return Object.fromEntries( - customFieldExtensions.map(({ name, component }) => [name, component]), - ); - }, [customFieldExtensions]); - - const handleSelectionChange = useCallback( - (selection: LegacyFieldExtensionOptions) => { - setSelectedField(selection); - setFieldFormState({}); - setFormState({}); - }, - [setFieldFormState, setSelectedField, setFormState], - ); - - const handleFieldConfigChange = useCallback( - (state: {}) => { - setFieldFormState(state); - // Force TemplateEditorForm to re-render since some fields - // may not be responsive to ui:option changes - setRefreshKey(Date.now()); - }, - [setFieldFormState, setRefreshKey], - ); - - return ( -
-
- - - Choose Custom Field Extension - - - - - - - -
-
- - - -
handleFieldConfigChange(e.formData)} - validator={validator} - schema={selectedField.schema?.uiOptions || {}} - experimental_defaultFormStateBehavior={{ - allOf: 'populateDefaults', - }} - > - -
-
-
-
-
- - - - - - - null} - /> -
-
- ); -}; diff --git a/plugins/scaffolder/src/legacy/TemplateEditorPage/TemplateEditor.tsx b/plugins/scaffolder/src/legacy/TemplateEditorPage/TemplateEditor.tsx deleted file mode 100644 index f6e7b38d7d..0000000000 --- a/plugins/scaffolder/src/legacy/TemplateEditorPage/TemplateEditor.tsx +++ /dev/null @@ -1,92 +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. - */ -import { makeStyles } from '@material-ui/core'; -import React, { useState } from 'react'; -import { LegacyFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha'; -import { TemplateDirectoryAccess } from '../../lib/filesystem'; -import { LayoutOptions } from '@backstage/plugin-scaffolder-react'; -import { DirectoryEditorProvider } from '../../next/TemplateEditorPage/DirectoryEditorContext'; -import { DryRunProvider } from '../../next/TemplateEditorPage/DryRunContext'; -import { TemplateEditorBrowser } from '../../next/TemplateEditorPage/TemplateEditorBrowser'; -import { TemplateEditorTextArea } from '../../next/TemplateEditorPage/TemplateEditorTextArea'; -import { TemplateEditorForm } from './TemplateEditorForm'; -import { DryRunResults } from '../../next/TemplateEditorPage/DryRunResults'; - -const useStyles = makeStyles({ - // Reset and fix sizing to make sure scrolling behaves correctly - root: { - gridArea: 'pageContent', - - display: 'grid', - gridTemplateAreas: ` - "browser editor preview" - "results results results" - `, - gridTemplateColumns: '1fr 3fr 2fr', - gridTemplateRows: '1fr auto', - }, - browser: { - gridArea: 'browser', - overflow: 'auto', - }, - editor: { - gridArea: 'editor', - overflow: 'auto', - }, - preview: { - gridArea: 'preview', - overflow: 'auto', - }, - results: { - gridArea: 'results', - }, -}); - -export const TemplateEditor = (props: { - directory: TemplateDirectoryAccess; - fieldExtensions?: LegacyFieldExtensionOptions[]; - layouts?: LayoutOptions[]; - onClose?: () => void; -}) => { - const classes = useStyles(); - - const [errorText, setErrorText] = useState(); - - return ( - - -
-
- -
-
- -
-
- -
-
- -
-
-
-
- ); -}; diff --git a/plugins/scaffolder/src/legacy/TemplateEditorPage/TemplateEditorForm.tsx b/plugins/scaffolder/src/legacy/TemplateEditorPage/TemplateEditorForm.tsx deleted file mode 100644 index df05efc921..0000000000 --- a/plugins/scaffolder/src/legacy/TemplateEditorPage/TemplateEditorForm.tsx +++ /dev/null @@ -1,261 +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. - */ -import { useApiHolder } from '@backstage/core-plugin-api'; -import { JsonObject, JsonValue } from '@backstage/types'; -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 { - LayoutOptions, - TemplateParameterSchema, -} from '@backstage/plugin-scaffolder-react'; - -import { useDryRun } from '../../next/TemplateEditorPage/DryRunContext'; -import { useDirectoryEditor } from '../../next/TemplateEditorPage/DirectoryEditorContext'; -import { MultistepJsonForm } from '../MultistepJsonForm'; -import { createValidator } from '../TemplatePage'; -import { LegacyFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha'; - -const useStyles = makeStyles({ - containerWrapper: { - position: 'relative', - width: '100%', - height: '100%', - }, - container: { - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, - overflow: 'auto', - }, -}); - -interface ErrorBoundaryProps { - invalidator: unknown; - setErrorText(errorText: string | undefined): void; - children: ReactNode; -} - -interface ErrorBoundaryState { - shouldRender: boolean; -} - -class ErrorBoundary extends Component { - state = { - shouldRender: true, - }; - - componentDidUpdate(prevProps: { invalidator: unknown }) { - if (prevProps.invalidator !== this.props.invalidator) { - this.setState({ shouldRender: true }); - } - } - - componentDidCatch(error: Error) { - this.props.setErrorText(error.message); - this.setState({ shouldRender: false }); - } - - render() { - return this.state.shouldRender ? this.props.children : null; - } -} - -interface TemplateEditorFormProps { - content?: string; - /** Setting this to true will cause the content to be parsed as if it is the template entity spec */ - contentIsSpec?: boolean; - data: JsonObject; - onUpdate: (data: JsonObject) => void; - setErrorText: (errorText?: string) => void; - - onDryRun?: (data: JsonObject) => Promise; - fieldExtensions?: LegacyFieldExtensionOptions[]; - layouts?: LayoutOptions[]; -} - -function isJsonObject(value: JsonValue | undefined): value is JsonObject { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - -/** Shows the a template form that is parsed from the provided content */ -export function TemplateEditorForm(props: TemplateEditorFormProps) { - const { - content, - contentIsSpec, - data, - onUpdate, - onDryRun, - setErrorText, - fieldExtensions = [], - layouts = [], - } = props; - const classes = useStyles(); - const apiHolder = useApiHolder(); - - const [steps, setSteps] = useState(); - - const fields = useMemo(() => { - return Object.fromEntries( - fieldExtensions.map(({ name, component }) => [name, component]), - ); - }, [fieldExtensions]); - - useDebounce( - () => { - try { - if (!content) { - setSteps(undefined); - return; - } - const parsed: JsonValue = yaml - .parseAllDocuments(content) - .filter(c => c) - .map(c => c.toJSON())[0]; - - if (!isJsonObject(parsed)) { - setSteps(undefined); - return; - } - - let rootObj = parsed; - if (!contentIsSpec) { - const isTemplate = - String(parsed.kind).toLocaleLowerCase('en-US') === 'template'; - if (!isTemplate) { - setSteps(undefined); - return; - } - - rootObj = isJsonObject(parsed.spec) ? parsed.spec : {}; - } - - const { parameters } = rootObj; - - if (!Array.isArray(parameters)) { - setErrorText('Template parameters must be an array'); - setSteps(undefined); - return; - } - - const fieldValidators = Object.fromEntries( - fieldExtensions.map(({ name, validation }) => [name, validation]), - ); - - setErrorText(); - setSteps( - parameters.flatMap(param => - isJsonObject(param) - ? [ - { - title: String(param.title), - schema: param, - validate: createValidator(param, fieldValidators, { - apiHolder, - }), - }, - ] - : [], - ), - ); - } catch (e) { - setErrorText(e.message); - } - }, - 250, - [contentIsSpec, content, apiHolder], - ); - - if (!steps) { - return null; - } - - return ( -
-
- - onUpdate(e.formData)} - onReset={() => onUpdate({})} - finishButtonLabel={onDryRun && 'Try It'} - onFinish={onDryRun && (() => onDryRun(data))} - layouts={layouts} - /> - -
-
- ); -} - -/** A version of the TemplateEditorForm that is connected to the DirectoryEditor and DryRun contexts */ -export function TemplateEditorFormDirectoryEditorDryRun( - props: Pick< - TemplateEditorFormProps, - 'setErrorText' | 'fieldExtensions' | 'layouts' - >, -) { - const { setErrorText, fieldExtensions = [], layouts } = props; - const dryRun = useDryRun(); - - const directoryEditor = useDirectoryEditor(); - const { selectedFile } = directoryEditor; - - const [data, setData] = useState({}); - - const handleDryRun = async () => { - if (!selectedFile) { - return; - } - - try { - await dryRun.execute({ - templateContent: selectedFile.content, - values: data, - files: directoryEditor.files, - }); - setErrorText(); - } catch (e) { - setErrorText(String(e.cause || e)); - throw e; - } - }; - - const content = - selectedFile && selectedFile.path.match(/\.ya?ml$/) - ? selectedFile.content - : undefined; - - return ( - - ); -} - -TemplateEditorForm.DirectoryEditorDryRun = - TemplateEditorFormDirectoryEditorDryRun; diff --git a/plugins/scaffolder/src/legacy/TemplateEditorPage/TemplateEditorPage.tsx b/plugins/scaffolder/src/legacy/TemplateEditorPage/TemplateEditorPage.tsx deleted file mode 100644 index 127c73dcf5..0000000000 --- a/plugins/scaffolder/src/legacy/TemplateEditorPage/TemplateEditorPage.tsx +++ /dev/null @@ -1,105 +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. - */ -import React, { useState } from 'react'; -import { Content, Header, Page } from '@backstage/core-components'; -import { - TemplateDirectoryAccess, - WebFileSystemAccess, -} from '../../lib/filesystem'; -import { type LayoutOptions } from '@backstage/plugin-scaffolder-react'; -import { LegacyFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha'; -import { TemplateEditorIntro } from '../../next/TemplateEditorPage/TemplateEditorIntro'; -import { TemplateEditor } from './TemplateEditor'; -import { TemplateFormPreviewer } from './TemplateFormPreviewer'; -import { CustomFieldExplorer } from './CustomFieldExplorer'; - -type Selection = - | { - type: 'local'; - directory: TemplateDirectoryAccess; - } - | { - type: 'form'; - } - | { - type: 'field-explorer'; - }; - -interface TemplateEditorPageProps { - defaultPreviewTemplate?: string; - customFieldExtensions?: LegacyFieldExtensionOptions[]; - layouts?: LayoutOptions[]; -} - -export function TemplateEditorPage(props: TemplateEditorPageProps) { - const [selection, setSelection] = useState(); - - let content: JSX.Element | null = null; - if (selection?.type === 'local') { - content = ( - setSelection(undefined)} - layouts={props.layouts} - /> - ); - } else if (selection?.type === 'form') { - content = ( - setSelection(undefined)} - layouts={props.layouts} - /> - ); - } else if (selection?.type === 'field-explorer') { - content = ( - setSelection(undefined)} - /> - ); - } else { - content = ( - - { - if (option === 'local') { - WebFileSystemAccess.requestDirectoryAccess() - .then(directory => setSelection({ type: 'local', directory })) - .catch(() => {}); - } else if (option === 'form') { - setSelection({ type: 'form' }); - } else if (option === 'field-explorer') { - setSelection({ type: 'field-explorer' }); - } - }} - /> - - ); - } - - return ( - -
- {content} - - ); -} diff --git a/plugins/scaffolder/src/legacy/TemplateEditorPage/TemplateFormPreviewer.tsx b/plugins/scaffolder/src/legacy/TemplateEditorPage/TemplateFormPreviewer.tsx deleted file mode 100644 index 9e92d12d93..0000000000 --- a/plugins/scaffolder/src/legacy/TemplateEditorPage/TemplateFormPreviewer.tsx +++ /dev/null @@ -1,221 +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. - */ -import { Entity } from '@backstage/catalog-model'; -import { alertApiRef, useApi } from '@backstage/core-plugin-api'; -import { - catalogApiRef, - humanizeEntityRef, -} from '@backstage/plugin-catalog-react'; -import { - FormControl, - IconButton, - InputLabel, - LinearProgress, - makeStyles, - MenuItem, - Select, -} from '@material-ui/core'; -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 { LayoutOptions } from '@backstage/plugin-scaffolder-react'; -import { LegacyFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha'; -import { TemplateEditorTextArea } from '../../next/TemplateEditorPage/TemplateEditorTextArea'; -import { TemplateEditorForm } from './TemplateEditorForm'; - -const EXAMPLE_TEMPLATE_PARAMS_YAML = `# Edit the template parameters below to see how they will render in the scaffolder form UI -parameters: - - title: Fill in some steps - required: - - name - properties: - name: - title: Name - type: string - description: Unique name of the component - owner: - title: Owner - type: string - description: Owner of the component - ui:field: OwnerPicker - ui:options: - catalogFilter: - kind: Group - - title: Choose a location - required: - - repoUrl - properties: - repoUrl: - title: Repository Location - type: string - ui:field: RepoUrlPicker - ui:options: - allowedHosts: - - github.com -steps: - - id: fetch-base - name: Fetch Base - action: fetch:template - input: - url: ./template - values: - name: \${{parameters.name}} -`; - -type TemplateOption = { - label: string; - value: Entity; -}; - -const useStyles = makeStyles(theme => ({ - root: { - gridArea: 'pageContent', - display: 'grid', - gridTemplateAreas: ` - "controls controls" - "textArea preview" - `, - gridTemplateRows: 'auto 1fr', - gridTemplateColumns: '1fr 1fr', - }, - controls: { - gridArea: 'controls', - display: 'flex', - flexFlow: 'row nowrap', - alignItems: 'center', - margin: theme.spacing(1), - }, - textArea: { - gridArea: 'textArea', - }, - preview: { - gridArea: 'preview', - }, -})); - -export const TemplateFormPreviewer = ({ - defaultPreviewTemplate = EXAMPLE_TEMPLATE_PARAMS_YAML, - customFieldExtensions = [], - onClose, - layouts = [], -}: { - defaultPreviewTemplate?: string; - customFieldExtensions?: LegacyFieldExtensionOptions[]; - onClose?: () => void; - layouts?: LayoutOptions[]; -}) => { - const classes = useStyles(); - const alertApi = useApi(alertApiRef); - const catalogApi = useApi(catalogApiRef); - const [selectedTemplate, setSelectedTemplate] = useState(''); - const [errorText, setErrorText] = useState(); - const [templateOptions, setTemplateOptions] = useState([]); - const [templateYaml, setTemplateYaml] = useState(defaultPreviewTemplate); - const [formState, setFormState] = useState({}); - - const { loading } = useAsync( - () => - catalogApi - .getEntities({ - filter: { kind: 'template' }, - fields: [ - 'kind', - 'metadata.namespace', - 'metadata.name', - 'metadata.title', - 'spec.parameters', - 'spec.steps', - 'spec.output', - ], - }) - .then(({ items }) => - setTemplateOptions( - items.map(template => ({ - label: - template.metadata.title ?? - humanizeEntityRef(template, { defaultKind: 'template' }), - value: template, - })), - ), - ) - .catch(e => - alertApi.post({ - message: `Error loading exisiting templates: ${e.message}`, - severity: 'error', - }), - ), - [catalogApi], - ); - - const handleSelectChange = useCallback( - // TODO(Rugvip): Afaik this should be Entity, but didn't want to make runtime changes while fixing types - (selected: any) => { - setSelectedTemplate(selected); - setTemplateYaml(yaml.stringify(selected.spec)); - }, - [setTemplateYaml], - ); - - return ( - <> - {loading && } -
-
- - - Load Existing Template - - - - - - - -
-
- -
-
- -
-
- - ); -}; diff --git a/plugins/scaffolder/src/legacy/TemplateEditorPage/index.ts b/plugins/scaffolder/src/legacy/TemplateEditorPage/index.ts deleted file mode 100644 index 7de0d3c679..0000000000 --- a/plugins/scaffolder/src/legacy/TemplateEditorPage/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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 { TemplateEditorPage } from './TemplateEditorPage'; diff --git a/plugins/scaffolder/src/legacy/TemplateList/TemplateList.test.tsx b/plugins/scaffolder/src/legacy/TemplateList/TemplateList.test.tsx deleted file mode 100644 index c65613d2f5..0000000000 --- a/plugins/scaffolder/src/legacy/TemplateList/TemplateList.test.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2020 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 { screen } from '@testing-library/react'; -import React from 'react'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; -import { TemplateList } from './TemplateList'; -import { rootRouteRef } from '../../routes'; -import { - ScmIntegrationsApi, - scmIntegrationsApiRef, -} from '@backstage/integration-react'; -import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; - -jest.mock('@backstage/plugin-catalog-react', () => ({ - useEntityList: jest.fn().mockReturnValue({ - loading: false, - entities: [ - { - apiVersion: 'scaffolder.backstage.io/v1beta3', - kind: 'Template', - metadata: { - name: 't1', - }, - spec: {}, - }, - { - apiVersion: 'scaffolder.backstage.io/v1beta3', - kind: 'Template', - metadata: { - name: 't2', - }, - spec: {}, - }, - ], - }), - getEntityRelations: jest.fn().mockImplementation(() => []), - getEntitySourceLocation: jest.fn().mockImplementation(() => ({})), -})); - -describe('TemplateList', () => { - const mockIntegrationsApi: Partial = { - byHost: () => ({ type: 'github' }), - }; - - it('should filter out templates based on provided filter condition', async () => { - const TemplateCardComponent = ({ - template, - }: { - template: TemplateEntityV1beta3; - }) => ( -
{template.metadata.name}
- ); - - await renderInTestApp( - -
- e.metadata.name === 't1'} - TemplateCardComponent={TemplateCardComponent} - /> -
-
, - { mountedRoutes: { '/': rootRouteRef } }, - ); - - expect(() => screen.getByTestId('t2')).toThrow(); - expect(screen.getByTestId('t1')).toBeDefined(); - }); -}); diff --git a/plugins/scaffolder/src/legacy/TemplateList/TemplateList.tsx b/plugins/scaffolder/src/legacy/TemplateList/TemplateList.tsx deleted file mode 100644 index c925cdf6ef..0000000000 --- a/plugins/scaffolder/src/legacy/TemplateList/TemplateList.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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 React, { ComponentType } from 'react'; -import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; -import { - isTemplateEntityV1beta3, - TemplateEntityV1beta3, -} from '@backstage/plugin-scaffolder-common'; -import { - Content, - ContentHeader, - ItemCardGrid, - Link, - Progress, - WarningPanel, -} from '@backstage/core-components'; -import { useEntityList } from '@backstage/plugin-catalog-react'; -import { Typography } from '@material-ui/core'; -import { TemplateCard } from '../TemplateCard'; - -/** - * @internal - */ -export type TemplateListProps = { - TemplateCardComponent?: - | ComponentType<{ template: TemplateEntityV1beta3 }> - | undefined; - group?: { - title?: React.ReactNode; - filter: (entity: TemplateEntityV1beta3) => boolean; - }; - templateFilter?: (entity: TemplateEntityV1beta3) => boolean; -}; - -/** - * @internal - */ -export const TemplateList = ({ - TemplateCardComponent, - group, - templateFilter, -}: TemplateListProps) => { - const { loading, error, entities } = useEntityList(); - const Card = TemplateCardComponent || TemplateCard; - const templateEntities = entities.filter(isTemplateEntityV1beta3); - const maybeFilteredEntities = ( - group ? templateEntities.filter(group.filter) : templateEntities - ).filter(e => (templateFilter ? templateFilter(e) : true)); - - const titleComponent: React.ReactNode = (() => { - if (group && group.title) { - if (typeof group.title === 'string') { - return ; - } - return group.title; - } - - return ; - })(); - - if (group && maybeFilteredEntities.length === 0) { - return null; - } - return ( - <> - {loading && } - - {error && ( - - {error.message} - - )} - - {!error && !loading && !entities.length && ( - - No templates found that match your filter. Learn more about{' '} - - adding templates - - . - - )} - - - {titleComponent} - - {maybeFilteredEntities && - maybeFilteredEntities?.length > 0 && - maybeFilteredEntities.map((template: Entity) => ( - - ))} - - - - ); -}; diff --git a/plugins/scaffolder/src/legacy/TemplateList/index.ts b/plugins/scaffolder/src/legacy/TemplateList/index.ts deleted file mode 100644 index 0b62f9cbb1..0000000000 --- a/plugins/scaffolder/src/legacy/TemplateList/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 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 { TemplateList } from './TemplateList'; -export type { TemplateListProps } from './TemplateList'; diff --git a/plugins/scaffolder/src/legacy/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/legacy/TemplatePage/TemplatePage.test.tsx deleted file mode 100644 index 344929fa43..0000000000 --- a/plugins/scaffolder/src/legacy/TemplatePage/TemplatePage.test.tsx +++ /dev/null @@ -1,366 +0,0 @@ -/* - * Copyright 2020 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 { - MockAnalyticsApi, - renderInTestApp, - TestApiRegistry, -} from '@backstage/test-utils'; -import { act, fireEvent, screen, within } from '@testing-library/react'; -import React from 'react'; -import { Route, Routes } from 'react-router-dom'; -import { - scaffolderApiRef, - ScaffolderApi, - SecretsContextProvider, -} from '@backstage/plugin-scaffolder-react'; -import { TemplatePage } from './TemplatePage'; -import { - featureFlagsApiRef, - FeatureFlagsApi, - analyticsApiRef, -} 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 { - ...(jest.requireActual('react-router-dom') as any), - useParams: () => ({ - templateName: 'test', - }), - }; -}); - -const scaffolderApiMock: jest.Mocked = { - cancelTask: jest.fn(), - scaffold: jest.fn(), - getTemplateParameterSchema: jest.fn(), - getIntegrationsList: jest.fn(), - getTask: jest.fn(), - streamLogs: jest.fn(), - listActions: jest.fn(), - listTasks: jest.fn(), -}; - -const featureFlagsApiMock: jest.Mocked = { - isActive: jest.fn(), - registerFlag: jest.fn(), - getRegisteredFlags: jest.fn(), - save: jest.fn(), -}; - -const errorApiMock = { post: jest.fn(), error$: jest.fn() }; - -const analyticsMock = new MockAnalyticsApi(); - -const schemaMockValue = { - title: 'my-schema', - steps: [ - { - title: 'Fill in some steps', - schema: { - title: 'Fill in some steps', - 'backstage:featureFlag': 'experimental-feature', - properties: { - name: { - title: 'Name', - type: 'string', - 'backstage:featureFlag': 'should-show-some-stuff-first-option', - }, - description: { - title: 'Description', - type: 'string', - description: 'A description for the component', - }, - owner: { - title: 'Owner', - type: 'string', - description: 'Owner of the component', - }, - }, - type: 'object', - }, - }, - { - title: 'Send data', - schema: { - title: 'Send data', - properties: { - user: { - title: 'User', - type: 'string', - }, - }, - type: 'object', - }, - }, - ], -}; - -const apis = TestApiRegistry.from( - [scaffolderApiRef, scaffolderApiMock], - [errorApiRef, errorApiMock], - [featureFlagsApiRef, featureFlagsApiMock], - [analyticsApiRef, analyticsMock], -); - -describe('TemplatePage', () => { - beforeEach(() => jest.resetAllMocks()); - - it('renders correctly', async () => { - scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ - title: 'React SSR Template', - steps: [], - }); - const rendered = await renderInTestApp( - - - - - , - { - mountedRoutes: { - '/create': rootRouteRef, - }, - }, - ); - - expect(rendered.getByText('Create a New Component')).toBeInTheDocument(); - expect(rendered.getByText('React SSR Template')).toBeInTheDocument(); - }); - - it('renders spinner while loading', async () => { - let resolve: Function; - const promise = new Promise(res => { - resolve = res; - }); - scaffolderApiMock.getTemplateParameterSchema.mockReturnValueOnce(promise); - const rendered = await renderInTestApp( - - - - - , - { - mountedRoutes: { - '/create': rootRouteRef, - }, - }, - ); - - expect(rendered.getByText('Create a New Component')).toBeInTheDocument(); - expect(rendered.getByTestId('loading-progress')).toBeInTheDocument(); - - await act(async () => { - resolve!({ - title: 'React SSR Template', - steps: [], - }); - }); - }); - - it('captures expected analytics events', async () => { - scaffolderApiMock.scaffold.mockResolvedValue({ taskId: 'xyz' }); - scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ - title: 'schema-4-analytics', - steps: [ - { - title: 'Fill in some steps', - schema: { - properties: { - name: { - title: 'Name', - type: 'string', - }, - }, - required: ['name'], - }, - }, - ], - }); - const { findByLabelText, findByText } = await renderInTestApp( - - - - - , - { - mountedRoutes: { - '/create': rootRouteRef, - }, - }, - ); - - // Fill out the name field - expect(await findByText('Fill in some steps')).toBeInTheDocument(); - fireEvent.change(await findByLabelText('Name', { exact: false }), { - target: { value: 'expected-name' }, - }); - - // Go to the final page - fireEvent.click(await findByText('Next step')); - expect(await findByText('Reset')).toBeInTheDocument(); - - // Create the software - await act(async () => { - fireEvent.click(await findByText('Create')); - }); - - // The "Next Step" button should have fired an event - expect(analyticsMock.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'Next Step (1)', - context: { entityRef: 'template:default/test' }, - }); - - // And the "Create" button should have fired an event - expect(analyticsMock.getEvents()[1]).toMatchObject({ - action: 'create', - subject: 'expected-name', - context: { entityRef: 'template:default/test' }, - }); - }); - - it('navigates away if no template was loaded', async () => { - scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue( - undefined as any, - ); - - const rendered = await renderInTestApp( - - - - - - } - /> - This is root} /> - - , - { - routeEntries: ['/create'], - mountedRoutes: { '/create': rootRouteRef }, - }, - ); - - expect( - rendered.queryByText('Create a New Component'), - ).not.toBeInTheDocument(); - expect(rendered.getByText('This is root')).toBeInTheDocument(); - }); - - it('display template with oneOf', async () => { - scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ - title: 'my-schema', - steps: [ - { - title: 'Fill in some steps', - schema: { - oneOf: [ - { - title: 'First', - properties: { - name: { - title: 'Name', - type: 'string', - }, - }, - required: ['name'], - }, - { - title: 'Second', - properties: { - something: { - title: 'Something', - type: 'string', - }, - }, - required: ['something'], - }, - ], - }, - }, - ], - }); - - const { findByText, findByLabelText, findAllByRole, findByRole } = - await renderInTestApp( - - - - - , - { - mountedRoutes: { - '/create/actions': rootRouteRef, - }, - }, - ); - - expect(await findByText('Fill in some steps')).toBeInTheDocument(); - - // Fill the first option - fireEvent.change(await findByLabelText('Name', { exact: false }), { - target: { value: 'my-name' }, - }); - - // Switch to second option - fireEvent.mouseDown((await findAllByRole('button'))[0]); - const listbox = within(await findByRole('listbox')); - fireEvent.click(listbox.getByText(/Second/i)); - - // Fill the second option - fireEvent.change(await findByLabelText('Something', { exact: false }), { - target: { value: 'my-something' }, - }); - - // Go to the final page - fireEvent.click(await findByText('Next step')); - expect(await findByText('Reset')).toBeInTheDocument(); - }); - - it('should display a section or property based on a feature flag', async () => { - featureFlagsApiMock.isActive.mockImplementation(flag => { - return flag === 'experimental-feature'; - }); - scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue( - schemaMockValue, - ); - - await renderInTestApp( - - - - - , - { - mountedRoutes: { - '/create/actions': rootRouteRef, - }, - }, - ); - - expect(screen.queryByText('Name')).not.toBeInTheDocument(); - expect(screen.getByText('Description')).toBeInTheDocument(); - expect(screen.getByText('Owner')).toBeInTheDocument(); - expect(screen.getByText('Send data')).toBeInTheDocument(); - }); -}); diff --git a/plugins/scaffolder/src/legacy/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/legacy/TemplatePage/TemplatePage.tsx deleted file mode 100644 index ffb754bae3..0000000000 --- a/plugins/scaffolder/src/legacy/TemplatePage/TemplatePage.tsx +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2020 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 { LinearProgress } from '@material-ui/core'; -import { IChangeEvent } from '@rjsf/core'; -import qs from 'qs'; -import React, { ComponentType, useCallback, useState } from 'react'; -import { Navigate, useNavigate } from 'react-router-dom'; -import useAsync from 'react-use/lib/useAsync'; -import { - type LayoutOptions, - scaffolderApiRef, - useTemplateSecrets, - ReviewStepProps, -} from '@backstage/plugin-scaffolder-react'; -import { MultistepJsonForm } from '../MultistepJsonForm'; -import { createValidator } from './createValidator'; - -import { Content, Header, InfoCard, Page } from '@backstage/core-components'; -import { - AnalyticsContext, - errorApiRef, - useApi, - useApiHolder, - useRouteRef, - useRouteRefParams, -} from '@backstage/core-plugin-api'; -import { stringifyEntityRef } from '@backstage/catalog-model'; -import { - rootRouteRef, - scaffolderTaskRouteRef, - selectedTemplateRouteRef, -} from '../../routes'; -import { LegacyFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha'; - -const useTemplateParameterSchema = (templateRef: string) => { - const scaffolderApi = useApi(scaffolderApiRef); - const { value, loading, error } = useAsync( - () => scaffolderApi.getTemplateParameterSchema(templateRef), - [scaffolderApi, templateRef], - ); - return { schema: value, loading, error }; -}; - -type Props = { - ReviewStepComponent?: ComponentType; - customFieldExtensions?: LegacyFieldExtensionOptions[]; - layouts?: LayoutOptions[]; - headerOptions?: { - pageTitleOverride?: string; - title?: string; - subtitle?: string; - }; -}; - -export const TemplatePage = ({ - ReviewStepComponent, - customFieldExtensions = [], - layouts = [], - headerOptions, -}: Props) => { - const apiHolder = useApiHolder(); - const secretsContext = useTemplateSecrets(); - const errorApi = useApi(errorApiRef); - const scaffolderApi = useApi(scaffolderApiRef); - const { templateName, namespace } = useRouteRefParams( - selectedTemplateRouteRef, - ); - const templateRef = stringifyEntityRef({ - name: templateName, - kind: 'template', - namespace, - }); - const navigate = useNavigate(); - const scaffolderTaskRoute = useRouteRef(scaffolderTaskRouteRef); - const rootRoute = useRouteRef(rootRouteRef); - const { schema, loading, error } = useTemplateParameterSchema(templateRef); - const [formState, setFormState] = useState>(() => { - const query = qs.parse(window.location.search, { - ignoreQueryPrefix: true, - }); - - try { - return JSON.parse(query.formData as string); - } catch (e) { - return query.formData ?? {}; - } - }); - const handleFormReset = () => setFormState({}); - const handleChange = useCallback( - (e: IChangeEvent) => setFormState(e.formData), - [setFormState], - ); - - const handleCreate = async () => { - const { taskId } = await scaffolderApi.scaffold({ - templateRef, - values: formState, - secrets: secretsContext?.secrets, - }); - - const formParams = qs.stringify( - { formData: formState }, - { addQueryPrefix: true }, - ); - const newUrl = `${window.location.pathname}${formParams}`; - // We use direct history manipulation since useSearchParams and - // useNavigate in react-router-dom cause unnecessary extra rerenders. - // Also make sure to replace the state rather than pushing to avoid - // extra back/forward slots. - window.history?.replaceState(null, document.title, newUrl); - - navigate(scaffolderTaskRoute({ taskId })); - }; - - if (error) { - errorApi.post(new Error(`Failed to load template, ${error}`)); - return ; - } - if (!loading && !schema) { - errorApi.post(new Error('Template was not found.')); - return ; - } - - const customFieldComponents = Object.fromEntries( - customFieldExtensions.map(({ name, component }) => [name, component]), - ); - - const customFieldValidators = Object.fromEntries( - customFieldExtensions.map(({ name, validation }) => [name, validation]), - ); - - return ( - - -
- - {loading && } - {schema && ( - - { - return { - ...step, - validate: createValidator( - step.schema, - customFieldValidators, - { apiHolder }, - ), - }; - })} - /> - - )} - - - - ); -}; diff --git a/plugins/scaffolder/src/legacy/TemplatePage/createValidator.test.ts b/plugins/scaffolder/src/legacy/TemplatePage/createValidator.test.ts deleted file mode 100644 index d6a5cd7499..0000000000 --- a/plugins/scaffolder/src/legacy/TemplatePage/createValidator.test.ts +++ /dev/null @@ -1,200 +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. - */ - -import { createValidator } from './createValidator'; -import { LegacyCustomFieldValidator } from '@backstage/plugin-scaffolder-react/alpha'; -import { ApiHolder } from '@backstage/core-plugin-api'; -import { FieldValidation, FormValidation } from '@rjsf/utils'; - -type CustomLinkType = { - url: string; - title: string; - icon: string; -}; - -describe('createValidator', () => { - const validators: Record< - string, - undefined | LegacyCustomFieldValidator - > = { - CustomPicker: ( - value: unknown, - fieldValidation: FieldValidation, - _context: { apiHolder: ApiHolder }, - ) => { - if (!value || !(value as { value?: unknown }).value) { - fieldValidation.addError('Error !'); - } - }, - CustomLink: ( - values: unknown, - fieldValidation: FieldValidation, - _context: { apiHolder: ApiHolder }, - ) => { - const input = values as CustomLinkType[]; - for (const item of input) { - const validGitlabUrlRegex = - /gitlab\.(?:stg\.)?spotify\.com\?owner=.*&repo=.*/; - - if (!item || !validGitlabUrlRegex.test(item.url)) { - fieldValidation.addError( - `Make sure to put in a valid gitlab clone url.`, - ); - } - } - }, - TagPicker: ( - values: unknown, - fieldValidation: FieldValidation, - _context: { apiHolder: ApiHolder }, - ) => { - const input = values as string[]; - for (const item of input) { - if (!/^[a-z0-9-]+$/.test(item)) { - fieldValidation.addError( - 'A tag name can only contain lowercase letters, numeric characters or dashes', - ); - } - } - }, - }; - - const apiHolderMock: jest.Mocked = { - get: jest.fn().mockImplementation(() => { - return null; - }), - }; - - const context = { - apiHolder: apiHolderMock, - }; - - it('should call validator for object property from a custom field extension', () => { - /* GIVEN */ - const rootSchema = { - title: 'Title', - properties: { - p1: { - title: 'PropertyOn', - type: 'object', - 'ui:field': 'CustomPicker', - }, - }, - }; - const validator = createValidator(rootSchema, validators, context); - - const formData = { - p1: {}, - }; - const errors = { - addError: jest.fn(), - p1: { - addError: jest.fn(), - } as unknown as FormValidation, - } as unknown as FormValidation; - - /* WHEN */ - const result = validator(formData, errors); - - /* THEN */ - expect(result).not.toBeNull(); - expect(result.p1?.addError).toHaveBeenCalledTimes(1); - }); - - it('should call validator for array property from a custom field extension', () => { - /* GIVEN */ - const rootSchema = { - title: 'My form', - properties: { - tags: { - title: 'Tags', - type: 'array', - items: { - type: 'string', - 'ui:field': 'TagPicker', - }, - }, - }, - }; - const validator = createValidator(rootSchema, validators, context); - - const formData = { - tags: ['invalid-tag$$'], - }; - const errors = { - addError: jest.fn(), - tags: { - addError: jest.fn(), - } as unknown as FormValidation, - } as unknown as FormValidation; - - /* WHEN */ - const result = validator(formData, errors); - - /* THEN */ - expect(result).not.toBeNull(); - expect(result.tags?.addError).toHaveBeenCalledTimes(1); - }); - - it('should call validator for array object property from a custom field extension', () => { - /* GIVEN */ - const rootSchema = { - title: 'My links', - properties: { - links: { - title: 'Links', - type: 'array', - items: { - type: 'object', - required: ['url', 'title', 'icon'], - properties: { - url: { - title: 'url', - description: 'url', - type: 'object', - 'ui:field': 'CustomLink', - }, - }, - }, - }, - }, - }; - const validator = createValidator(rootSchema, validators, context); - - const formData = { - links: [ - { - url: 'http://gitlab.spotify.nl/owener=me&repo=test', - icon: 'subject', - title: 'My repository for testing features', - } as CustomLinkType, - ], - }; - const errors = { - addError: jest.fn(), - links: { - addError: jest.fn(), - } as unknown as FormValidation, - } as unknown as FormValidation; - - /* WHEN */ - const result = validator(formData, errors); - - /* THEN */ - expect(result).not.toBeNull(); - expect(result.links?.addError).toHaveBeenCalledTimes(1); - }); -}); diff --git a/plugins/scaffolder/src/legacy/TemplatePage/createValidator.ts b/plugins/scaffolder/src/legacy/TemplatePage/createValidator.ts deleted file mode 100644 index ae0b834fe9..0000000000 --- a/plugins/scaffolder/src/legacy/TemplatePage/createValidator.ts +++ /dev/null @@ -1,103 +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. - */ - -import { LegacyCustomFieldValidator } from '@backstage/plugin-scaffolder-react/alpha'; -import { FieldValidation, FormValidation } from '@rjsf/utils'; -import { JsonObject, JsonValue } from '@backstage/types'; -import { ApiHolder } from '@backstage/core-plugin-api'; - -function isObject(obj: unknown): obj is JsonObject { - return typeof obj === 'object' && obj !== null && !Array.isArray(obj); -} - -function isArray(obj: unknown): obj is JsonObject { - return typeof obj === 'object' && obj !== null && Array.isArray(obj); -} - -export const createValidator = ( - rootSchema: JsonObject, - validators: Record>, - context: { - apiHolder: ApiHolder; - }, -) => { - function validate( - schema: JsonObject, - formData: JsonObject, - errors: FormValidation, - ) { - const schemaProps = schema.properties; - const customObject = schema.type === 'object' && schemaProps === undefined; - - if (!isObject(schemaProps) && !customObject) { - return; - } - - if (schemaProps) { - for (const [key, propData] of Object.entries(formData)) { - const propValidation = errors[key]; - - const doValidate = (item: JsonValue | undefined) => { - if (item && isObject(item)) { - const fieldName = item['ui:field'] as string; - if (fieldName && typeof validators[fieldName] === 'function') { - validators[fieldName]!( - propData as JsonObject[], - propValidation as FieldValidation, - context, - ); - } - } - }; - - const propSchemaProps = schemaProps[key]; - if (isObject(propData) && isObject(propSchemaProps)) { - validate( - propSchemaProps, - propData as JsonObject, - propValidation as FormValidation, - ); - } else if (isArray(propData)) { - if (isObject(propSchemaProps)) { - const { items } = propSchemaProps; - if (isObject(items)) { - if (items.type === 'object') { - const properties = (items?.properties ?? []) as JsonObject[]; - for (const [, value] of Object.entries(properties)) { - doValidate(value); - } - } else { - doValidate(items); - } - } - } - } else { - doValidate(propSchemaProps); - } - } - } else if (customObject) { - const fieldName = schema['ui:field'] as string; - if (fieldName && typeof validators[fieldName] === 'function') { - validators[fieldName]!(formData, errors, context); - } - } - } - - return (formData: JsonObject, errors: FormValidation) => { - validate(rootSchema, formData, errors); - return errors; - }; -}; diff --git a/plugins/scaffolder/src/legacy/TemplatePage/index.ts b/plugins/scaffolder/src/legacy/TemplatePage/index.ts deleted file mode 100644 index d3ae24504b..0000000000 --- a/plugins/scaffolder/src/legacy/TemplatePage/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 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 { TemplatePage } from './TemplatePage'; -export { createValidator } from './createValidator'; diff --git a/plugins/scaffolder/src/legacy/index.ts b/plugins/scaffolder/src/legacy/index.ts deleted file mode 100644 index 2393be0f69..0000000000 --- a/plugins/scaffolder/src/legacy/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 { LegacyRouter, type LegacyRouterProps } from './Router'; -export { LegacyScaffolderPage } from '../plugin'; diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/DryRunResults/DryRunResultsView.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/DryRunResults/DryRunResultsView.tsx index 68ed6f906f..fb69dabe13 100644 --- a/plugins/scaffolder/src/next/TemplateEditorPage/DryRunResults/DryRunResultsView.tsx +++ b/plugins/scaffolder/src/next/TemplateEditorPage/DryRunResults/DryRunResultsView.tsx @@ -27,8 +27,8 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useDryRun } from '../DryRunContext'; import { DryRunResultsSplitView } from './DryRunResultsSplitView'; import { FileBrowser } from '../../../components/FileBrowser'; -import { TaskPageLinks } from '../../../legacy/TaskPage/TaskPageLinks'; -import { TaskStatusStepper } from '../../../legacy/TaskPage/TaskPage'; +import { TaskPageLinks } from './TaskPageLinks'; +import { TaskStatusStepper } from './TaskStatusStepper'; const useStyles = makeStyles({ root: { diff --git a/plugins/scaffolder/src/legacy/TaskPage/IconLink.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/DryRunResults/IconLink.tsx similarity index 97% rename from plugins/scaffolder/src/legacy/TaskPage/IconLink.tsx rename to plugins/scaffolder/src/next/TemplateEditorPage/DryRunResults/IconLink.tsx index dd0b3da876..6676c622fa 100644 --- a/plugins/scaffolder/src/legacy/TaskPage/IconLink.tsx +++ b/plugins/scaffolder/src/next/TemplateEditorPage/DryRunResults/IconLink.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * Copyright 2024 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import React from 'react'; import { Grid, LinkProps, makeStyles, Typography } from '@material-ui/core'; import LanguageIcon from '@material-ui/icons/Language'; diff --git a/plugins/scaffolder/src/legacy/TaskPage/TaskPageLinks.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/DryRunResults/TaskPageLinks.tsx similarity index 100% rename from plugins/scaffolder/src/legacy/TaskPage/TaskPageLinks.tsx rename to plugins/scaffolder/src/next/TemplateEditorPage/DryRunResults/TaskPageLinks.tsx diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/DryRunResults/TaskStatusStepper.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/DryRunResults/TaskStatusStepper.tsx new file mode 100644 index 0000000000..f776d133e0 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateEditorPage/DryRunResults/TaskStatusStepper.tsx @@ -0,0 +1,199 @@ +/* + * Copyright 2024 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 React, { useState } from 'react'; +import { memo } from 'react'; +import Step from '@material-ui/core/Step'; +import StepLabel from '@material-ui/core/StepLabel'; +import Stepper from '@material-ui/core/Stepper'; +import { ScaffolderTaskStatus } from '@backstage/plugin-scaffolder-react'; +import { + StepButton, + StepIconProps, + Theme, + createStyles, + makeStyles, + CircularProgress, +} from '@material-ui/core'; +import Cancel from '@material-ui/icons/Cancel'; +import Check from '@material-ui/icons/Check'; +import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; +import Typography from '@material-ui/core/Typography'; +import { DateTime, Interval } from 'luxon'; +import useInterval from 'react-use/lib/useInterval'; +import humanizeDuration from 'humanize-duration'; +import classNames from 'classnames'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + width: '100%', + }, + button: { + marginBottom: theme.spacing(2), + marginLeft: theme.spacing(2), + }, + actionsContainer: { + marginBottom: theme.spacing(2), + }, + resetContainer: { + padding: theme.spacing(3), + }, + labelWrapper: { + display: 'flex', + flex: 1, + flexDirection: 'row', + justifyContent: 'space-between', + }, + stepWrapper: { + width: '100%', + }, + }), +); + +type TaskStep = { + id: string; + name: string; + status: ScaffolderTaskStatus; + startedAt?: string; + endedAt?: string; +}; + +const useStepIconStyles = makeStyles(theme => + createStyles({ + root: { + color: theme.palette.text.disabled, + display: 'flex', + height: 22, + alignItems: 'center', + }, + completed: { + color: theme.palette.status.ok, + }, + error: { + color: theme.palette.status.error, + }, + }), +); + +const StepTimeTicker = ({ step }: { step: TaskStep }) => { + const [time, setTime] = useState(''); + + useInterval(() => { + if (!step.startedAt) { + setTime(''); + return; + } + + const end = step.endedAt + ? DateTime.fromISO(step.endedAt) + : DateTime.local(); + + const startedAt = DateTime.fromISO(step.startedAt); + const formatted = Interval.fromDateTimes(startedAt, end) + .toDuration() + .valueOf(); + + setTime(humanizeDuration(formatted, { round: true })); + }, 1000); + + return {time}; +}; + +function TaskStepIconComponent(props: StepIconProps) { + const classes = useStepIconStyles(); + const { active, completed, error } = props; + + const getMiddle = () => { + if (active) { + return ; + } + if (completed) { + return ; + } + if (error) { + return ; + } + return ; + }; + + return ( +
+ {getMiddle()} +
+ ); +} + +export const TaskStatusStepper = memo( + (props: { + steps: TaskStep[]; + currentStepId: string | undefined; + onUserStepChange: (id: string) => void; + classes?: { + root?: string; + }; + }) => { + const { steps, currentStepId, onUserStepChange } = props; + const classes = useStyles(props); + + return ( +
+ s.id === currentStepId)} + orientation="vertical" + nonLinear + > + {steps.map((step, index) => { + const isCancelled = step.status === 'cancelled'; + const isActive = step.status === 'processing'; + const isCompleted = step.status === 'completed'; + const isFailed = step.status === 'failed'; + const isSkipped = step.status === 'skipped'; + + return ( + + onUserStepChange(step.id)}> + +
+ {step.name} + {isSkipped ? ( + Skipped + ) : ( + + )} +
+
+
+
+ ); + })} +
+
+ ); + }, +); diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index fdde81695d..7fa7dc1521 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -212,15 +212,3 @@ export const EntityTagsPickerFieldExtension = scaffolderPlugin.provide( schema: EntityTagsPickerSchema, }), ); - -/** - * @alpha - * The Router and main entrypoint to the Alpha Scaffolder plugin. - */ -export const LegacyScaffolderPage = scaffolderPlugin.provide( - createRoutableExtension({ - name: 'LegacyScaffolderPage', - component: () => import('./legacy/Router').then(m => m.LegacyRouter), - mountPoint: rootRouteRef, - }), -);