diff --git a/.changeset/curly-cars-relax.md b/.changeset/curly-cars-relax.md new file mode 100644 index 0000000000..de1b4c3d89 --- /dev/null +++ b/.changeset/curly-cars-relax.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-search-react': patch +'@backstage/plugin-catalog': patch +--- + +Deprecate the old pattern of `create*Extension`, and replace it with the equivalent Blueprint implementation instead. diff --git a/plugins/catalog-react/api-report-alpha.md b/plugins/catalog-react/api-report-alpha.md index edfa183903..c5dc632946 100644 --- a/plugins/catalog-react/api-report-alpha.md +++ b/plugins/catalog-react/api-report-alpha.md @@ -6,16 +6,19 @@ /// import { AnyExtensionInputMap } from '@backstage/frontend-plugin-api'; +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { PortableSchema } from '@backstage/frontend-plugin-api'; +import { default as React_2 } from 'react'; import { ResolvedExtensionInputs } from '@backstage/frontend-plugin-api'; import { ResourcePermission } from '@backstage/plugin-permission-common'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/core-plugin-api/alpha'; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const catalogExtensionData: { entityContentTitle: ConfigurableExtensionDataRef< string, @@ -92,7 +95,7 @@ export const catalogReactTranslationRef: TranslationRef< } >; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export function createEntityCardExtension< TConfig extends { filter?: string; @@ -125,7 +128,7 @@ export function createEntityCardExtension< string | undefined >; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export function createEntityContentExtension< TInputs extends AnyExtensionInputMap, >(options: { @@ -164,6 +167,117 @@ export function createEntityContentExtension< string | undefined >; +// @alpha +export const EntityCardBlueprint: ExtensionBlueprint< + 'entity-card', + undefined, + undefined, + { + loader: () => Promise; + filter?: string | ((entity: Entity) => boolean) | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + { + filterFunction: ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + {} + >; + filterExpression: ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + {} + >; + } +>; + +// @alpha +export const EntityContentBlueprint: ExtensionBlueprint< + 'entity-content', + undefined, + undefined, + { + loader: () => Promise; + defaultPath: string; + defaultTitle: string; + routeRef?: RouteRef | undefined; + filter?: string | ((entity: Entity) => boolean) | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + { + path: string | undefined; + title: string | undefined; + filter: string | undefined; + }, + { + filter?: string | undefined; + title?: string | undefined; + path?: string | undefined; + }, + { + title: ConfigurableExtensionDataRef< + string, + 'catalog.entity-content-title', + {} + >; + filterFunction: ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + {} + >; + filterExpression: ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + {} + >; + } +>; + // @alpha export function isOwnerOf(owner: Entity, entity: Entity): boolean; diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index e6b6acae3a..ef5d924200 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -29,7 +29,7 @@ "sideEffects": false, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.tsx", + "./alpha": "./src/alpha/index.ts", "./package.json": "./package.json" }, "main": "src/index.ts", @@ -37,7 +37,7 @@ "typesVersions": { "*": { "alpha": [ - "src/alpha.tsx" + "src/alpha/index.ts" ], "package.json": [ "package.json" @@ -85,6 +85,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", + "@backstage/frontend-test-utils": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/test-utils": "workspace:^", diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx new file mode 100644 index 0000000000..db787766fd --- /dev/null +++ b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx @@ -0,0 +1,178 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { EntityCardBlueprint } from './EntityCardBlueprint'; +import { + coreExtensionData, + createExtension, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { waitFor, screen } from '@testing-library/react'; +import { Entity } from '@backstage/catalog-model'; + +describe('EntityCardBlueprint', () => { + it('should return an extension with sensible defaults', () => { + const extension = EntityCardBlueprint.make({ + name: 'test', + params: { + filter: 'has:labels', + loader: async () =>
im a card
, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "entity-content:catalog/overview", + "input": "cards", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "filter": { + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "entity-card", + "name": "test", + "namespace": undefined, + "output": [ + [Function], + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "catalog.entity-filter-function", + "optional": [Function], + "toString": [Function], + }, + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "catalog.entity-filter-expression", + "optional": [Function], + "toString": [Function], + }, + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should output the correct filter output', () => { + const mockFilter = (_entity: Entity) => true; + + expect( + createExtensionTester( + EntityCardBlueprint.make({ + name: 'test', + params: { + loader: async () =>
Test!
, + filter: 'test', + }, + }), + ).data(EntityCardBlueprint.dataRefs.filterExpression), + ).toBe('test'); + + expect( + createExtensionTester( + EntityCardBlueprint.make({ + name: 'test', + params: { + loader: async () =>
Test!
, + }, + }), + { config: { filter: 'test' } }, + ).data(EntityCardBlueprint.dataRefs.filterExpression), + ).toBe('test'); + + expect( + createExtensionTester( + EntityCardBlueprint.make({ + name: 'test', + params: { + filter: mockFilter, + loader: async () =>
Test!
, + }, + }), + ).data(EntityCardBlueprint.dataRefs.filterFunction), + ).toBe(mockFilter); + }); + + it('should allow overriding config and inputs', async () => { + const extension = EntityCardBlueprint.makeWithOverrides({ + name: 'test', + inputs: { + mock: createExtensionInput([coreExtensionData.reactElement]), + }, + config: { + schema: { + mock: z => z.string(), + }, + }, + factory(originalFactory, { inputs, config }) { + return originalFactory({ + loader: async () => ( +
+ config: {config.mock} +
+ {inputs.mock.map((i, k) => ( +
{i.get(coreExtensionData.reactElement)}
+ ))} +
+
+ ), + }); + }, + }); + + const mockExtension = createExtension({ + attachTo: { id: 'entity-card:test', input: 'mock' }, + output: [coreExtensionData.reactElement], + factory() { + return [coreExtensionData.reactElement(
im a mock
)]; + }, + }); + + createExtensionTester(extension, { config: { mock: 'mock test config' } }) + .add(mockExtension) + .render(); + + await waitFor(() => { + expect(screen.getByTestId('test')).toBeInTheDocument(); + expect(screen.getByTestId('test')).toHaveTextContent( + 'config: mock test config', + ); + expect(screen.getByTestId('contents')).toHaveTextContent('im a mock'); + }); + }); +}); diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.tsx new file mode 100644 index 0000000000..c3153b6059 --- /dev/null +++ b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { lazy } from 'react'; +import { + ExtensionBoundary, + coreExtensionData, + createExtensionBlueprint, +} from '@backstage/frontend-plugin-api'; +import { catalogExtensionData } from '../extensions'; + +/** + * @alpha + * A blueprint for creating cards for the entity pages in the catalog. + */ +export const EntityCardBlueprint = createExtensionBlueprint({ + kind: 'entity-card', + attachTo: { id: 'entity-content:catalog/overview', input: 'cards' }, + output: [ + coreExtensionData.reactElement, + catalogExtensionData.entityFilterFunction.optional(), + catalogExtensionData.entityFilterExpression.optional(), + ], + dataRefs: { + filterFunction: catalogExtensionData.entityFilterFunction, + filterExpression: catalogExtensionData.entityFilterExpression, + }, + config: { + schema: { + filter: z => z.string().optional(), + }, + }, + *factory( + { + loader, + filter, + }: { + loader: () => Promise; + filter?: + | typeof catalogExtensionData.entityFilterFunction.T + | typeof catalogExtensionData.entityFilterExpression.T; + }, + { node, config }, + ) { + const ExtensionComponent = lazy(() => + loader().then(element => ({ default: () => element })), + ); + + yield coreExtensionData.reactElement( + + + , + ); + + if (config.filter) { + yield catalogExtensionData.entityFilterExpression(config.filter); + } else if (typeof filter === 'string') { + yield catalogExtensionData.entityFilterExpression(filter); + } else if (typeof filter === 'function') { + yield catalogExtensionData.entityFilterFunction(filter); + } + }, +}); diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.test.tsx new file mode 100644 index 0000000000..dab6013bab --- /dev/null +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.test.tsx @@ -0,0 +1,226 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { EntityContentBlueprint } from './EntityContentBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { + coreExtensionData, + createExtension, + createExtensionInput, + createRouteRef, +} from '@backstage/frontend-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { waitFor, screen } from '@testing-library/react'; + +describe('EntityContentBlueprint', () => { + it('should return an extension with sane defaults', () => { + const extension = EntityContentBlueprint.make({ + name: 'test', + params: { + defaultPath: '/test', + defaultTitle: 'Test', + loader: async () =>
Test!
, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "page:catalog/entity", + "input": "contents", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "filter": { + "type": "string", + }, + "path": { + "type": "string", + }, + "title": { + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "entity-content", + "name": "test", + "namespace": undefined, + "output": [ + [Function], + [Function], + [Function], + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "core.routing.ref", + "optional": [Function], + "toString": [Function], + }, + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "catalog.entity-filter-function", + "optional": [Function], + "toString": [Function], + }, + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "catalog.entity-filter-expression", + "optional": [Function], + "toString": [Function], + }, + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should emit the correct defaults', () => { + const mockRouteRef = createRouteRef(); + const extension = EntityContentBlueprint.make({ + name: 'test', + params: { + defaultPath: '/test', + defaultTitle: 'Test', + routeRef: mockRouteRef, + loader: async () =>
Test!
, + }, + }); + + const tester = createExtensionTester(extension); + + // todo(blam): route paths are always set to / in the createExtensionTester. This will work eventually. + // expect(tester.data(coreExtensionData.routePath)).toBe('/test'); + + expect(tester.data(coreExtensionData.routeRef)).toBe(mockRouteRef); + expect(tester.data(EntityContentBlueprint.dataRefs.title)).toBe('Test'); + }); + + it('should emit the correct filter output', () => { + const mockFilter = (_entity: Entity) => true; + + expect( + createExtensionTester( + EntityContentBlueprint.make({ + name: 'test', + params: { + defaultPath: '/test', + defaultTitle: 'Test', + loader: async () =>
Test!
, + filter: 'test', + }, + }), + ).data(EntityContentBlueprint.dataRefs.filterExpression), + ).toBe('test'); + + expect( + createExtensionTester( + EntityContentBlueprint.make({ + name: 'test', + params: { + defaultPath: '/test', + defaultTitle: 'Test', + loader: async () =>
Test!
, + }, + }), + { config: { filter: 'test' } }, + ).data(EntityContentBlueprint.dataRefs.filterExpression), + ).toBe('test'); + + expect( + createExtensionTester( + EntityContentBlueprint.make({ + name: 'test', + params: { + defaultPath: '/test', + defaultTitle: 'Test', + filter: mockFilter, + loader: async () =>
Test!
, + }, + }), + ).data(EntityContentBlueprint.dataRefs.filterFunction), + ).toBe(mockFilter); + }); + + it('should allow overriding config and inputs', async () => { + const extension = EntityContentBlueprint.makeWithOverrides({ + name: 'test', + inputs: { + mock: createExtensionInput([coreExtensionData.reactElement]), + }, + config: { + schema: { + mock: z => z.string(), + }, + }, + factory(originalFactory, { inputs, config }) { + return originalFactory({ + defaultPath: '/test', + defaultTitle: 'Test', + loader: async () => ( +
+ config: {config.mock} +
+ {inputs.mock.map((i, k) => ( +
{i.get(coreExtensionData.reactElement)}
+ ))} +
+
+ ), + }); + }, + }); + + const mockExtension = createExtension({ + attachTo: { id: 'entity-content:test', input: 'mock' }, + output: [coreExtensionData.reactElement], + factory() { + return [coreExtensionData.reactElement(
im a mock
)]; + }, + }); + + createExtensionTester(extension, { config: { mock: 'mock test config' } }) + .add(mockExtension) + .render(); + + await waitFor(() => { + expect(screen.getByTestId('test')).toBeInTheDocument(); + expect(screen.getByTestId('test')).toHaveTextContent( + 'config: mock test config', + ); + expect(screen.getByTestId('contents')).toHaveTextContent('im a mock'); + }); + }); +}); diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.tsx new file mode 100644 index 0000000000..7c7a6c43b7 --- /dev/null +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.tsx @@ -0,0 +1,99 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { lazy } from 'react'; +import { + coreExtensionData, + createExtensionBlueprint, + ExtensionBoundary, + RouteRef, +} from '@backstage/frontend-plugin-api'; +import { catalogExtensionData } from '../extensions'; + +/** + * @alpha + * Creates an EntityContent extension. + */ +export const EntityContentBlueprint = createExtensionBlueprint({ + kind: 'entity-content', + attachTo: { id: 'page:catalog/entity', input: 'contents' }, + output: [ + coreExtensionData.reactElement, + coreExtensionData.routePath, + catalogExtensionData.entityContentTitle, + coreExtensionData.routeRef.optional(), + catalogExtensionData.entityFilterFunction.optional(), + catalogExtensionData.entityFilterExpression.optional(), + ], + dataRefs: { + title: catalogExtensionData.entityContentTitle, + filterFunction: catalogExtensionData.entityFilterFunction, + filterExpression: catalogExtensionData.entityFilterExpression, + }, + config: { + schema: { + path: z => z.string().optional(), + title: z => z.string().optional(), + filter: z => z.string().optional(), + }, + }, + *factory( + { + loader, + defaultPath, + defaultTitle, + filter, + routeRef, + }: { + loader: () => Promise; + defaultPath: string; + defaultTitle: string; + routeRef?: RouteRef; + filter?: + | typeof catalogExtensionData.entityFilterFunction.T + | typeof catalogExtensionData.entityFilterExpression.T; + }, + { node, config }, + ) { + const path = config.path ?? defaultPath; + const title = config.title ?? defaultTitle; + + const ExtensionComponent = lazy(() => + loader().then(element => ({ default: () => element })), + ); + + yield coreExtensionData.reactElement( + + + , + ); + + yield coreExtensionData.routePath(path); + + yield catalogExtensionData.entityContentTitle(title); + + if (routeRef) { + yield coreExtensionData.routeRef(routeRef); + } + + if (config.filter) { + yield catalogExtensionData.entityFilterExpression(config.filter); + } else if (typeof filter === 'string') { + yield catalogExtensionData.entityFilterExpression(filter); + } else if (typeof filter === 'function') { + yield catalogExtensionData.entityFilterFunction(filter); + } + }, +}); diff --git a/plugins/catalog/src/alpha.ts b/plugins/catalog-react/src/alpha/blueprints/index.ts similarity index 73% rename from plugins/catalog/src/alpha.ts rename to plugins/catalog-react/src/alpha/blueprints/index.ts index b78a5e6225..dccaf28cd9 100644 --- a/plugins/catalog/src/alpha.ts +++ b/plugins/catalog-react/src/alpha/blueprints/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * Copyright 2024 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,8 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -export * from './alpha/index'; -export { default } from './alpha/index'; -export { catalogTranslationRef } from './translation'; -export * from './translation'; +export { EntityCardBlueprint } from './EntityCardBlueprint'; +export { EntityContentBlueprint } from './EntityContentBlueprint'; diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha/extensions.tsx similarity index 92% rename from plugins/catalog-react/src/alpha.tsx rename to plugins/catalog-react/src/alpha/extensions.tsx index 42d69726b6..e527edc355 100644 --- a/plugins/catalog-react/src/alpha.tsx +++ b/plugins/catalog-react/src/alpha/extensions.tsx @@ -28,13 +28,16 @@ import { import React, { lazy } from 'react'; import { Entity } from '@backstage/catalog-model'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { Expand } from '../../../packages/frontend-plugin-api/src/types'; +import { Expand } from '../../../../packages/frontend-plugin-api/src/types'; -export { useEntityPermission } from './hooks/useEntityPermission'; -export { isOwnerOf } from './utils'; -export * from './translation'; +export { useEntityPermission } from '../hooks/useEntityPermission'; +export { isOwnerOf } from '../utils'; +export * from '../translation'; -/** @alpha */ +/** + * @alpha + * @deprecated use `dataRefs` property on either {@link EntityCardBlueprint} or {@link EntityContentBlueprint} instead + */ export const catalogExtensionData = { entityContentTitle: createExtensionDataRef().with({ id: 'catalog.entity-content-title', @@ -48,7 +51,10 @@ export const catalogExtensionData = { }; // TODO: Figure out how to merge with provided config schema -/** @alpha */ +/** + * @alpha + * @deprecated use {@link EntityCardBlueprint} instead + */ export function createEntityCardExtension< TConfig extends { filter?: string }, TInputs extends AnyExtensionInputMap, @@ -110,7 +116,10 @@ export function createEntityCardExtension< }); } -/** @alpha */ +/** + * @alpha + * @deprecated use {@link EntityContentBlueprint} instead + */ export function createEntityContentExtension< TInputs extends AnyExtensionInputMap, >(options: { diff --git a/plugins/catalog-react/src/alpha/index.ts b/plugins/catalog-react/src/alpha/index.ts new file mode 100644 index 0000000000..132b288008 --- /dev/null +++ b/plugins/catalog-react/src/alpha/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './blueprints'; +export * from './extensions'; diff --git a/plugins/catalog-react/src/setupTests.ts b/plugins/catalog-react/src/setupTests.ts index 963c0f188b..dae4360fd3 100644 --- a/plugins/catalog-react/src/setupTests.ts +++ b/plugins/catalog-react/src/setupTests.ts @@ -15,3 +15,18 @@ */ import '@testing-library/jest-dom'; + +// eslint-disable-next-line no-console +const originalConsoleWarn = console.warn; +// eslint-disable-next-line no-console +console.warn = (...args: any[]) => { + const message = args[0]; + if ( + typeof message === 'string' && + (message.includes('CSSOM.parse is not a function') || + message.includes('[JSS]')) + ) { + return; + } + originalConsoleWarn(...args); +}; diff --git a/plugins/catalog/api-report-alpha.md b/plugins/catalog/api-report-alpha.md index 5019fc25ea..399580d6a2 100644 --- a/plugins/catalog/api-report-alpha.md +++ b/plugins/catalog/api-report-alpha.md @@ -6,13 +6,35 @@ /// import { AnyExtensionInputMap } from '@backstage/frontend-plugin-api'; +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { PortableSchema } from '@backstage/frontend-plugin-api'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/core-plugin-api/alpha'; +// @alpha +export const CatalogFilterBlueprint: ExtensionBlueprint< + 'catalog-filter', + undefined, + undefined, + { + loader: () => Promise; + }, + ConfigurableExtensionDataRef, + {}, + {}, + {}, + never +>; + // @alpha (undocumented) export const catalogTranslationRef: TranslationRef< 'catalog', @@ -98,7 +120,7 @@ export const catalogTranslationRef: TranslationRef< } >; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export function createCatalogFilterExtension< TInputs extends AnyExtensionInputMap, TConfig, @@ -141,7 +163,344 @@ const _default: BackstagePlugin< }>; unregisterRedirect: ExternalRouteRef; }, - {} + { + 'entity-card:catalog/about': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'about' + >; + 'entity-card:catalog/links': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'links' + >; + 'entity-card:catalog/labels': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'labels' + >; + 'entity-card:catalog/depends-on-components': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'depends-on-components' + >; + 'entity-card:catalog/depends-on-resources': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'depends-on-resources' + >; + 'entity-card:catalog/has-components': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'has-components' + >; + 'entity-card:catalog/has-resources': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'has-resources' + >; + 'entity-card:catalog/has-subcomponents': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'has-subcomponents' + >; + 'entity-card:catalog/has-subdomains': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'has-subdomains' + >; + 'entity-card:catalog/has-systems': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'has-systems' + >; + 'entity-content:catalog/overview': ExtensionDefinition< + { + [x: string]: any; + } & { + path: string | undefined; + title: string | undefined; + filter: string | undefined; + }, + { + [x: string]: any; + } & { + filter?: string | undefined; + title?: string | undefined; + path?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + { + cards: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + } + >; + }, + 'entity-content', + undefined, + 'overview' + >; + } >; export default _default; diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index aaef0ab951..b8f8864f3c 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -29,7 +29,7 @@ "sideEffects": false, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", + "./alpha": "./src/alpha/index.ts", "./package.json": "./package.json" }, "main": "src/index.ts", @@ -37,7 +37,7 @@ "typesVersions": { "*": { "alpha": [ - "src/alpha.ts" + "src/alpha/index.ts" ], "package.json": [ "package.json" @@ -90,6 +90,7 @@ "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", + "@backstage/frontend-test-utils": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/dom": "^10.0.0", diff --git a/plugins/catalog/src/alpha/blueprints/CatalogFilterBlueprint.test.tsx b/plugins/catalog/src/alpha/blueprints/CatalogFilterBlueprint.test.tsx new file mode 100644 index 0000000000..9cf8b65957 --- /dev/null +++ b/plugins/catalog/src/alpha/blueprints/CatalogFilterBlueprint.test.tsx @@ -0,0 +1,104 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { CatalogFilterBlueprint } from './CatalogFilterBlueprint'; +import { + coreExtensionData, + createExtension, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { waitFor, screen } from '@testing-library/react'; + +describe('CatalogFilterBlueprint', () => { + it('should create an extension with sane defaults', () => { + const extension = CatalogFilterBlueprint.make({ + params: { + loader: async () =>
, + }, + }); + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "page:catalog", + "input": "filters", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "catalog-filter", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should allow overrding of inputs and config', async () => { + const extension = CatalogFilterBlueprint.makeWithOverrides({ + name: 'test-name', + inputs: { + mock: createExtensionInput([coreExtensionData.reactElement]), + }, + config: { + schema: { + test: z => z.string(), + }, + }, + factory(originalFactory, { config, inputs }) { + return originalFactory({ + loader: async () => ( +
+ config: {config.test} +
+ {inputs.mock.map((i, k) => ( +
{i.get(coreExtensionData.reactElement)}
+ ))} +
+
+ ), + }); + }, + }); + + const mockExtension = createExtension({ + attachTo: { id: 'catalog-filter:test-name', input: 'mock' }, + output: [coreExtensionData.reactElement], + factory() { + return [coreExtensionData.reactElement(
im a mock
)]; + }, + }); + + createExtensionTester(extension, { config: { test: 'mock test config' } }) + .add(mockExtension) + .render(); + + await waitFor(() => { + expect(screen.getByTestId('test')).toBeInTheDocument(); + expect(screen.getByTestId('test')).toHaveTextContent( + 'config: mock test config', + ); + expect(screen.getByTestId('contents')).toHaveTextContent('im a mock'); + }); + }); +}); diff --git a/plugins/catalog/src/alpha/blueprints/CatalogFilterBlueprint.tsx b/plugins/catalog/src/alpha/blueprints/CatalogFilterBlueprint.tsx new file mode 100644 index 0000000000..e8d6df55d2 --- /dev/null +++ b/plugins/catalog/src/alpha/blueprints/CatalogFilterBlueprint.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { lazy } from 'react'; +import { + ExtensionBoundary, + coreExtensionData, + createExtensionBlueprint, +} from '@backstage/frontend-plugin-api'; + +/** + * Creates Catalog Filter Extensions + * @alpha + */ +export const CatalogFilterBlueprint = createExtensionBlueprint({ + kind: 'catalog-filter', + attachTo: { id: 'page:catalog', input: 'filters' }, + output: [coreExtensionData.reactElement], + factory(params: { loader: () => Promise }, { node }) { + const ExtensionComponent = lazy(() => + params.loader().then(element => ({ default: () => element })), + ); + return [ + coreExtensionData.reactElement( + + + , + ), + ]; + }, +}); diff --git a/plugins/catalog/src/alpha/blueprints/index.ts b/plugins/catalog/src/alpha/blueprints/index.ts new file mode 100644 index 0000000000..a3a7bcb33f --- /dev/null +++ b/plugins/catalog/src/alpha/blueprints/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { CatalogFilterBlueprint } from './CatalogFilterBlueprint'; diff --git a/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx b/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx index d8a218f8f3..85d911ef3b 100644 --- a/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx +++ b/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx @@ -23,7 +23,10 @@ import { createExtension, } from '@backstage/frontend-plugin-api'; -/** @alpha */ +/** + * @alpha + * @deprecated Use {@link CatalogFilterBlueprint} instead + */ export function createCatalogFilterExtension< TInputs extends AnyExtensionInputMap, TConfig, diff --git a/plugins/catalog/src/alpha/entityCards.tsx b/plugins/catalog/src/alpha/entityCards.tsx index 8a18efabde..5f8ee16f87 100644 --- a/plugins/catalog/src/alpha/entityCards.tsx +++ b/plugins/catalog/src/alpha/entityCards.tsx @@ -15,96 +15,116 @@ */ import React from 'react'; -import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; +import { EntityCardBlueprint } from '@backstage/plugin-catalog-react/alpha'; import { compatWrapper } from '@backstage/core-compat-api'; -export const catalogAboutEntityCard = createEntityCardExtension({ +export const catalogAboutEntityCard = EntityCardBlueprint.make({ name: 'about', - loader: async () => - import('../components/AboutCard').then(m => - compatWrapper(), - ), + params: { + loader: async () => + import('../components/AboutCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogLinksEntityCard = createEntityCardExtension({ +export const catalogLinksEntityCard = EntityCardBlueprint.make({ name: 'links', - filter: 'has:links', - loader: async () => - import('../components/EntityLinksCard').then(m => - compatWrapper(), - ), + params: { + filter: 'has:links', + loader: async () => + import('../components/EntityLinksCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogLabelsEntityCard = createEntityCardExtension({ +export const catalogLabelsEntityCard = EntityCardBlueprint.make({ name: 'labels', - filter: 'has:labels', - loader: async () => - import('../components/EntityLabelsCard').then(m => - compatWrapper(), - ), + params: { + filter: 'has:labels', + loader: async () => + import('../components/EntityLabelsCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogDependsOnComponentsEntityCard = createEntityCardExtension({ +export const catalogDependsOnComponentsEntityCard = EntityCardBlueprint.make({ name: 'depends-on-components', - filter: 'kind:component', - loader: async () => - import('../components/DependsOnComponentsCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:component', + loader: async () => + import('../components/DependsOnComponentsCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogDependsOnResourcesEntityCard = createEntityCardExtension({ +export const catalogDependsOnResourcesEntityCard = EntityCardBlueprint.make({ name: 'depends-on-resources', - filter: 'kind:component', - loader: async () => - import('../components/DependsOnResourcesCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:component', + loader: async () => + import('../components/DependsOnResourcesCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogHasComponentsEntityCard = createEntityCardExtension({ +export const catalogHasComponentsEntityCard = EntityCardBlueprint.make({ name: 'has-components', - filter: 'kind:system', - loader: async () => - import('../components/HasComponentsCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:system', + loader: async () => + import('../components/HasComponentsCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogHasResourcesEntityCard = createEntityCardExtension({ +export const catalogHasResourcesEntityCard = EntityCardBlueprint.make({ name: 'has-resources', - filter: 'kind:system', - loader: async () => - import('../components/HasResourcesCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:system', + loader: async () => + import('../components/HasResourcesCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogHasSubcomponentsEntityCard = createEntityCardExtension({ +export const catalogHasSubcomponentsEntityCard = EntityCardBlueprint.make({ name: 'has-subcomponents', - filter: 'kind:component', - loader: async () => - import('../components/HasSubcomponentsCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:component', + loader: async () => + import('../components/HasSubcomponentsCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogHasSubdomainsEntityCard = createEntityCardExtension({ +export const catalogHasSubdomainsEntityCard = EntityCardBlueprint.make({ name: 'has-subdomains', - filter: 'kind:domain', - loader: async () => - import('../components/HasSubdomainsCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:domain', + loader: async () => + import('../components/HasSubdomainsCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogHasSystemsEntityCard = createEntityCardExtension({ +export const catalogHasSystemsEntityCard = EntityCardBlueprint.make({ name: 'has-systems', - filter: 'kind:domain', - loader: async () => - import('../components/HasSystemsCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:domain', + loader: async () => + import('../components/HasSystemsCard').then(m => + compatWrapper(), + ), + }, }); export default [ diff --git a/plugins/catalog/src/alpha/entityContents.tsx b/plugins/catalog/src/alpha/entityContents.tsx index 75c3fef49f..259b92492c 100644 --- a/plugins/catalog/src/alpha/entityContents.tsx +++ b/plugins/catalog/src/alpha/entityContents.tsx @@ -19,27 +19,38 @@ import { coreExtensionData, createExtensionInput, } from '@backstage/frontend-plugin-api'; -import { - createEntityContentExtension, - catalogExtensionData, -} from '@backstage/plugin-catalog-react/alpha'; +import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; -export const catalogOverviewEntityContent = createEntityContentExtension({ - name: 'overview', - defaultPath: '/', - defaultTitle: 'Overview', - disabled: false, - inputs: { - cards: createExtensionInput({ - element: coreExtensionData.reactElement, - filterFunction: catalogExtensionData.entityFilterFunction.optional(), - filterExpression: catalogExtensionData.entityFilterExpression.optional(), - }), - }, - loader: async ({ inputs }) => - import('./EntityOverviewPage').then(m => ( - c.output)} /> - )), -}); +export const catalogOverviewEntityContent = + EntityContentBlueprint.makeWithOverrides({ + name: 'overview', + inputs: { + cards: createExtensionInput([ + coreExtensionData.reactElement, + EntityContentBlueprint.dataRefs.filterFunction.optional(), + EntityContentBlueprint.dataRefs.filterExpression.optional(), + ]), + }, + factory: (originalFactory, { inputs }) => { + return originalFactory({ + defaultPath: '/', + defaultTitle: 'Overview', + loader: async () => + import('./EntityOverviewPage').then(m => ( + ({ + element: c.get(coreExtensionData.reactElement), + filterFunction: c.get( + EntityContentBlueprint.dataRefs.filterFunction, + ), + filterExpression: c.get( + EntityContentBlueprint.dataRefs.filterExpression, + ), + }))} + /> + )), + }); + }, + }); export default [catalogOverviewEntityContent]; diff --git a/plugins/catalog/src/alpha/index.ts b/plugins/catalog/src/alpha/index.ts index 06a78ec6ea..f004c4f19e 100644 --- a/plugins/catalog/src/alpha/index.ts +++ b/plugins/catalog/src/alpha/index.ts @@ -16,3 +16,6 @@ export { default } from './plugin'; export { createCatalogFilterExtension } from './createCatalogFilterExtension'; + +export * from './blueprints'; +export * from './translation'; diff --git a/plugins/catalog/src/translation.ts b/plugins/catalog/src/alpha/translation.ts similarity index 100% rename from plugins/catalog/src/translation.ts rename to plugins/catalog/src/alpha/translation.ts diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index ff5d7fc801..cf4a3c2d43 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -64,7 +64,7 @@ import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common import { useSourceTemplateCompoundEntityRef } from './hooks'; import { taskCreatePermission } from '@backstage/plugin-scaffolder-common/alpha'; import { usePermission } from '@backstage/plugin-permission-react'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref'; diff --git a/plugins/catalog/src/components/AboutCard/AboutContent.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.tsx index 3f781f4759..f6d2d80264 100644 --- a/plugins/catalog/src/components/AboutCard/AboutContent.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutContent.tsx @@ -33,7 +33,7 @@ import React from 'react'; import { AboutField } from './AboutField'; import { LinksGridList } from '../EntityLinksCard/LinksGridList'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; const useStyles = makeStyles({ description: { diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx index db765b9145..a46a552b8d 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx @@ -33,7 +33,7 @@ import { import React, { ReactNode } from 'react'; import { createComponentRouteRef } from '../../routes'; import { CatalogTable, CatalogTableRow } from '../CatalogTable'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { CatalogTableColumnsFunc } from '../CatalogTable/types'; diff --git a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx index 1a596f8b9b..f4501bd795 100644 --- a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx +++ b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx @@ -27,7 +27,7 @@ import { ResultHighlight, } from '@backstage/plugin-search-common'; import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; const useStyles = makeStyles( diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index e18467249a..1026dc5d26 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -49,7 +49,7 @@ import { CatalogTableColumnsFunc, CatalogTableRow } from './types'; import { PaginatedCatalogTable } from './PaginatedCatalogTable'; import { defaultCatalogTableColumnsFunc } from './defaultCatalogTableColumnsFunc'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; /** * Props for {@link CatalogTable}. diff --git a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx index 2f0557d11e..6c6f811cc1 100644 --- a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx +++ b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx @@ -30,7 +30,7 @@ import { componentEntityHelpLink, RelatedEntitiesCard, } from '../RelatedEntitiesCard'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx index fe56ea582b..eafa33346d 100644 --- a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx +++ b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx @@ -27,7 +27,7 @@ import { componentEntityHelpLink, RelatedEntitiesCard, } from '../RelatedEntitiesCard'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx index 281bfd8766..723b87cc29 100644 --- a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx +++ b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx @@ -27,7 +27,7 @@ import { RelatedEntitiesCard, resourceEntityColumns, } from '../RelatedEntitiesCard'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx index 5b740038a1..77f934e7de 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx @@ -33,7 +33,7 @@ import { catalogEntityDeletePermission } from '@backstage/plugin-catalog-common/ import { UnregisterEntity, UnregisterEntityOptions } from './UnregisterEntity'; import { useApi, alertApiRef } from '@backstage/core-plugin-api'; import useCopyToClipboard from 'react-use/esm/useCopyToClipboard'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.tsx b/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.tsx index 08496d4819..63247d0c62 100644 --- a/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.tsx @@ -19,7 +19,7 @@ import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import MenuItem from '@material-ui/core/MenuItem'; import CancelIcon from '@material-ui/icons/Cancel'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; type VisibleType = 'visible' | 'hidden' | 'disable'; diff --git a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx index d48b45f054..c4557b6774 100644 --- a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx +++ b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx @@ -25,7 +25,7 @@ import { import { EntityLabelsEmptyState } from './EntityLabelsEmptyState'; import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsEmptyState.tsx b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsEmptyState.tsx index 16d0a3a897..d78be2d2ce 100644 --- a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsEmptyState.tsx +++ b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsEmptyState.tsx @@ -19,7 +19,7 @@ import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; import { CodeSnippet } from '@backstage/core-components'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const ENTITY_YAML = `metadata: diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index bbc7792acb..e8b957497d 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -53,7 +53,7 @@ import React, { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; import { rootRouteRef, unregisterRedirectRouteRef } from '../../routes'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx index b5d8269a0b..59e1f6d252 100644 --- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx @@ -23,7 +23,7 @@ import { ColumnBreakpoints } from './types'; import { IconComponent, useApp } from '@backstage/core-plugin-api'; import { InfoCard, InfoCardVariants } from '@backstage/core-components'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; /** @public */ export interface EntityLinksCardProps { diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx index 09362385f9..513183677d 100644 --- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx @@ -20,7 +20,7 @@ import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; import { CodeSnippet } from '@backstage/core-components'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; const ENTITY_YAML = `metadata: name: example diff --git a/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx b/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx index 252005dff5..a033199ffd 100644 --- a/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx +++ b/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx @@ -20,7 +20,7 @@ import Button from '@material-ui/core/Button'; import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import { Illo } from './Illo'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const useStyles = makeStyles(theme => ({ diff --git a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx index c9c24a14a1..b1a7afbb50 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx @@ -23,7 +23,7 @@ import DialogTitle from '@material-ui/core/DialogTitle'; import React, { useState } from 'react'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; import { assertError } from '@backstage/errors'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; interface DeleteEntityDialogProps { diff --git a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx index 10a10e54fd..eb561f9c19 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx @@ -22,7 +22,7 @@ import { useNavigate } from 'react-router-dom'; import { DeleteEntityDialog } from './DeleteEntityDialog'; import { useRouteRef } from '@backstage/core-plugin-api'; import { rootRouteRef } from '../../routes'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx index bd49785abe..3c7c5bb2e3 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx @@ -31,7 +31,7 @@ import { import { useApi, ApiHolder } from '@backstage/core-plugin-api'; import useAsync from 'react-use/esm/useAsync'; import { SerializedError } from '@backstage/errors'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const errorFilter = (i: EntityStatusItem) => diff --git a/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx b/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx index 2ce9120bf0..ed816bfc75 100644 --- a/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx +++ b/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx @@ -26,7 +26,7 @@ import useAsync from 'react-use/esm/useAsync'; import Box from '@material-ui/core/Box'; import { ResponseErrorPanel } from '@backstage/core-components'; import { useApi, ApiHolder } from '@backstage/core-plugin-api'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; async function getRelationWarnings(entity: Entity, catalogApi: CatalogApi) { diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx index 13a9e75928..2fc6cde81b 100644 --- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx @@ -27,7 +27,7 @@ import { componentEntityHelpLink, RelatedEntitiesCard, } from '../RelatedEntitiesCard'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx index b99bca1716..6b90ce0443 100644 --- a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx +++ b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx @@ -27,7 +27,7 @@ import { resourceEntityColumns, resourceEntityHelpLink, } from '../RelatedEntitiesCard'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx index ceea839ef4..ceab037a45 100644 --- a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx +++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx @@ -26,7 +26,7 @@ import { componentEntityColumns, RelatedEntitiesCard, } from '../RelatedEntitiesCard'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx b/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx index 811f74d820..71705ea90f 100644 --- a/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx +++ b/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx @@ -22,7 +22,7 @@ import { componentEntityColumns, RelatedEntitiesCard, } from '../RelatedEntitiesCard'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx index 15b4f933ba..48742f9d28 100644 --- a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx +++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx @@ -27,7 +27,7 @@ import { systemEntityColumns, systemEntityHelpLink, } from '../RelatedEntitiesCard'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx b/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx index e6e375646d..a2814527cc 100644 --- a/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx +++ b/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx @@ -42,7 +42,7 @@ import { systemEntityColumns, systemEntityHelpLink, } from './presets'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx index 5e485d672b..879a5076b1 100644 --- a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx +++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx @@ -46,7 +46,7 @@ import { } from '@backstage/core-components'; import { useApi, useRouteRef } from '@backstage/core-plugin-api'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/setupTests.ts b/plugins/catalog/src/setupTests.ts index 963c0f188b..dae4360fd3 100644 --- a/plugins/catalog/src/setupTests.ts +++ b/plugins/catalog/src/setupTests.ts @@ -15,3 +15,18 @@ */ import '@testing-library/jest-dom'; + +// eslint-disable-next-line no-console +const originalConsoleWarn = console.warn; +// eslint-disable-next-line no-console +console.warn = (...args: any[]) => { + const message = args[0]; + if ( + typeof message === 'string' && + (message.includes('CSSOM.parse is not a function') || + message.includes('[JSS]')) + ) { + return; + } + originalConsoleWarn(...args); +}; diff --git a/plugins/search-react/api-report-alpha.md b/plugins/search-react/api-report-alpha.md index 4bcdbb3f78..0126fdc590 100644 --- a/plugins/search-react/api-report-alpha.md +++ b/plugins/search-react/api-report-alpha.md @@ -6,6 +6,7 @@ /// import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { ListItemProps } from '@material-ui/core/ListItem'; import { PortableSchema } from '@backstage/frontend-plugin-api'; @@ -18,7 +19,7 @@ export type BaseSearchResultListItemProps = T & { result?: SearchDocument; } & Omit; -// @alpha (undocumented) +// @alpha @deprecated export function createSearchResultListItemExtension< TConfig extends { noTrack?: boolean; @@ -35,9 +36,9 @@ export function createSearchResultListItemExtension< string | undefined >; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export namespace createSearchResultListItemExtension { - const // (undocumented) + const // @deprecated (undocumented) itemDataRef: ConfigurableExtensionDataRef< { predicate?: SearchResultItemExtensionPredicate | undefined; @@ -55,7 +56,7 @@ export type SearchResultItemExtensionComponent = < props: P, ) => JSX.Element | null; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export type SearchResultItemExtensionOptions< TConfig extends { noTrack?: boolean; @@ -79,5 +80,48 @@ export type SearchResultItemExtensionPredicate = ( result: SearchResult, ) => boolean; +// @alpha +export const SearchResultListItemBlueprint: ExtensionBlueprint< + 'search-result-list-item', + undefined, + undefined, + SearchResultListItemBlueprintParams, + ConfigurableExtensionDataRef< + { + predicate?: SearchResultItemExtensionPredicate | undefined; + component: SearchResultItemExtensionComponent; + }, + 'search.search-result-list-item.item', + {} + >, + {}, + { + noTrack: boolean; + }, + { + noTrack?: boolean | undefined; + }, + { + item: ConfigurableExtensionDataRef< + { + predicate?: SearchResultItemExtensionPredicate | undefined; + component: SearchResultItemExtensionComponent; + }, + 'search.search-result-list-item.item', + {} + >; + } +>; + +// @alpha (undocumented) +export interface SearchResultListItemBlueprintParams { + component: (options: { + config: { + noTrack?: boolean; + }; + }) => Promise; + predicate?: SearchResultItemExtensionPredicate; +} + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index e16430c55b..887b41cb83 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -28,7 +28,7 @@ "sideEffects": false, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.tsx", + "./alpha": "./src/alpha/index.ts", "./package.json": "./package.json" }, "main": "src/index.ts", @@ -36,7 +36,7 @@ "typesVersions": { "*": { "alpha": [ - "src/alpha.tsx" + "src/alpha/index.ts" ], "package.json": [ "package.json" diff --git a/plugins/search-react/src/alpha/blueprints/SearchResultListItemBlueprint.test.tsx b/plugins/search-react/src/alpha/blueprints/SearchResultListItemBlueprint.test.tsx new file mode 100644 index 0000000000..4f3b7501b4 --- /dev/null +++ b/plugins/search-react/src/alpha/blueprints/SearchResultListItemBlueprint.test.tsx @@ -0,0 +1,123 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { SearchResultListItemBlueprint } from './SearchResultListItemBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { + PageBlueprint, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; +import { searchResultListItemDataRef } from './types'; +import _ from 'lodash'; + +describe('SearchResultListItemBlueprint', () => { + it('should return an extension with sane defaults', () => { + const extension = SearchResultListItemBlueprint.make({ + name: 'test', + params: { + component: async () => () =>
Hello
, + predicate: () => true, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "page:search", + "input": "items", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "noTrack": { + "default": false, + "type": "boolean", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "search-result-list-item", + "name": "test", + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('defaults and passes on config properly', async () => { + const extension = SearchResultListItemBlueprint.make({ + name: 'test', + params: { + component: + async ({ config: { noTrack } }) => + () => +
noTrack: {String(noTrack)}
, + }, + }); + + const mockSearchPage = PageBlueprint.makeWithOverrides({ + namespace: 'search', + inputs: { + items: createExtensionInput([searchResultListItemDataRef]), + }, + factory(originalFactory, { inputs }) { + return originalFactory({ + defaultPath: '/', + loader: async () => { + const items = inputs.items.map(i => + i.get(searchResultListItemDataRef), + ); + return ( +
+ {items.map((item, i) => ( + + ))} +
+ ); + }, + }); + }, + }); + + await expect( + createExtensionTester(mockSearchPage) + .add(extension) + .render() + .findByText('noTrack: false'), + ).resolves.toBeInTheDocument(); + + await expect( + createExtensionTester(mockSearchPage) + .add(extension, { config: { noTrack: true } }) + .render() + .findByText('noTrack: true'), + ).resolves.toBeInTheDocument(); + }); +}); diff --git a/plugins/search-react/src/alpha/blueprints/SearchResultListItemBlueprint.tsx b/plugins/search-react/src/alpha/blueprints/SearchResultListItemBlueprint.tsx new file mode 100644 index 0000000000..b9a7b71719 --- /dev/null +++ b/plugins/search-react/src/alpha/blueprints/SearchResultListItemBlueprint.tsx @@ -0,0 +1,86 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { lazy } from 'react'; +import { + createExtensionBlueprint, + ExtensionBoundary, +} from '@backstage/frontend-plugin-api'; +import { + SearchResultItemExtensionComponent, + SearchResultItemExtensionPredicate, + searchResultListItemDataRef, +} from './types'; +import { + SearchResultListItemExtension, + SearchResultListItemExtensionProps, +} from '../../extensions'; + +/** @alpha */ +export interface SearchResultListItemBlueprintParams { + /** + * The extension component. + */ + component: (options: { + config: { noTrack?: boolean }; + }) => Promise; + /** + * When an extension defines a predicate, it returns true if the result should be rendered by that extension. + * Defaults to a predicate that returns true, which means it renders all sorts of results. + */ + predicate?: SearchResultItemExtensionPredicate; +} + +/** + * @alpha + * Creates SearchResultListItem extensions + */ +export const SearchResultListItemBlueprint = createExtensionBlueprint({ + kind: 'search-result-list-item', + attachTo: { + id: 'page:search', + input: 'items', + }, + config: { + schema: { + noTrack: z => z.boolean().default(false), + }, + }, + output: [searchResultListItemDataRef], + dataRefs: { + item: searchResultListItemDataRef, + }, + *factory(params: SearchResultListItemBlueprintParams, { config, node }) { + const ExtensionComponent = lazy(() => + params.component({ config }).then(component => ({ default: component })), + ); + + yield searchResultListItemDataRef({ + predicate: params.predicate, + component: (props: SearchResultListItemExtensionProps) => ( + + + + + + ), + }); + }, +}); diff --git a/plugins/search-react/src/alpha/blueprints/index.ts b/plugins/search-react/src/alpha/blueprints/index.ts new file mode 100644 index 0000000000..b9bbf1012e --- /dev/null +++ b/plugins/search-react/src/alpha/blueprints/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + SearchResultListItemBlueprint, + type SearchResultListItemBlueprintParams, +} from './SearchResultListItemBlueprint'; +export { + type BaseSearchResultListItemProps, + type SearchResultItemExtensionComponent, + type SearchResultItemExtensionPredicate, +} from './types'; diff --git a/plugins/search-react/src/alpha/blueprints/types.ts b/plugins/search-react/src/alpha/blueprints/types.ts new file mode 100644 index 0000000000..abfed4aba6 --- /dev/null +++ b/plugins/search-react/src/alpha/blueprints/types.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ListItemProps } from '@material-ui/core/ListItem'; +import { SearchDocument, SearchResult } from '@backstage/plugin-search-common'; +import { createExtensionDataRef } from '@backstage/frontend-plugin-api'; + +/** @alpha */ +export type BaseSearchResultListItemProps = T & { + rank?: number; + result?: SearchDocument; +} & Omit; + +/** @alpha */ +export type SearchResultItemExtensionComponent = < + P extends BaseSearchResultListItemProps, +>( + props: P, +) => JSX.Element | null; + +/** @alpha */ +export type SearchResultItemExtensionPredicate = ( + result: SearchResult, +) => boolean; + +export const searchResultListItemDataRef = createExtensionDataRef<{ + predicate?: SearchResultItemExtensionPredicate; + component: SearchResultItemExtensionComponent; +}>().with({ id: 'search.search-result-list-item.item' }); diff --git a/plugins/search-react/src/alpha.test.tsx b/plugins/search-react/src/alpha/extensions.test.tsx similarity index 97% rename from plugins/search-react/src/alpha.test.tsx rename to plugins/search-react/src/alpha/extensions.test.tsx index 6d77b5af97..5194fadd1c 100644 --- a/plugins/search-react/src/alpha.test.tsx +++ b/plugins/search-react/src/alpha/extensions.test.tsx @@ -23,10 +23,8 @@ import { createExtensionTester } from '@backstage/frontend-test-utils'; import { SearchResult } from '@backstage/plugin-search-common'; import { screen } from '@testing-library/react'; import React from 'react'; -import { - BaseSearchResultListItemProps, - createSearchResultListItemExtension, -} from './alpha'; +import { createSearchResultListItemExtension } from './extensions'; +import { BaseSearchResultListItemProps } from './blueprints'; describe('createSearchResultListItemExtension', () => { it('Should use the correct result component', async () => { diff --git a/plugins/search-react/src/alpha.tsx b/plugins/search-react/src/alpha/extensions.tsx similarity index 77% rename from plugins/search-react/src/alpha.tsx rename to plugins/search-react/src/alpha/extensions.tsx index a22eaf4244..db09510a1e 100644 --- a/plugins/search-react/src/alpha.tsx +++ b/plugins/search-react/src/alpha/extensions.tsx @@ -15,39 +15,23 @@ */ import React, { lazy } from 'react'; - -import { ListItemProps } from '@material-ui/core/ListItem'; - import { ExtensionBoundary, PortableSchema, createExtension, - createExtensionDataRef, createSchemaFromZod, } from '@backstage/frontend-plugin-api'; -import { SearchDocument, SearchResult } from '@backstage/plugin-search-common'; +import { + SearchResultItemExtensionComponent, + SearchResultItemExtensionPredicate, +} from './blueprints'; +import { SearchResultListItemExtension } from '../extensions'; +import { searchResultListItemDataRef } from './blueprints/types'; -import { SearchResultListItemExtension } from './extensions'; - -/** @alpha */ -export type BaseSearchResultListItemProps = T & { - rank?: number; - result?: SearchDocument; -} & Omit; - -/** @alpha */ -export type SearchResultItemExtensionComponent = < - P extends BaseSearchResultListItemProps, ->( - props: P, -) => JSX.Element | null; - -/** @alpha */ -export type SearchResultItemExtensionPredicate = ( - result: SearchResult, -) => boolean; - -/** @alpha */ +/** + * @alpha + * @deprecated Use {@link SearchResultListItemBlueprint} instead + */ export type SearchResultItemExtensionOptions< TConfig extends { noTrack?: boolean }, > = { @@ -80,7 +64,12 @@ export type SearchResultItemExtensionOptions< predicate?: SearchResultItemExtensionPredicate; }; -/** @alpha */ +/** + * Creates items for the search result list. + * + * @alpha + * @deprecated Use {@link SearchResultListItemBlueprint} instead + */ export function createSearchResultListItemExtension< TConfig extends { noTrack?: boolean }, >(options: SearchResultItemExtensionOptions) { @@ -132,10 +121,13 @@ export function createSearchResultListItemExtension< }); } -/** @alpha */ +/** + * @alpha + * @deprecated Use {@link SearchResultListItemBlueprint} instead + */ export namespace createSearchResultListItemExtension { - export const itemDataRef = createExtensionDataRef<{ - predicate?: SearchResultItemExtensionPredicate; - component: SearchResultItemExtensionComponent; - }>().with({ id: 'search.search-result-list-item.item' }); + /** + * @deprecated Use {@link SearchResultListItemBlueprint#dataRefs.item} instead + */ + export const itemDataRef = searchResultListItemDataRef; } diff --git a/plugins/search-react/src/alpha/index.ts b/plugins/search-react/src/alpha/index.ts new file mode 100644 index 0000000000..04026d629d --- /dev/null +++ b/plugins/search-react/src/alpha/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './extensions'; +export * from './blueprints'; diff --git a/plugins/search-react/src/setupTests.ts b/plugins/search-react/src/setupTests.ts index 992b60d3a4..c47fc7a427 100644 --- a/plugins/search-react/src/setupTests.ts +++ b/plugins/search-react/src/setupTests.ts @@ -15,3 +15,18 @@ */ import '@testing-library/jest-dom'; + +// eslint-disable-next-line no-console +const originalConsoleWarn = console.warn; +// eslint-disable-next-line no-console +console.warn = (...args: any[]) => { + const message = args[0]; + if ( + typeof message === 'string' && + (message.includes('CSSOM.parse is not a function') || + message.includes('[JSS]')) + ) { + return; + } + originalConsoleWarn(...args); +}; diff --git a/yarn.lock b/yarn.lock index 4c47bc5771..4a5132a6e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5920,6 +5920,7 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/frontend-test-utils": "workspace:^" "@backstage/integration-react": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" @@ -6000,6 +6001,7 @@ __metadata: "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/frontend-test-utils": "workspace:^" "@backstage/integration-react": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^"