From a8c5f1a08f0417c8725b7fde9e9b402e489b7b6b Mon Sep 17 00:00:00 2001 From: Iain Billett Date: Thu, 10 Mar 2022 14:41:07 +0000 Subject: [PATCH 01/92] Add an optional external Id field Signed-off-by: Iain Billett --- packages/backend-common/src/reading/AwsS3UrlReader.ts | 4 ++++ packages/integration/config.d.ts | 6 ++++++ packages/integration/src/awsS3/config.ts | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/packages/backend-common/src/reading/AwsS3UrlReader.ts b/packages/backend-common/src/reading/AwsS3UrlReader.ts index e64334321d..86ff0e7d65 100644 --- a/packages/backend-common/src/reading/AwsS3UrlReader.ts +++ b/packages/backend-common/src/reading/AwsS3UrlReader.ts @@ -154,11 +154,15 @@ export class AwsS3UrlReader implements UrlReader { const roleArn = integration.config.roleArn; if (roleArn) { + const externalIdParam = integration.config.externalId + ? { ExternalId: integration.config.externalId } + : {}; return new aws.ChainableTemporaryCredentials({ masterCredentials: explicitCredentials, params: { RoleSessionName: 'backstage-aws-s3-url-reader', RoleArn: roleArn, + ...externalIdParam, }, }); } diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index 8873f0f220..0b88a8ff81 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -200,6 +200,12 @@ export interface Config { * @visibility backend */ roleArn?: string; + + /** + * External ID to use when assuming role + * @visibility backend + */ + externalId?: string; }>; }; } diff --git a/packages/integration/src/awsS3/config.ts b/packages/integration/src/awsS3/config.ts index 45cbdbe210..2054b9eab0 100644 --- a/packages/integration/src/awsS3/config.ts +++ b/packages/integration/src/awsS3/config.ts @@ -59,6 +59,11 @@ export type AwsS3IntegrationConfig = { * (Optional) ARN of role to be assumed */ roleArn?: string; + + /** + * (optional) External ID to use when assuming role + */ + externalId?: string; }; /** @@ -98,6 +103,7 @@ export function readAwsS3IntegrationConfig( const accessKeyId = config.getOptionalString('accessKeyId'); const secretAccessKey = config.getOptionalString('secretAccessKey'); const roleArn = config.getOptionalString('roleArn'); + const externalId = config.getOptionalString('externalId'); return { host, @@ -106,6 +112,7 @@ export function readAwsS3IntegrationConfig( accessKeyId, secretAccessKey, roleArn, + externalId, }; } From 6ad0c456486a11769475c10648f157243093a8e2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 11 Mar 2022 18:03:32 +0100 Subject: [PATCH 02/92] cli: add experimental add-deps command Signed-off-by: Patrik Oldsberg --- .changeset/happy-foxes-arrive.md | 5 + packages/cli/src/commands/add-deps.ts | 142 ++++++++++++++++++++++++++ packages/cli/src/commands/index.ts | 7 ++ 3 files changed, 154 insertions(+) create mode 100644 .changeset/happy-foxes-arrive.md create mode 100644 packages/cli/src/commands/add-deps.ts diff --git a/.changeset/happy-foxes-arrive.md b/.changeset/happy-foxes-arrive.md new file mode 100644 index 0000000000..03ddfede0d --- /dev/null +++ b/.changeset/happy-foxes-arrive.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added an experimental `add-deps` command which adds missing monorepo dependencies to the target package.` diff --git a/packages/cli/src/commands/add-deps.ts b/packages/cli/src/commands/add-deps.ts new file mode 100644 index 0000000000..e4cdc4dc84 --- /dev/null +++ b/packages/cli/src/commands/add-deps.ts @@ -0,0 +1,142 @@ +/* + * 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 { paths } from '../lib/paths'; +import { ESLint } from 'eslint'; +import { join as joinPath, basename } from 'path'; +import fs from 'fs-extra'; +import { isChildPath } from '@backstage/cli-common'; +import { PackageGraph } from '../lib/monorepo'; + +function isTestPath(filePath: string) { + if (!isChildPath(joinPath(paths.targetDir, 'src'), filePath)) { + return true; + } + const name = basename(filePath); + return ( + name.startsWith('setupTests.') || + name.includes('.test.') || + name.includes('.stories.') + ); +} + +export async function command() { + const pkgJsonPath = paths.resolveTarget('package.json'); + const pkg = await fs.readJson(pkgJsonPath); + if (pkg.workspaces) { + throw new Error( + 'Adding dependencies to the workspace root is not supported', + ); + } + + const packages = await PackageGraph.listTargetPackages(); + const localPackageVersions = new Map( + packages.map(p => [p.packageJson.name, p.packageJson.version]), + ); + + const eslint = new ESLint({ + cwd: paths.targetDir, + overrideConfig: { + plugins: ['monorepo'], + rules: { + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: [ + `!${joinPath(paths.targetDir, 'src/**')}`, + joinPath(paths.targetDir, 'src/**/*.test.*'), + joinPath(paths.targetDir, 'src/**/*.stories.*'), + joinPath(paths.targetDir, 'src/setupTests.*'), + ], + optionalDependencies: true, + peerDependencies: true, + bundledDependencies: true, + }, + ], + }, + }, + extensions: ['jsx', 'ts', 'tsx', 'mjs', 'cjs'], + }); + + const results = await eslint.lintFiles(['.']); + + const addedDeps = new Set(); + const addedDevDeps = new Set(); + const removedDevDeps = new Set(); + + for (const result of results) { + for (const message of result.messages) { + // Just in case + if (message.ruleId !== 'import/no-extraneous-dependencies') { + continue; + } + + const match = message.message.match(/^'([^']*)' should be listed/); + if (!match) { + continue; + } + const packageName = match[1]; + if (!localPackageVersions.has(packageName)) { + continue; + } + + if (message.message.endsWith('not devDependencies.')) { + addedDeps.add(packageName); + removedDevDeps.add(packageName); + } else if (isTestPath(result.filePath)) { + addedDevDeps.add(packageName); + } else { + addedDeps.add(packageName); + } + } + } + + if (addedDeps.size || addedDevDeps.size || removedDevDeps.size) { + for (const name of addedDeps) { + if (!pkg.dependencies) { + pkg.dependencies = {}; + } + pkg.dependencies[name] = `^${localPackageVersions.get(name)}`; + } + for (const name of addedDevDeps) { + if (!pkg.devDependencies) { + pkg.devDependencies = {}; + } + pkg.devDependencies[name] = `^${localPackageVersions.get(name)}`; + } + for (const name of removedDevDeps) { + delete pkg.devDependencies[name]; + } + if (Object.keys(pkg.devDependencies).length === 0) { + delete pkg.devDependencies; + } + + if (pkg.dependencies) { + pkg.dependencies = Object.fromEntries( + Object.entries(pkg.dependencies).sort(([a], [b]) => a.localeCompare(b)), + ); + } + if (pkg.devDependencies) { + pkg.devDependencies = Object.fromEntries( + Object.entries(pkg.devDependencies).sort(([a], [b]) => + a.localeCompare(b), + ), + ); + } + + await fs.writeJson(pkgJsonPath, pkg, { spaces: 2 }); + } +} diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index bc427bb19b..b50831f981 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -386,6 +386,13 @@ export function registerCommands(program: CommanderStatic) { .description('Check Backstage package versioning') .action(lazy(() => import('./versions/lint').then(m => m.default))); + program + .command('add-deps', { hidden: true }) + .description( + 'Add missing monorepo dependencies to package.json [EXPERIMENTAL]', + ) + .action(lazy(() => import('./add-deps').then(m => m.command))); + // TODO(Rugvip): Deprecate in favor of package variant program .command('prepack') From 6927f8541a1b420e40261ee9a270c8a82453edc2 Mon Sep 17 00:00:00 2001 From: Iain Billett Date: Wed, 16 Mar 2022 12:09:27 +0000 Subject: [PATCH 03/92] Update API report Signed-off-by: Iain Billett --- packages/integration/api-report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 84bca54a90..d86acb291a 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -35,6 +35,7 @@ export type AwsS3IntegrationConfig = { accessKeyId?: string; secretAccessKey?: string; roleArn?: string; + externalId?: string; }; // @public From 3ef123bbf05258351c0952ecfc1ffb27ad059e8c Mon Sep 17 00:00:00 2001 From: Iain Billett Date: Wed, 16 Mar 2022 15:40:49 +0000 Subject: [PATCH 04/92] Add changeset Signed-off-by: Iain Billett --- .changeset/olive-geese-chew.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/olive-geese-chew.md diff --git a/.changeset/olive-geese-chew.md b/.changeset/olive-geese-chew.md new file mode 100644 index 0000000000..a08b73b4a7 --- /dev/null +++ b/.changeset/olive-geese-chew.md @@ -0,0 +1,10 @@ +--- +'@backstage/backend-common': patch +'@backstage/integration': patch +--- + +Support external ID when assuming roles in S3 integration + +In order to assume a role created by a 3rd party as external +ID is needed. This change adds an optional field to the s3 +integration configuration and consumes that in the AwsS3UrlReader. From 1fc19dd3ecfa7795c63ec6f1f40f04030165f209 Mon Sep 17 00:00:00 2001 From: Iain Billett Date: Wed, 16 Mar 2022 15:56:41 +0000 Subject: [PATCH 05/92] Add docs Signed-off-by: Iain Billett --- docs/integrations/aws-s3/locations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations/aws-s3/locations.md b/docs/integrations/aws-s3/locations.md index b0c6829e2c..9bac28f906 100644 --- a/docs/integrations/aws-s3/locations.md +++ b/docs/integrations/aws-s3/locations.md @@ -37,6 +37,7 @@ integrations: - accessKeyId: ${AWS_ACCESS_KEY_ID} secretAccessKey: ${AWS_SECRET_ACCESS_KEY} roleArn: 'arn:aws:iam::xxxxxxxxxxxx:role/example-role' + externalId: 'some-id' # optional ``` Configuration allows specifying custom S3 endpoint, along with From 06ce6d19d52acc10d1aa9b3981ae675853de5bfa Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 4 Feb 2022 15:42:56 +0100 Subject: [PATCH 06/92] feat: added new simple router with some tests Signed-off-by: blam --- .../src/next/Router/Router.test.tsx | 83 +++++++++++++++++++ plugins/scaffolder/src/next/Router/Router.tsx | 81 ++++++++++++++++++ plugins/scaffolder/src/next/Router/index.ts | 16 ++++ 3 files changed, 180 insertions(+) create mode 100644 plugins/scaffolder/src/next/Router/Router.test.tsx create mode 100644 plugins/scaffolder/src/next/Router/Router.tsx create mode 100644 plugins/scaffolder/src/next/Router/index.ts diff --git a/plugins/scaffolder/src/next/Router/Router.test.tsx b/plugins/scaffolder/src/next/Router/Router.test.tsx new file mode 100644 index 0000000000..5094c786e0 --- /dev/null +++ b/plugins/scaffolder/src/next/Router/Router.test.tsx @@ -0,0 +1,83 @@ +/* + * 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 from 'react'; +import { TemplateListPage } from '../TemplateListPage'; +import { TemplateWizardPage } from '../TemplateWizardPage'; +import { Router } from './Router'; +import { renderInTestApp } from '@backstage/test-utils'; +import { + createScaffolderFieldExtension, + ScaffolderFieldExtensions, +} from '../../extensions'; +import { scaffolderPlugin } from '../../plugin'; + +jest.mock('../TemplateListPage', () => ({ + TemplateListPage: jest.fn(() => null), +})); + +jest.mock('../TemplateWizardPage', () => ({ + TemplateWizardPage: jest.fn(() => null), +})); + +describe('Router', () => { + beforeEach(() => { + (TemplateWizardPage as jest.Mock).mockClear(); + (TemplateListPage as jest.Mock).mockClear(); + }); + describe('/', () => { + it('should render the TemplateListPage', async () => { + await renderInTestApp(); + + expect(TemplateListPage).toHaveBeenCalled(); + }); + }); + + describe('/templates/:templateName', () => { + it('should render the TemplateWizard page', async () => { + await renderInTestApp(, { routeEntries: ['/templates/foo'] }); + + expect(TemplateWizardPage).toHaveBeenCalled(); + }); + + it('should extract the fieldExtensions and pass them through', async () => { + const mockComponent = () => null; + const CustomFieldExtension = scaffolderPlugin.provide( + createScaffolderFieldExtension({ + name: 'custom', + component: mockComponent, + }), + ); + + await renderInTestApp( + + + + + , + { routeEntries: ['/templates/foo'] }, + ); + + const mock = TemplateWizardPage as jest.Mock; + const [{ customFieldExtensions }] = mock.mock.calls[0]; + + expect(customFieldExtensions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'custom', component: mockComponent }), + ]), + ); + }); + }); +}); diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx new file mode 100644 index 0000000000..8ab9755ab2 --- /dev/null +++ b/plugins/scaffolder/src/next/Router/Router.tsx @@ -0,0 +1,81 @@ +/* + * 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, { PropsWithChildren } from 'react'; +import { Routes, Route, useOutlet } from 'react-router'; +import { TemplateListPage } from '../TemplateListPage'; +import { SecretsContextProvider } from '../TemplateWizardPage/SecretsContext'; +import { TemplateWizardPage } from '../TemplateWizardPage'; +import { + FieldExtensionOptions, + FIELD_EXTENSION_WRAPPER_KEY, + FIELD_EXTENSION_KEY, + DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS, +} from '../../extensions'; + +import { useElementFilter } from '@backstage/core-plugin-api'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; + +export type RouterProps = { + TemplateCardComponent?: React.ComponentType<{ + template: TemplateEntityV1beta3; + }>; +}; + +export const Router = (props: PropsWithChildren) => { + const { TemplateCardComponent } = props; + + const outlet = useOutlet() || props.children; + + const customFieldExtensions = useElementFilter(outlet, elements => + elements + .selectByComponentData({ + key: FIELD_EXTENSION_WRAPPER_KEY, + }) + .findComponentData({ + key: FIELD_EXTENSION_KEY, + }), + ); + + const fieldExtensions = [ + ...customFieldExtensions, + ...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter( + ({ name }) => + !customFieldExtensions.some( + customFieldExtension => customFieldExtension.name === name, + ), + ), + ]; + + return ( + + + } + /> + + + + + } + /> + + ); +}; diff --git a/plugins/scaffolder/src/next/Router/index.ts b/plugins/scaffolder/src/next/Router/index.ts new file mode 100644 index 0000000000..5ad2ae6e6a --- /dev/null +++ b/plugins/scaffolder/src/next/Router/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { Router } from './Router'; From 8afeb82cc35d0b7528b5231b4f9d656fe597525f Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 4 Feb 2022 15:43:20 +0100 Subject: [PATCH 07/92] feat: adding a new TemplateListPage and CategoryPicker with tests Signed-off-by: blam --- .../TemplateListPage/CategoryPicker.test.tsx | 130 ++++++++++++++++++ .../next/TemplateListPage/CategoryPicker.tsx | 81 +++++++++++ .../TemplateListPage/TemplateListPage.tsx | 121 ++++++++++++++++ .../src/next/TemplateListPage/index.ts | 16 +++ 4 files changed, 348 insertions(+) create mode 100644 plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx create mode 100644 plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx create mode 100644 plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx create mode 100644 plugins/scaffolder/src/next/TemplateListPage/index.ts diff --git a/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx new file mode 100644 index 0000000000..a04d382445 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx @@ -0,0 +1,130 @@ +/* + * 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 from 'react'; +import { useEntityTypeFilter } from '@backstage/plugin-catalog-react'; +import { CategoryPicker } from './CategoryPicker'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { alertApiRef } from '@backstage/core-plugin-api'; +import { fireEvent } from '@testing-library/react'; + +jest.mock('@backstage/plugin-catalog-react', () => ({ + useEntityTypeFilter: jest.fn(), +})); + +describe('CategoryPicker', () => { + const mockAlertApi = { post: jest.fn() }; + + beforeEach(() => { + mockAlertApi.post.mockClear(); + }); + + it('should post the error to errorApi if an errors is returned', async () => { + (useEntityTypeFilter as jest.Mock).mockReturnValue({ + error: new Error('something broked'), + }); + + await renderInTestApp( + + + , + ); + + expect(mockAlertApi.post).toHaveBeenCalledWith({ + message: expect.stringContaining('something broked'), + severity: 'error', + }); + }); + + it('should render loading if the hook is loading', async () => { + (useEntityTypeFilter as jest.Mock).mockReturnValue({ + loading: true, + }); + + const { findByTestId } = await renderInTestApp( + + + , + ); + + expect(await findByTestId('progress')).toBeInTheDocument(); + }); + + it('should not render if there is no available types', async () => { + (useEntityTypeFilter as jest.Mock).mockReturnValue({ + availableTypes: null, + }); + + const { queryByText } = await renderInTestApp( + + + , + ); + + expect(queryByText('Categories')).not.toBeInTheDocument(); + }); + + it('renders the autocomplete with the availableTypes', async () => { + const mockAvailableTypes = ['foo', 'bar']; + + (useEntityTypeFilter as jest.Mock).mockReturnValue({ + availableTypes: mockAvailableTypes, + }); + + const { getByRole } = await renderInTestApp( + + + , + ); + + const openButton = getByRole('button', { name: 'Open' }); + openButton.click(); + + expect(getByRole('checkbox', { name: 'Foo' })).toBeInTheDocument(); + expect(getByRole('checkbox', { name: 'Bar' })).toBeInTheDocument(); + }); + + it('should call setSelectedTypes when one of the options are called', async () => { + const mockAvailableTypes = ['foo', 'bar']; + const mockSetSelectedTypes = jest.fn(); + + (useEntityTypeFilter as jest.Mock).mockReturnValue({ + availableTypes: mockAvailableTypes, + setSelectedTypes: mockSetSelectedTypes, + }); + + const { getByRole } = await renderInTestApp( + + + , + ); + + const openButton = getByRole('button', { name: 'Open' }); + await fireEvent(openButton, new MouseEvent('click', { bubbles: true })); + + const fooCheckbox = getByRole('checkbox', { name: 'Foo' }); + await fireEvent(fooCheckbox, new MouseEvent('click', { bubbles: true })); + + expect(mockSetSelectedTypes).toHaveBeenCalledWith(['foo']); + + await fireEvent(openButton, new MouseEvent('click', { bubbles: true })); + + const barCheckbox = getByRole('checkbox', { name: 'Bar' }); + await fireEvent(barCheckbox, new MouseEvent('click', { bubbles: true })); + + expect(mockSetSelectedTypes).toHaveBeenCalledWith(['foo', 'bar']); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx b/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx new file mode 100644 index 0000000000..94f0df2565 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx @@ -0,0 +1,81 @@ +/* + * 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 capitalize from 'lodash/capitalize'; +import { Progress } from '@backstage/core-components'; +import { + Box, + Checkbox, + FormControlLabel, + TextField, + Typography, +} from '@material-ui/core'; +import CheckBoxIcon from '@material-ui/icons/CheckBox'; +import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { Autocomplete } from '@material-ui/lab'; +import { useEntityTypeFilter } from '@backstage/plugin-catalog-react'; +import { alertApiRef, useApi } from '@backstage/core-plugin-api'; + +const icon = ; +const checkedIcon = ; + +export const CategoryPicker = () => { + const alertApi = useApi(alertApiRef); + const { error, loading, availableTypes, selectedTypes, setSelectedTypes } = + useEntityTypeFilter(); + + if (loading) return ; + + if (error) { + alertApi.post({ + message: `Failed to load entity types with error: ${error}`, + severity: 'error', + }); + return null; + } + + if (!availableTypes) return null; + + return ( + + Categories + setSelectedTypes(value)} + renderOption={(option, { selected }) => ( + + } + label={capitalize(option)} + /> + )} + size="small" + popupIcon={} + renderInput={params => } + /> + + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx new file mode 100644 index 0000000000..dc736408ac --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx @@ -0,0 +1,121 @@ +/* + * 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 from 'react'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { Entity } from '@backstage/catalog-model'; +import { makeStyles } from '@material-ui/core'; +import { + Content, + ContentHeader, + CreateButton, + Header, + Lifecycle, + Page, + SupportButton, +} from '@backstage/core-components'; +import { + EntityKindPicker, + EntityListProvider, + EntitySearchBar, + EntityTagPicker, + UserListPicker, +} from '@backstage/plugin-catalog-react'; +import { CategoryPicker } from './CategoryPicker'; + +export type TemplateListGroup = { + title?: string; + titleComponent?: React.ReactNode; + filter: (entity: Entity) => boolean; +}; + +export type TemplateListPageProps = { + TemplateCardComponent?: React.ComponentType<{ + template: TemplateEntityV1beta3; + }>; + groups?: TemplateListGroup[]; +}; + +const useStyles = makeStyles(theme => ({ + contentWrapper: { + display: 'grid', + gridTemplateAreas: "'filters' 'grid'", + gridTemplateColumns: '250px 1fr', + gridColumnGap: theme.spacing(2), + }, +})); + +export const TemplateListPage = (props: TemplateListPageProps) => { + const styles = useStyles(); + return ( + + +
+ Create a New Component + + } + subtitle="Create new software components using standard templates" + /> + + + {/* {allowed && ( + + )} */} + + Create new software components using standard templates. Different + templates create different kinds of components (services, + websites, documentation, ...). + + + +
+
+ +
+
+ {/* {groups && + groups.map((group, index) => ( + + ))} + */} +
+
+
+ + + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateListPage/index.ts b/plugins/scaffolder/src/next/TemplateListPage/index.ts new file mode 100644 index 0000000000..efe9f13d08 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { TemplateListPage } from './TemplateListPage'; From 44f44f6408b2f4d7d565846038800c7ed194e887 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 4 Feb 2022 15:50:55 +0100 Subject: [PATCH 08/92] chore: added a test that the selected types are rendered in the document Signed-off-by: blam --- .../TemplateListPage/CategoryPicker.test.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx index a04d382445..f02ca51354 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx @@ -127,4 +127,26 @@ describe('CategoryPicker', () => { expect(mockSetSelectedTypes).toHaveBeenCalledWith(['foo', 'bar']); }); + + it('should render the selectedTypes already in the document', async () => { + const mockAvailableTypes = ['foo', 'bar']; + const mockSelectedTypes = ['foo']; + + (useEntityTypeFilter as jest.Mock).mockReturnValue({ + availableTypes: mockAvailableTypes, + selectedTypes: mockSelectedTypes, + }); + + const { getByRole } = await renderInTestApp( + + + , + ); + + const openButton = getByRole('button', { name: 'Open' }); + await fireEvent(openButton, new MouseEvent('click', { bubbles: true })); + + const fooCheckbox = getByRole('checkbox', { name: 'Foo' }); + expect(fooCheckbox).toBeChecked(); + }); }); From a4ad1ddcb67abbb07db85aeae6842f51bab8505e Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 4 Feb 2022 19:22:26 +0100 Subject: [PATCH 09/92] chore: added deprecation to create button, doesn't belong in core-components Signed-off-by: blam --- .../src/components/CreateButton/CreateButton.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core-components/src/components/CreateButton/CreateButton.tsx b/packages/core-components/src/components/CreateButton/CreateButton.tsx index d5d7080851..056ead5811 100644 --- a/packages/core-components/src/components/CreateButton/CreateButton.tsx +++ b/packages/core-components/src/components/CreateButton/CreateButton.tsx @@ -25,6 +25,7 @@ import AddCircleOutline from '@material-ui/icons/AddCircleOutline'; /** * Properties for {@link CreateButton} * + * @deprecated * @public */ export type CreateButtonProps = { @@ -35,6 +36,9 @@ export type CreateButtonProps = { * Responsive Button giving consistent UX for creation of different things * * @public + * + * @deprecated Please use IconButton directly and have your own implementation of this component. + * It doesn't really fit as a core-component. */ export function CreateButton(props: CreateButtonProps) { const { title, to } = props; From 8db4dc297a13d6f325d18960f57e76f4bafb559b Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 5 Feb 2022 04:48:49 +0100 Subject: [PATCH 10/92] feat: added some nice tests for the TemplateListPage Signed-off-by: blam --- .../RegisterExistingButton.test.tsx | 57 +++++++++++++++ .../RegisterExistingButton.tsx | 66 +++++++++++++++++ .../TemplateListPage.test.tsx | 73 +++++++++++++++++++ .../TemplateListPage/TemplateListPage.tsx | 15 ++-- .../SecretsContext/SecretsContext.test.tsx | 43 +++++++++++ .../SecretsContext/SecretsContext.tsx | 73 +++++++++++++++++++ .../SecretsContext/index.ts | 20 +++++ .../TemplateWizardPage/TemplateWizardPage.tsx | 16 ++++ .../src/next/TemplateWizardPage/index.ts | 17 +++++ 9 files changed, 374 insertions(+), 6 deletions(-) create mode 100644 plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.test.tsx create mode 100644 plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.tsx create mode 100644 plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx create mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.test.tsx create mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.tsx create mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/index.ts create mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx create mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/index.ts diff --git a/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.test.tsx new file mode 100644 index 0000000000..e6022d16fb --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.test.tsx @@ -0,0 +1,57 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { RegisterExistingButton } from './RegisterExistingButton'; +import { usePermission } from '@backstage/plugin-permission-react'; + +jest.mock('@backstage/plugin-permission-react', () => ({ + usePermission: jest.fn(), +})); + +describe('RegisterExistingButton', () => { + beforeEach(() => { + (usePermission as jest.Mock).mockClear(); + }); + + it('should not render if to is unset', async () => { + (usePermission as jest.Mock).mockReturnValue({ allowed: true }); + + const { queryByText } = await renderInTestApp( + , + ); + + expect(await queryByText('Pick me')).not.toBeInTheDocument(); + }); + + it('should not render if permissions are not allowed', async () => { + (usePermission as jest.Mock).mockReturnValue({ allowed: false }); + const { queryByText } = await renderInTestApp( + , + ); + + expect(await queryByText('Pick me')).not.toBeInTheDocument(); + }); + + it('should render the button with the text', async () => { + (usePermission as jest.Mock).mockReturnValue({ allowed: true }); + const { queryByText } = await renderInTestApp( + , + ); + + expect(await queryByText('Pick me')).toBeInTheDocument(); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.tsx b/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.tsx new file mode 100644 index 0000000000..be6b496a6c --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.tsx @@ -0,0 +1,66 @@ +/* + * 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 { BackstageTheme } from '@backstage/theme'; +import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; +import React from 'react'; +import { Link as RouterLink, LinkProps } from 'react-router-dom'; +import AddCircleOutline from '@material-ui/icons/AddCircleOutline'; +import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; +import { usePermission } from '@backstage/plugin-permission-react'; + +/** + * Properties for {@link RegisterExistingButton} + * + * @public + */ +export type RegisterExistingButtonProps = { + title: string; +} & Partial>; + +/** + * A button that helps users to register an existing component. + * @public + */ +export const RegisterExistingButton = (props: RegisterExistingButtonProps) => { + const { title, to } = props; + const { allowed } = usePermission(catalogEntityCreatePermission); + const isXSScreen = useMediaQuery(theme => + theme.breakpoints.down('xs'), + ); + + if (!to || !allowed) { + return null; + } + + return isXSScreen ? ( + + + + ) : ( + + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx new file mode 100644 index 0000000000..48ee53cdb7 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx @@ -0,0 +1,73 @@ +/* + * 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 { + catalogApiRef, + DefaultStarredEntitiesApi, + starredEntitiesApiRef, +} from '@backstage/plugin-catalog-react'; +import { permissionApiRef } from '@backstage/plugin-permission-react'; +import { + MockStorageApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; +import React from 'react'; +import { TemplateListPage } from './TemplateListPage'; + +describe('TemplateListPage', () => { + it('should render the search bar for templates', async () => { + const { getByPlaceholderText } = await renderInTestApp( + + + , + ); + + expect(getByPlaceholderText('Search')).toBeInTheDocument(); + }); + + it('should render the all and starred filters', async () => { + const { getByRole } = await renderInTestApp( + + + , + ); + + expect(getByRole('menuitem', { name: 'All' })).toBeInTheDocument(); + expect(getByRole('menuitem', { name: 'Starred' })).toBeInTheDocument(); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx index dc736408ac..25441a6812 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx @@ -35,6 +35,9 @@ import { UserListPicker, } from '@backstage/plugin-catalog-react'; import { CategoryPicker } from './CategoryPicker'; +import { RegisterExistingButton } from './RegisterExistingButton'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import { registerComponentRouteRef } from '../../routes'; export type TemplateListGroup = { title?: string; @@ -60,6 +63,8 @@ const useStyles = makeStyles(theme => ({ export const TemplateListPage = (props: TemplateListPageProps) => { const styles = useStyles(); + const registerComponentLink = useRouteRef(registerComponentRouteRef); + return ( @@ -74,12 +79,10 @@ export const TemplateListPage = (props: TemplateListPageProps) => { /> - {/* {allowed && ( - - )} */} + Create new software components using standard templates. Different templates create different kinds of components (services, diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.test.tsx new file mode 100644 index 0000000000..37d35c9015 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.test.tsx @@ -0,0 +1,43 @@ +/* + * 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, { useContext } from 'react'; +import { + useTemplateSecrets, + SecretsContextProvider, + SecretsContext, +} from './SecretsContext'; +import { renderHook, act } from '@testing-library/react-hooks'; + +describe('SecretsContext', () => { + it('should allow the setting of secrets in the context', async () => { + const { result } = renderHook( + () => ({ + hook: useTemplateSecrets(), + context: useContext(SecretsContext), + }), + { + wrapper: ({ children }) => ( + {children} + ), + }, + ); + expect(result.current.context?.secrets.foo).toEqual(undefined); + + act(() => result.current.hook.setSecret({ foo: 'bar' })); + + expect(result.current.context?.secrets.foo).toEqual('bar'); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.tsx new file mode 100644 index 0000000000..6570f9d901 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.tsx @@ -0,0 +1,73 @@ +/* + * 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, + useCallback, + useContext, + createContext, + PropsWithChildren, +} from 'react'; + +type SecretsContextContents = { + secrets: Record; + setSecrets: React.Dispatch>>; +}; + +/** + * The actual context object. + */ +export const SecretsContext = createContext( + undefined, +); + +/** + * The Context Provider that holds the state for the secrets. + * + * @public + */ +export const SecretsContextProvider = ({ children }: PropsWithChildren<{}>) => { + const [secrets, setSecrets] = useState>({}); + + return ( + + {children} + + ); +}; + +/** + * Hook to access the secrets context. + * @public + */ +export const useTemplateSecrets = () => { + const value = useContext(SecretsContext); + if (!value) { + throw new Error( + 'useTemplateSecrets must be used within a SecretsContextProvider', + ); + } + + const { setSecrets } = value; + + const setSecret = useCallback( + (input: Record) => { + setSecrets(currentSecrets => ({ ...currentSecrets, ...input })); + }, + [setSecrets], + ); + + return { setSecret }; +}; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/index.ts b/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/index.ts new file mode 100644 index 0000000000..65b530dea1 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { + useTemplateSecrets, + SecretsContext, + SecretsContextProvider, +} from './SecretsContext'; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx new file mode 100644 index 0000000000..94f592eb60 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export const TemplateWizardPage = () => {}; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/index.ts b/plugins/scaffolder/src/next/TemplateWizardPage/index.ts new file mode 100644 index 0000000000..84ecf6b3d5 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { TemplateWizardPage } from './TemplateWizardPage'; +export { useTemplateSecrets } from './SecretsContext'; From df0d2e7d97b9b2ba97e2f58512cdf32c98673f93 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 7 Feb 2022 17:17:21 +0100 Subject: [PATCH 11/92] feat: added some more steps towards rewriting the scaffodler-frontend Signed-off-by: blam --- app-config.yaml | 3 +++ packages/app/src/App.tsx | 4 ++-- .../EntitySearchBar/EntitySearchBar.tsx | 1 + plugins/scaffolder/src/index.ts | 1 + .../TemplateListPage.test.tsx | 21 +++++++++++++++++++ plugins/scaffolder/src/plugin.ts | 8 +++++++ 6 files changed, 36 insertions(+), 2 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index b3ca3c7203..597073fede 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -283,6 +283,9 @@ catalog: # Backstage end-to-end tests of TechDocs - type: file target: ../../cypress/e2e-fixture.catalog.info.yaml + + - type: url + target: https://github.com/benjdlambert/software-templates/blob/main/scaffolder-templates/react-ssr-template/template.yaml scaffolder: # Use to customize default commit author info used when new components are created # defaultAuthor: diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index befd3274f3..e17e0e80a8 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -59,7 +59,7 @@ import { LighthousePage } from '@backstage/plugin-lighthouse'; import { NewRelicPage } from '@backstage/plugin-newrelic'; import { ScaffolderFieldExtensions, - ScaffolderPage, + NextScaffolderPage, scaffolderPlugin, } from '@backstage/plugin-scaffolder'; import { SearchPage } from '@backstage/plugin-search'; @@ -178,7 +178,7 @@ const routes = ( { { expect(getByRole('menuitem', { name: 'All' })).toBeInTheDocument(); expect(getByRole('menuitem', { name: 'Starred' })).toBeInTheDocument(); }); + + it('should render the category picker', () => { + const { getByText } = await renderInTestApp( + + + , + ); + + expect(getByText('Categories')).toBeInTheDocument(); + }); }); diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index ffa0efaaa0..9b79ea8381 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -150,3 +150,11 @@ export const EntityTagsPickerFieldExtension = scaffolderPlugin.provide( name: 'EntityTagsPicker', }), ); + +export const NextScaffolderPage = scaffolderPlugin.provide( + createRoutableExtension({ + name: 'NextScaffolderPage', + component: () => import('./next/Router').then(m => m.Router), + mountPoint: rootRouteRef, + }), +); From 1c05488c9209c19d89d3f8f488a0c41ccf6583e2 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 25 Feb 2022 13:52:56 +0100 Subject: [PATCH 12/92] chore: added some more tests for the TemplateListPage Signed-off-by: blam --- .../TemplateListPage.test.tsx | 36 ++++++++++++++++++- .../TemplateListPage/TemplateListPage.tsx | 1 - 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx index 3cc865b95c..1f6c60237c 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx @@ -71,7 +71,7 @@ describe('TemplateListPage', () => { expect(getByRole('menuitem', { name: 'Starred' })).toBeInTheDocument(); }); - it('should render the category picker', () => { + it('should render the category picker', async () => { const { getByText } = await renderInTestApp( { expect(getByText('Categories')).toBeInTheDocument(); }); + + it('should render the EntityTag picker', async () => { + const { getByText } = await renderInTestApp( + ({ + items: [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'blob', tags: ['blob'] }, + }, + ], + }), + }, + ], + [ + starredEntitiesApiRef, + new DefaultStarredEntitiesApi({ + storageApi: MockStorageApi.create(), + }), + ], + [permissionApiRef, {}], + ]} + > + + , + ); + + expect(getByText('Tags')).toBeInTheDocument(); + }); }); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx index 25441a6812..aab72f3cdf 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx @@ -21,7 +21,6 @@ import { makeStyles } from '@material-ui/core'; import { Content, ContentHeader, - CreateButton, Header, Lifecycle, Page, From 671cba631cb99fe93f32cee422fdbbe1ed40b2ff Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 25 Feb 2022 15:42:48 +0100 Subject: [PATCH 13/92] chore: working on writing some more components for all this stuff Signed-off-by: blam --- plugins/scaffolder/src/next/Router/Router.tsx | 12 ++-- .../next/TemplateListPage/TemplateCard.tsx | 26 +++++++ .../next/TemplateListPage/TemplateGroup.tsx | 70 +++++++++++++++++++ .../next/TemplateListPage/TemplateGroups.tsx | 37 ++++++++++ .../TemplateListPage/TemplateListPage.tsx | 38 ++++------ 5 files changed, 154 insertions(+), 29 deletions(-) create mode 100644 plugins/scaffolder/src/next/TemplateListPage/TemplateCard.tsx create mode 100644 plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx create mode 100644 plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx index 8ab9755ab2..b199abc8bb 100644 --- a/plugins/scaffolder/src/next/Router/Router.tsx +++ b/plugins/scaffolder/src/next/Router/Router.tsx @@ -29,13 +29,17 @@ import { useElementFilter } from '@backstage/core-plugin-api'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; export type RouterProps = { - TemplateCardComponent?: React.ComponentType<{ - template: TemplateEntityV1beta3; - }>; + components: { + TemplateCardComponent?: React.ComponentType<{ + template: TemplateEntityV1beta3; + }>; + TaskPageComponent?: React.ComponentType<{}>; + }; }; export const Router = (props: PropsWithChildren) => { - const { TemplateCardComponent } = props; + const { components: { TaskPageComponent, TemplateCardComponent } = {} } = + props; const outlet = useOutlet() || props.children; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard.tsx new file mode 100644 index 0000000000..53ea12d4d6 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard.tsx @@ -0,0 +1,26 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; + +export interface TemplateCardProps { + template: TemplateEntityV1beta3; + deprecated?: boolean; +} + +export const TemplateCard = (props: TemplateCardProps) => { + return null; +}; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx new file mode 100644 index 0000000000..8af551d562 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx @@ -0,0 +1,70 @@ +/* + * 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 { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import React from 'react'; +import { + Content, + ContentHeader, + ItemCardGrid, + Link, +} from '@backstage/core-components'; +import { Typography } from '@material-ui/core'; +import { TemplateCard, TemplateCardProps } from './TemplateCard'; +import { stringifyEntityRef } from '@backstage/catalog-model'; + +export interface TemplateGroupProps { + templates: TemplateEntityV1beta3[]; + title?: string; + components?: { + titleComponent?: React.ReactNode; + CardComponent?: React.ComponentType; + }; +} + +export const TemplateGroup = (props: TemplateGroupProps) => { + const { + templates, + title, + components: { titleComponent, CardComponent } = {}, + } = props; + + if (templates.length === 0) { + return ( + + No templates found that match your filter. Learn more about{' '} + + adding templates + + . + + ); + } + + const titleFragment = titleComponent || ; + + const Card = CardComponent || TemplateCard; + + return ( + + {titleFragment} + + {templates.map(template => ( + + ))} + + + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx new file mode 100644 index 0000000000..17790729b1 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx @@ -0,0 +1,37 @@ +/* + * 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 { TemplateGroup } from './TemplateGroup'; +import { Entity } from '@backstage/catalog-model'; +import { useEntityList } from '@backstage/plugin-catalog-react'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; + +export type TemplateGroupFilter = { + title?: string; + titleComponent?: React.ReactNode; + filter: (entity: Entity) => boolean; +}; + +export interface TemplateGroupsProps { + groups: TemplateGroupFilter[]; + TemplateCardComponent?: React.ComponentType<{ + template: TemplateEntityV1beta3; + }>; +} + +export const TemplateGroups = (props: TemplateGroupsProps) => { + const { loading, error, entities } = useEntityList(); + return null; +}; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx index aab72f3cdf..497f78594c 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { Entity } from '@backstage/catalog-model'; + import { makeStyles } from '@material-ui/core'; import { Content, @@ -37,18 +37,13 @@ import { CategoryPicker } from './CategoryPicker'; import { RegisterExistingButton } from './RegisterExistingButton'; import { useRouteRef } from '@backstage/core-plugin-api'; import { registerComponentRouteRef } from '../../routes'; - -export type TemplateListGroup = { - title?: string; - titleComponent?: React.ReactNode; - filter: (entity: Entity) => boolean; -}; +import { TemplateGroupFilter, TemplateGroups } from './TemplateGroups'; export type TemplateListPageProps = { TemplateCardComponent?: React.ComponentType<{ template: TemplateEntityV1beta3; }>; - groups?: TemplateListGroup[]; + groups?: TemplateGroupFilter[]; }; const useStyles = makeStyles(theme => ({ @@ -60,20 +55,22 @@ const useStyles = makeStyles(theme => ({ }, })); +const defaultGroup: TemplateGroupFilter = { + title: 'All Templates', + filter: () => true, +}; + export const TemplateListPage = (props: TemplateListPageProps) => { const styles = useStyles(); const registerComponentLink = useRouteRef(registerComponentRouteRef); + const { TemplateCardComponent, groups = [defaultGroup] } = props; return (
- Create a New Component - - } + title="Create a New Component" subtitle="Create new software components using standard templates" /> @@ -101,19 +98,10 @@ export const TemplateListPage = (props: TemplateListPageProps) => {
- {/* {groups && - groups.map((group, index) => ( - - ))} - */} + />
From 7b133cd34ce64250106dbaec00c349331ced1749 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 25 Feb 2022 17:37:11 +0100 Subject: [PATCH 14/92] chore: reworking the types to be a little more user friendly Signed-off-by: blam --- .../src/next/TemplateListPage/TemplateGroup.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx index 8af551d562..389ec83231 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx @@ -27,19 +27,14 @@ import { stringifyEntityRef } from '@backstage/catalog-model'; export interface TemplateGroupProps { templates: TemplateEntityV1beta3[]; - title?: string; + title: React.ReactNode; components?: { - titleComponent?: React.ReactNode; CardComponent?: React.ComponentType; }; } export const TemplateGroup = (props: TemplateGroupProps) => { - const { - templates, - title, - components: { titleComponent, CardComponent } = {}, - } = props; + const { templates, title, components: { CardComponent } = {} } = props; if (templates.length === 0) { return ( @@ -53,13 +48,14 @@ export const TemplateGroup = (props: TemplateGroupProps) => { ); } - const titleFragment = titleComponent || ; + const titleComponent = + typeof title === 'string' ? : title; const Card = CardComponent || TemplateCard; return ( - {titleFragment} + {titleComponent} {templates.map(template => ( From 4ef2db835a9ab3f47ea65fa1fc54e2bbf690840a Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 25 Feb 2022 18:22:03 +0100 Subject: [PATCH 15/92] chore: starting to add tests for the TemplateGroups components Signed-off-by: blam --- .../TemplateListPage/TemplateGroups.test.tsx | 40 +++++++++++++++++++ .../next/TemplateListPage/TemplateGroups.tsx | 29 +++++++++++++- .../TemplateListPage.test.tsx | 39 ++++++++++-------- .../TemplateListPage/TemplateListPage.tsx | 1 - 4 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx new file mode 100644 index 0000000000..35f878a5d1 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +jest.mock('@backstage/plugin-catalog-react', () => ({ + useEntityList: jest.fn(), +})); + +import React from 'react'; +import { render } from '@testing-library/react'; +import { useEntityList } from '@backstage/plugin-catalog-react'; +import { TemplateGroups } from './TemplateGroups'; +import { TestApiProvider } from '@backstage/test-utils'; +import { errorApiRef } from '@backstage/core-plugin-api'; + +describe('TemplateGroups', () => { + it('should return progress if the hook is loading', async () => { + (useEntityList as jest.Mock).mockReturnValue({ loading: true }); + + const { findByTestId } = render( + + + , + ); + + expect(await findByTestId('progress')).toBeInTheDocument(); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx index 17790729b1..59abe8c34c 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx @@ -13,14 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import React from 'react'; import { TemplateGroup } from './TemplateGroup'; import { Entity } from '@backstage/catalog-model'; import { useEntityList } from '@backstage/plugin-catalog-react'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { Progress, Link } from '@backstage/core-components'; +import { Typography } from '@material-ui/core'; +import { errorApiRef, useApi } from '@backstage/core-plugin-api'; export type TemplateGroupFilter = { - title?: string; - titleComponent?: React.ReactNode; + title?: React.ReactNode; filter: (entity: Entity) => boolean; }; @@ -33,5 +36,27 @@ export interface TemplateGroupsProps { export const TemplateGroups = (props: TemplateGroupsProps) => { const { loading, error, entities } = useEntityList(); + const errorApi = useApi(errorApiRef); + + if (loading) { + return ; + } + + if (error) { + errorApi.post(error); + } + + if (!entities) { + return ( + + No templates found that match your filter. Learn more about{' '} + + adding templates + + . + + ); + } + return null; }; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx index 1f6c60237c..7aee8cc397 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx @@ -28,11 +28,29 @@ import React from 'react'; import { TemplateListPage } from './TemplateListPage'; describe('TemplateListPage', () => { + const mockCatalogApi = { + getEntities: async () => ({ + items: [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'blob', tags: ['blob'] }, + spec: { + type: 'service', + }, + }, + ], + }), + getEntityFacets: async () => ({ + facets: { 'spec.type': [{ value: 'service', count: 1 }] }, + }), + }; + it('should render the search bar for templates', async () => { const { getByPlaceholderText } = await renderInTestApp( { const { getByRole } = await renderInTestApp( { const { getByText } = await renderInTestApp( { const { getByText } = await renderInTestApp( ({ - items: [ - { - apiVersion: 'scaffolder.backstage.io/v1beta3', - kind: 'Template', - metadata: { name: 'blob', tags: ['blob'] }, - }, - ], - }), - }, - ], + [catalogApiRef, mockCatalogApi], [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx index 497f78594c..9f7961a077 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx @@ -22,7 +22,6 @@ import { Content, ContentHeader, Header, - Lifecycle, Page, SupportButton, } from '@backstage/core-components'; From 71543c9a91d3d4141d94a1c4194d7d590246d5a0 Mon Sep 17 00:00:00 2001 From: blam Date: Sun, 27 Feb 2022 17:21:53 +0100 Subject: [PATCH 16/92] chore: added some more tests for the TemplateGroups Signed-off-by: blam --- .../TemplateListPage/TemplateGroups.test.tsx | 132 ++++++++++++++++++ .../next/TemplateListPage/TemplateGroups.tsx | 19 ++- 2 files changed, 149 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx index 35f878a5d1..2d8cdc3150 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx @@ -18,12 +18,17 @@ jest.mock('@backstage/plugin-catalog-react', () => ({ useEntityList: jest.fn(), })); +jest.mock('./TemplateGroup', () => ({ + TemplateGroup: jest.fn(() => null), +})); + import React from 'react'; import { render } from '@testing-library/react'; import { useEntityList } from '@backstage/plugin-catalog-react'; import { TemplateGroups } from './TemplateGroups'; import { TestApiProvider } from '@backstage/test-utils'; import { errorApiRef } from '@backstage/core-plugin-api'; +import { TemplateGroup } from './TemplateGroup'; describe('TemplateGroups', () => { it('should return progress if the hook is loading', async () => { @@ -37,4 +42,131 @@ describe('TemplateGroups', () => { expect(await findByTestId('progress')).toBeInTheDocument(); }); + + it('should use the error api if there is an error with the retrieval of entitylist', async () => { + const mockError = new Error('tings went poop'); + (useEntityList as jest.Mock).mockReturnValue({ + error: mockError, + }); + const errorApi = { + post: jest.fn(), + }; + render( + + + , + ); + + expect(errorApi.post).toHaveBeenCalledWith(mockError); + }); + + it('should return a no templates message if entities is unset', async () => { + (useEntityList as jest.Mock).mockReturnValue({ + entities: null, + loading: false, + error: null, + }); + + const { findByText } = render( + + + , + ); + + expect(await findByText(/No templates found/)).toBeInTheDocument(); + }); + + it('should return a no templates message if entities has no values in it', async () => { + (useEntityList as jest.Mock).mockReturnValue({ + entities: [], + loading: false, + error: null, + }); + + const { findByText } = render( + + + , + ); + + expect(await findByText(/No templates found/)).toBeInTheDocument(); + }); + + it('should call the template group with the components', async () => { + const mockEntities = [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 't1', + }, + spec: {}, + }, + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 't2', + }, + spec: {}, + }, + ]; + + (useEntityList as jest.Mock).mockReturnValue({ + entities: mockEntities, + loading: false, + error: null, + }); + + render( + + true }]} /> + , + ); + + expect(TemplateGroup).toHaveBeenCalledWith( + expect.objectContaining({ templates: mockEntities }), + {}, + ); + }); + + it('should apply the filter for each group', async () => { + const mockEntities = [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 't1', + }, + spec: {}, + }, + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 't2', + }, + spec: {}, + }, + ]; + + (useEntityList as jest.Mock).mockReturnValue({ + entities: mockEntities, + loading: false, + error: null, + }); + + render( + + e.metadata.name === 't1' }]} + /> + , + ); + + expect(TemplateGroup).toHaveBeenCalledWith( + expect.objectContaining({ templates: [mockEntities[0]] }), + {}, + ); + }); }); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx index 59abe8c34c..6b85270f9c 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx @@ -36,6 +36,7 @@ export interface TemplateGroupsProps { export const TemplateGroups = (props: TemplateGroupsProps) => { const { loading, error, entities } = useEntityList(); + const { groups, TemplateCardComponent } = props; const errorApi = useApi(errorApiRef); if (loading) { @@ -44,9 +45,10 @@ export const TemplateGroups = (props: TemplateGroupsProps) => { if (error) { errorApi.post(error); + return null; } - if (!entities) { + if (!entities || !entities.length) { return ( No templates found that match your filter. Learn more about{' '} @@ -58,5 +60,18 @@ export const TemplateGroups = (props: TemplateGroupsProps) => { ); } - return null; + return ( + <> + {groups.map(({ title, filter }, index) => ( + + filter(e), + )} + title={title} + components={{ CardComponent: TemplateCardComponent }} + /> + ))} + + ); }; From e8134f3536f46f15b7a2c5c037f7496be5169514 Mon Sep 17 00:00:00 2001 From: blam Date: Sun, 27 Feb 2022 17:43:55 +0100 Subject: [PATCH 17/92] chore: added a break as to where we are at now Signed-off-by: blam --- .../TemplateListPage/TemplateGroup.test.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx new file mode 100644 index 0000000000..f6c4e99183 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx @@ -0,0 +1,18 @@ +/* + * 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. + */ +describe('TemplateGroup', () => { + // this is where we are at now +}); From 447f665bd71cdbc0025fa6ee51a7ec552f14b8bd Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 2 Mar 2022 14:30:30 +0100 Subject: [PATCH 18/92] chore: added some more tests Signed-off-by: blam --- .../TemplateListPage/TemplateGroup.test.tsx | 96 ++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx index f6c4e99183..0644e23e37 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx @@ -13,6 +13,100 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +jest.mock('./TemplateCard', () => ({ TemplateCard: jest.fn(() => null) })); + +import React from 'react'; +import { TemplateGroup } from './TemplateGroup'; +import { render } from '@testing-library/react'; +import { TemplateCard } from './TemplateCard'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; + describe('TemplateGroup', () => { - // this is where we are at now + it('should return a message when no templates are passed in', async () => { + const { getByText } = render(); + + expect( + getByText(/No templates found that match your filter/), + ).toBeInTheDocument(); + }); + + it('should render a card for each template with the template being passed as a prop', () => { + const mockTemplates: TemplateEntityV1beta3[] = [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'test' }, + spec: { + parameters: [], + steps: [], + type: 'website', + }, + }, + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'test2' }, + spec: { + parameters: [], + steps: [], + type: 'service', + }, + }, + ]; + + render(); + + expect(TemplateCard).toHaveBeenCalledTimes(2); + + for (const template of mockTemplates) { + expect(TemplateCard).toHaveBeenCalledWith( + expect.objectContaining({ template }), + {}, + ); + } + }); + + it('should use the passed in TemplateCard prop to render the template card', () => { + const mockTemplateCardComponent = jest.fn(() => null); + + const mockTemplates: TemplateEntityV1beta3[] = [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'test' }, + spec: { + parameters: [], + steps: [], + type: 'website', + }, + }, + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'test2' }, + spec: { + parameters: [], + steps: [], + type: 'service', + }, + }, + ]; + + render( + , + ); + + expect(mockTemplateCardComponent).toHaveBeenCalledTimes(2); + + for (const template of mockTemplates) { + expect(mockTemplateCardComponent).toHaveBeenCalledWith( + expect.objectContaining({ template }), + {}, + ); + } + }); }); From 1e323ab305046c4e0f2718b3b94832f171cf4111 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 09:56:16 +0100 Subject: [PATCH 19/92] chore: added some more tests for the start of the template Card Signed-off-by: blam --- .../next/TemplateListPage/CategoryPicker.tsx | 4 ++ .../TemplateCard/CardHeader.test.tsx | 39 ++++++++++++++ .../TemplateCard/CardHeader.tsx | 54 +++++++++++++++++++ .../TemplateCard/TemplateCard.tsx | 43 +++++++++++++++ .../index.ts} | 12 +---- .../TemplateListPage/TemplateGroup.test.tsx | 31 +++++++++++ .../next/TemplateListPage/TemplateGroup.tsx | 22 ++++---- 7 files changed, 184 insertions(+), 21 deletions(-) create mode 100644 plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx create mode 100644 plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx create mode 100644 plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx rename plugins/scaffolder/src/next/TemplateListPage/{TemplateCard.tsx => TemplateCard/index.ts} (70%) diff --git a/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx b/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx index 94f0df2565..483751f2e2 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx @@ -34,6 +34,10 @@ import { alertApiRef, useApi } from '@backstage/core-plugin-api'; const icon = ; const checkedIcon = ; +/** + * The Category Picker that is rendered on the left side for picking + * categories and filtering the template list. + */ export const CategoryPicker = () => { const alertApi = useApi(alertApiRef); const { error, loading, availableTypes, selectedTypes, setSelectedTypes } = diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx new file mode 100644 index 0000000000..460a41e092 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx @@ -0,0 +1,39 @@ +/* + * 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 from 'react'; +import { render } from '@testing-library/react'; +import { CardHeader } from './CardHeader'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; + +describe('CardHeader', () => { + it('should select the correct theme from the theme provider from the header', () => { + // Can't really test what we want here. + // But we can check that we call the getPage theme with the right type of template at least. + const mockTheme = { + ...lightTheme, + getPageTheme: jest.fn(lightTheme.getPageTheme), + }; + + const { container } = render( + + + , + ); + + expect(mockTheme.getPageTheme).toHaveBeenCalledWith({ themeId: 'service' }); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx new file mode 100644 index 0000000000..da1a1cb661 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx @@ -0,0 +1,54 @@ +/* + * 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 from 'react'; +import { makeStyles, useTheme } from '@material-ui/core'; +import { ItemCardHeader } from '@backstage/core-components'; +import { BackstageTheme } from '@backstage/theme'; + +const useStyles = makeStyles<{}, { cardBackgroundImage: string }>(theme => ({ + header: { + backgroundImage: ({ cardBackgroundImage }) => cardBackgroundImage, + }, +})); + +/** + * Props for the CardHeader component + */ +export interface CardHeaderProps { + type?: string; + title: string; +} + +/** + * The Card Header with the background for the TemplateCard. + */ +export const CardHeader = (props: CardHeaderProps) => { + const { type = 'other', title } = props; + const { getPageTheme } = useTheme(); + const themeForType = getPageTheme({ themeId: type }); + const styles = useStyles({ + cardBackgroundImage: themeForType.backgroundImage, + }); + + return ( + + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx new file mode 100644 index 0000000000..86cb5dc050 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx @@ -0,0 +1,43 @@ +/* + * 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 from 'react'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { Card } from '@material-ui/core'; +import { CardHeader } from './CardHeader'; +/** + * The Props for the Template Card component + * @public + */ +export interface TemplateCardProps { + template: TemplateEntityV1beta3; + deprecated?: boolean; +} + +/** + * The Template Card component that is rendered in a list for each template + * @public + */ +export const TemplateCard = (props: TemplateCardProps) => { + const { template } = props; + return ( + + + + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/index.ts similarity index 70% rename from plugins/scaffolder/src/next/TemplateListPage/TemplateCard.tsx rename to plugins/scaffolder/src/next/TemplateListPage/TemplateCard/index.ts index 53ea12d4d6..738c3fc5d4 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/index.ts @@ -13,14 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; - -export interface TemplateCardProps { - template: TemplateEntityV1beta3; - deprecated?: boolean; -} - -export const TemplateCard = (props: TemplateCardProps) => { - return null; -}; +export { TemplateCard } from './TemplateCard'; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx index 0644e23e37..59c6989c0d 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx @@ -109,4 +109,35 @@ describe('TemplateGroup', () => { ); } }); + + it('should render the title when no templates passed', () => { + const { getByText } = render(); + expect(getByText('Test')).toBeInTheDocument(); + }); + + it('should render the title when there are templates in the list', () => { + const mockTemplates: TemplateEntityV1beta3[] = [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'test' }, + spec: { parameters: [], steps: [], type: 'website' }, + }, + ]; + + const { getByText } = render( + , + ); + + expect(getByText('Test')).toBeInTheDocument(); + }); + + it('should allow for passing through a user given title component', () => { + const TitleComponent =

Im a custom header

; + const { getByText } = render( + , + ); + + expect(getByText('Im a custom header')).toBeInTheDocument(); + }); }); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx index 389ec83231..4344c195ae 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx @@ -35,22 +35,24 @@ export interface TemplateGroupProps { export const TemplateGroup = (props: TemplateGroupProps) => { const { templates, title, components: { CardComponent } = {} } = props; + const titleComponent = + typeof title === 'string' ? : title; if (templates.length === 0) { return ( - - No templates found that match your filter. Learn more about{' '} - - adding templates - - . - + + {titleComponent} + + No templates found that match your filter. Learn more about{' '} + + adding templates + + . + + ); } - const titleComponent = - typeof title === 'string' ? : title; - const Card = CardComponent || TemplateCard; return ( From 1850d45b8cebae3f1fa92f7e67eef2c46c933e3e Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 4 Mar 2022 08:36:12 +0100 Subject: [PATCH 20/92] chore: added a favorite icon back to the card header with flex Signed-off-by: blam --- app-config.yaml | 3 ++ .../TemplateCard/CardHeader.tsx | 51 ++++++++++++++----- .../TemplateCard/TemplateCard.tsx | 5 +- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 597073fede..b8506429b4 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -284,6 +284,9 @@ catalog: - type: file target: ../../cypress/e2e-fixture.catalog.info.yaml + - type: file + target: '../../template-test.yaml' + - type: url target: https://github.com/benjdlambert/software-templates/blob/main/scaffolder-templates/react-ssr-template/template.yaml scaffolder: diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx index da1a1cb661..42dec6f87f 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx @@ -15,39 +15,66 @@ */ import React from 'react'; -import { makeStyles, useTheme } from '@material-ui/core'; -import { ItemCardHeader } from '@backstage/core-components'; +import { + Link, + makeStyles, + Tooltip, + Typography, + useTheme, +} from '@material-ui/core'; +import { ItemCardHeader, WarningIcon } from '@backstage/core-components'; import { BackstageTheme } from '@backstage/theme'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { FavoriteEntity } from '@backstage/plugin-catalog-react'; -const useStyles = makeStyles<{}, { cardBackgroundImage: string }>(theme => ({ - header: { - backgroundImage: ({ cardBackgroundImage }) => cardBackgroundImage, - }, -})); +const useStyles = makeStyles( + theme => ({ + header: { + backgroundImage: ({ cardBackgroundImage }) => cardBackgroundImage, + }, + subtitleWrapper: { + display: 'flex', + justifyContent: 'space-between', + }, + }), +); /** * Props for the CardHeader component */ export interface CardHeaderProps { - type?: string; - title: string; + template: TemplateEntityV1beta3; } /** * The Card Header with the background for the TemplateCard. */ export const CardHeader = (props: CardHeaderProps) => { - const { type = 'other', title } = props; + const { + template: { + metadata: { title, name }, + spec: { type }, + }, + } = props; const { getPageTheme } = useTheme(); const themeForType = getPageTheme({ themeId: type }); const styles = useStyles({ cardBackgroundImage: themeForType.backgroundImage, }); + const SubtitleComponent = ( +
+
{type}
+
+ +
+
+ ); + return ( ); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx index 86cb5dc050..60860c567b 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx @@ -34,10 +34,7 @@ export const TemplateCard = (props: TemplateCardProps) => { const { template } = props; return ( - + ); }; From 2624e676dcea65333f611385e53626975581d9c9 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 8 Mar 2022 13:50:08 +0100 Subject: [PATCH 21/92] chore: save the current state Signed-off-by: blam --- .../TemplateCard/CardHeader.test.tsx | 14 ++++++++++++-- .../TemplateListPage/TemplateCard/CardHeader.tsx | 10 ++-------- .../next/TemplateListPage/TemplateCard/index.ts | 1 + .../next/TemplateWizardPage/TemplateWizardPage.tsx | 7 ++++++- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx index 460a41e092..eeac119810 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx @@ -28,9 +28,19 @@ describe('CardHeader', () => { getPageTheme: jest.fn(lightTheme.getPageTheme), }; - const { container } = render( + render( - + , ); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx index 42dec6f87f..2e1fdeac00 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx @@ -15,14 +15,8 @@ */ import React from 'react'; -import { - Link, - makeStyles, - Tooltip, - Typography, - useTheme, -} from '@material-ui/core'; -import { ItemCardHeader, WarningIcon } from '@backstage/core-components'; +import { makeStyles, useTheme } from '@material-ui/core'; +import { ItemCardHeader } from '@backstage/core-components'; import { BackstageTheme } from '@backstage/theme'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { FavoriteEntity } from '@backstage/plugin-catalog-react'; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/index.ts b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/index.ts index 738c3fc5d4..99af0cbb50 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/index.ts +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export { TemplateCard } from './TemplateCard'; +export type { TemplateCardProps } from './TemplateCard'; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx index 94f592eb60..fb5f49b9f8 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -13,4 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export const TemplateWizardPage = () => {}; +export interface TemplateWizardPageProps { + customFieldExtensions: any; +} +export const TemplateWizardPage = (props: TemplateWizardPageProps) => { + return null; +}; From 8e638059917c0b820149f2d8fdbc565df77fc139 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 8 Mar 2022 14:12:47 +0100 Subject: [PATCH 22/92] chore: start adding tests Signed-off-by: blam --- .../TemplateCard/CardHeader.test.tsx | 18 ++++++++++++++++++ .../TemplateCard/CardHeader.tsx | 1 + 2 files changed, 19 insertions(+) diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx index eeac119810..4f227be508 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx @@ -46,4 +46,22 @@ describe('CardHeader', () => { expect(mockTheme.getPageTheme).toHaveBeenCalledWith({ themeId: 'service' }); }); + + it('should render the type', () => { + const { getByText } = render( + , + ); + + expect(getByText('Service')).toBeInTheDocument(); + }); }); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx index 2e1fdeac00..db134bb18e 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx @@ -52,6 +52,7 @@ export const CardHeader = (props: CardHeaderProps) => { } = props; const { getPageTheme } = useTheme(); const themeForType = getPageTheme({ themeId: type }); + const styles = useStyles({ cardBackgroundImage: themeForType.backgroundImage, }); From 04a2821d83b5fcf40a74c95ecec1cd1e86205f80 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 8 Mar 2022 14:56:10 +0100 Subject: [PATCH 23/92] chore: added another output for thi Signed-off-by: blam --- .../TemplateCard/CardHeader.test.tsx | 71 +++++++++++++------ .../TemplateListPage.test.tsx | 4 +- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx index 4f227be508..1fe55b7b02 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx @@ -18,6 +18,13 @@ import { render } from '@testing-library/react'; import { CardHeader } from './CardHeader'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; +import { + MockStorageApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; +import { starredEntitiesApiRef } from '@backstage/plugin-catalog-react'; +import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; describe('CardHeader', () => { it('should select the correct theme from the theme provider from the header', () => { @@ -29,7 +36,47 @@ describe('CardHeader', () => { }; render( - + + + + + , + ); + + expect(mockTheme.getPageTheme).toHaveBeenCalledWith({ themeId: 'service' }); + }); + + it('should render the type', async () => { + const { getByText } = await renderInTestApp( + { }, }} /> - , +
, ); - expect(mockTheme.getPageTheme).toHaveBeenCalledWith({ themeId: 'service' }); - }); - - it('should render the type', () => { - const { getByText } = render( - , - ); - - expect(getByText('Service')).toBeInTheDocument(); + expect(getByText('service')).toBeInTheDocument(); }); }); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx index 7aee8cc397..4f12bde863 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; import { catalogApiRef, - DefaultStarredEntitiesApi, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; import { permissionApiRef } from '@backstage/plugin-permission-react'; @@ -110,7 +110,7 @@ describe('TemplateListPage', () => { expect(getByText('Categories')).toBeInTheDocument(); }); - it('should render the EntityTag picker', async () => { + it.skip('should render the EntityTag picker', async () => { const { getByText } = await renderInTestApp( Date: Wed, 9 Mar 2022 16:30:18 +0100 Subject: [PATCH 24/92] chore: added some more tests to favoriting Signed-off-by: blam --- .../FavoriteEntity/FavoriteEntity.tsx | 1 + .../TemplateCard/CardHeader.test.tsx | 94 ++++++++++++++++++- .../TemplateCard/CardHeader.tsx | 2 +- 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx b/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx index e055f4056f..163d819a35 100644 --- a/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx +++ b/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx @@ -43,6 +43,7 @@ export const FavoriteEntity = (props: FavoriteEntityProps) => { ); return ( toggleStarredEntity()} diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx index 1fe55b7b02..287d813ca7 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { render } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import { CardHeader } from './CardHeader'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; @@ -25,6 +25,9 @@ import { } from '@backstage/test-utils'; import { starredEntitiesApiRef } from '@backstage/plugin-catalog-react'; import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; +import Observable from 'zen-observable'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; describe('CardHeader', () => { it('should select the correct theme from the theme provider from the header', () => { @@ -93,4 +96,93 @@ describe('CardHeader', () => { expect(getByText('service')).toBeInTheDocument(); }); + + it('should enable favoriting of the entity', async () => { + const starredEntitiesApi = { + starredEntitie$: () => new Observable(() => {}), + toggleStarred: jest.fn(async () => {}), + }; + + const mockTemplate: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'bob' }, + spec: { + steps: [], + type: 'service', + }, + }; + + const { getByRole } = await renderInTestApp( + + + , + ); + + const favorite = getByRole('button', { name: 'favorite' }); + + await fireEvent.click(favorite); + + expect(starredEntitiesApi.toggleStarred).toHaveBeenCalledWith( + stringifyEntityRef(mockTemplate), + ); + }); + + it('should render the name of the entity', async () => { + const { getByText } = await renderInTestApp( + + + , + ); + + expect(getByText('bob')).toBeInTheDocument(); + }); + + it('should render the title of the entity in favor of the name if it is provided', async () => { + const { getByText } = await renderInTestApp( + + + , + ); + + expect(getByText('Iamtitle')).toBeInTheDocument(); + }); }); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx index db134bb18e..3a5785a4f6 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx @@ -22,7 +22,7 @@ import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { FavoriteEntity } from '@backstage/plugin-catalog-react'; const useStyles = makeStyles( - theme => ({ + () => ({ header: { backgroundImage: ({ cardBackgroundImage }) => cardBackgroundImage, }, From 910e34dd936c0bffe5caf1477c0601ce5ef3b2d4 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 10 Mar 2022 18:09:52 +0100 Subject: [PATCH 25/92] chore: making the TemplateCards pretty Signed-off-by: blam --- plugins/scaffolder/src/next/Router/Router.tsx | 12 ++- .../TemplateCard/TemplateCard.tsx | 87 ++++++++++++++++++- 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx index b199abc8bb..5aa55440e6 100644 --- a/plugins/scaffolder/src/next/Router/Router.tsx +++ b/plugins/scaffolder/src/next/Router/Router.tsx @@ -27,19 +27,20 @@ import { import { useElementFilter } from '@backstage/core-plugin-api'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups'; export type RouterProps = { - components: { + components?: { TemplateCardComponent?: React.ComponentType<{ template: TemplateEntityV1beta3; }>; TaskPageComponent?: React.ComponentType<{}>; }; + groups?: TemplateGroupFilter[]; }; export const Router = (props: PropsWithChildren) => { - const { components: { TaskPageComponent, TemplateCardComponent } = {} } = - props; + const { components: { TemplateCardComponent } = {} } = props; const outlet = useOutlet() || props.children; @@ -68,7 +69,10 @@ export const Router = (props: PropsWithChildren) => { + } /> diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx index 60860c567b..53e6f0320f 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx @@ -15,8 +15,64 @@ */ import React from 'react'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { Card } from '@material-ui/core'; +import { + Box, + Card, + CardActions, + CardContent, + Chip, + Divider, + makeStyles, +} from '@material-ui/core'; import { CardHeader } from './CardHeader'; +import { MarkdownContent, UserIcon, Button } from '@backstage/core-components'; +import { RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { + EntityRefLinks, + getEntityRelations, +} from '@backstage/plugin-catalog-react'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import { selectedTemplateRouteRef } from '../../../routes'; +import { BackstageTheme } from '@backstage/theme'; + +const useStyles = makeStyles(theme => ({ + box: { + overflow: 'hidden', + textOverflow: 'ellipsis', + display: '-webkit-box', + '-webkit-line-clamp': 10, + '-webkit-box-orient': 'vertical', + /** to make the styles for React Markdown not leak into the description */ + '& p:first-child': { + marginTop: 0, + marginBottom: theme.spacing(2), + }, + }, + label: { + color: theme.palette.text.secondary, + textTransform: 'uppercase', + fontWeight: 'bold', + letterSpacing: 0.5, + lineHeight: 1, + fontSize: '0.75rem', + }, + margin: { + marginBottom: theme.spacing(2), + }, + footer: { + display: 'flex', + justifyContent: 'space-between', + flex: 1, + alignItems: 'center', + }, + ownedBy: { + display: 'flex', + alignItems: 'center', + flex: 1, + color: theme.palette.link, + }, +})); + /** * The Props for the Template Card component * @public @@ -32,9 +88,38 @@ export interface TemplateCardProps { */ export const TemplateCard = (props: TemplateCardProps) => { const { template } = props; + const styles = useStyles(); + const ownedByRelations = getEntityRelations(template, RELATION_OWNED_BY); + const templateRoute = useRouteRef(selectedTemplateRouteRef); + const href = templateRoute({ templateName: template.metadata.name }); + return ( + + + + + + + {template.metadata.tags?.map(tag => ( + + ))} + + + +
+
+ + +
+ +
+
); }; From 412877f94357c42e993824b79a043b65056ce95b Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 11 Mar 2022 13:29:22 +0100 Subject: [PATCH 26/92] chore: added final test for the template card Signed-off-by: blam --- .../TemplateCard/TemplateCard.test.tsx | 249 ++++++++++++++++++ .../TemplateCard/TemplateCard.tsx | 27 +- 2 files changed, 268 insertions(+), 8 deletions(-) create mode 100644 plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx new file mode 100644 index 0000000000..088727a5a0 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx @@ -0,0 +1,249 @@ +/* + * 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 { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; +import { + entityRouteRef, + starredEntitiesApiRef, +} from '@backstage/plugin-catalog-react'; +import { + MockStorageApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; +import { TemplateCard } from './TemplateCard'; +import React from 'react'; +import { rootRouteRef } from '../../../routes'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { RELATION_OWNED_BY } from '@backstage/catalog-model'; + +describe('TemplateCard', () => { + it('should render the card title', async () => { + const mockTemplate: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'bob' }, + spec: { + steps: [], + type: 'service', + }, + }; + + const { getByText } = await renderInTestApp( + + + , + { mountedRoutes: { '/': rootRouteRef } }, + ); + + expect(getByText('bob')).toBeInTheDocument(); + }); + + it('should render the description as markdown', async () => { + const mockTemplate: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'bob', description: 'hello **test**' }, + spec: { + steps: [], + type: 'service', + }, + }; + + const { getByText } = await renderInTestApp( + + + , + { mountedRoutes: { '/': rootRouteRef } }, + ); + + const description = getByText('hello'); + expect(description.querySelector('strong')).toBeInTheDocument(); + }); + + it('should render no descroption if none is provided through the template', async () => { + const mockTemplate: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'bob' }, + spec: { + steps: [], + type: 'service', + }, + }; + + const { getByText } = await renderInTestApp( + + + , + { mountedRoutes: { '/': rootRouteRef } }, + ); + + expect(getByText('No description')).toBeInTheDocument(); + }); + + it('should render the tags', async () => { + const mockTemplate: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'bob', tags: ['cpp', 'react'] }, + spec: { + steps: [], + type: 'service', + }, + }; + + const { getByText } = await renderInTestApp( + + + , + { mountedRoutes: { '/': rootRouteRef } }, + ); + + for (const tag of mockTemplate.metadata.tags!) { + expect(getByText(tag)).toBeInTheDocument(); + } + }); + + it('should render a link to the owner', async () => { + const mockTemplate: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'bob', tags: ['cpp', 'react'] }, + spec: { + steps: [], + type: 'service', + }, + relations: [ + { + targetRef: 'group:default/my-test-user', + type: RELATION_OWNED_BY, + /** remove when target is removed as it's deprecated */ + target: { kind: 'User', name: 'my-test-user', namespace: 'default' }, + }, + ], + }; + + const { getByRole } = await renderInTestApp( + + + , + { + mountedRoutes: { + '/': rootRouteRef, + '/catalog/:kind/:namespace/:name': entityRouteRef, + }, + }, + ); + + expect(getByRole('link', { name: 'my-test-user' })).toBeInTheDocument(); + expect(getByRole('link', { name: 'my-test-user' })).toHaveAttribute( + 'href', + '/catalog/group/default/my-test-user', + ); + }); + + it('should render the choose button to navigate to the selected template', async () => { + const mockTemplate: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'bob', tags: ['cpp', 'react'] }, + spec: { + steps: [], + type: 'service', + }, + relations: [ + { + targetRef: 'group:default/my-test-user', + type: RELATION_OWNED_BY, + /** remove when target is removed as it's deprecated */ + target: { kind: 'User', name: 'my-test-user', namespace: 'default' }, + }, + ], + }; + + const { getByRole } = await renderInTestApp( + + + , + { + mountedRoutes: { + '/': rootRouteRef, + '/catalog/:kind/:namespace/:name': entityRouteRef, + }, + }, + ); + + expect(getByRole('button', { name: 'Choose' })).toBeInTheDocument(); + expect(getByRole('button', { name: 'Choose' })).toHaveAttribute( + 'href', + '/templates/bob', + ); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx index 53e6f0320f..26f74ac053 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx @@ -102,18 +102,29 @@ export const TemplateCard = (props: TemplateCardProps) => { content={template.metadata.description ?? 'No description'} /> - - - {template.metadata.tags?.map(tag => ( - - ))} - + {template.metadata.tags?.length ? ( + <> + + + {template.metadata.tags?.map(tag => ( + + ))} + + + ) : null}
- - + {ownedByRelations.length ? ( + <> + + + + ) : null}