Merge pull request #26005 from backstage/blam/nfs/last-of-the-extensions

NFS: Migrate the last of the `create*Extension` creators
This commit is contained in:
Ben Lambert
2024-08-15 11:17:46 +02:00
committed by GitHub
60 changed files with 1831 additions and 173 deletions
+7
View File
@@ -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.
+117 -3
View File
@@ -6,16 +6,19 @@
/// <reference types="react" />
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<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
},
| ConfigurableExtensionDataRef<React_2.JSX.Element, 'core.reactElement', {}>
| 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<JSX.Element>;
defaultPath: string;
defaultTitle: string;
routeRef?: RouteRef<AnyRouteRefParams> | undefined;
filter?: string | ((entity: Entity) => boolean) | undefined;
},
| ConfigurableExtensionDataRef<React_2.JSX.Element, 'core.reactElement', {}>
| ConfigurableExtensionDataRef<string, 'core.routing.path', {}>
| ConfigurableExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>
| ConfigurableExtensionDataRef<string, 'catalog.entity-content-title', {}>
| 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;
+3 -2
View File
@@ -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:^",
@@ -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 () => <div>im a card</div>,
},
});
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 () => <div>Test!</div>,
filter: 'test',
},
}),
).data(EntityCardBlueprint.dataRefs.filterExpression),
).toBe('test');
expect(
createExtensionTester(
EntityCardBlueprint.make({
name: 'test',
params: {
loader: async () => <div>Test!</div>,
},
}),
{ config: { filter: 'test' } },
).data(EntityCardBlueprint.dataRefs.filterExpression),
).toBe('test');
expect(
createExtensionTester(
EntityCardBlueprint.make({
name: 'test',
params: {
filter: mockFilter,
loader: async () => <div>Test!</div>,
},
}),
).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 () => (
<div data-testid="test">
config: {config.mock}
<div data-testid="contents">
{inputs.mock.map((i, k) => (
<div key={k}>{i.get(coreExtensionData.reactElement)}</div>
))}
</div>
</div>
),
});
},
});
const mockExtension = createExtension({
attachTo: { id: 'entity-card:test', input: 'mock' },
output: [coreExtensionData.reactElement],
factory() {
return [coreExtensionData.reactElement(<div>im a mock</div>)];
},
});
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');
});
});
});
@@ -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<JSX.Element>;
filter?:
| typeof catalogExtensionData.entityFilterFunction.T
| typeof catalogExtensionData.entityFilterExpression.T;
},
{ node, config },
) {
const ExtensionComponent = lazy(() =>
loader().then(element => ({ default: () => element })),
);
yield coreExtensionData.reactElement(
<ExtensionBoundary node={node}>
<ExtensionComponent />
</ExtensionBoundary>,
);
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);
}
},
});
@@ -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 () => <div>Test!</div>,
},
});
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 () => <div>Test!</div>,
},
});
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 () => <div>Test!</div>,
filter: 'test',
},
}),
).data(EntityContentBlueprint.dataRefs.filterExpression),
).toBe('test');
expect(
createExtensionTester(
EntityContentBlueprint.make({
name: 'test',
params: {
defaultPath: '/test',
defaultTitle: 'Test',
loader: async () => <div>Test!</div>,
},
}),
{ config: { filter: 'test' } },
).data(EntityContentBlueprint.dataRefs.filterExpression),
).toBe('test');
expect(
createExtensionTester(
EntityContentBlueprint.make({
name: 'test',
params: {
defaultPath: '/test',
defaultTitle: 'Test',
filter: mockFilter,
loader: async () => <div>Test!</div>,
},
}),
).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 () => (
<div data-testid="test">
config: {config.mock}
<div data-testid="contents">
{inputs.mock.map((i, k) => (
<div key={k}>{i.get(coreExtensionData.reactElement)}</div>
))}
</div>
</div>
),
});
},
});
const mockExtension = createExtension({
attachTo: { id: 'entity-content:test', input: 'mock' },
output: [coreExtensionData.reactElement],
factory() {
return [coreExtensionData.reactElement(<div>im a mock</div>)];
},
});
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');
});
});
});
@@ -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<JSX.Element>;
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(
<ExtensionBoundary node={node}>
<ExtensionComponent />
</ExtensionBoundary>,
);
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);
}
},
});
@@ -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';
@@ -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<string>().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: {
+17
View File
@@ -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';
+15
View File
@@ -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);
};
+361 -2
View File
@@ -6,13 +6,35 @@
/// <reference types="react" />
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<JSX.Element>;
},
ConfigurableExtensionDataRef<React_2.JSX.Element, 'core.reactElement', {}>,
{},
{},
{},
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<undefined>;
},
{}
{
'entity-card:catalog/about': ExtensionDefinition<
{
filter: string | undefined;
},
{
filter?: string | undefined;
},
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| 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<JSX_2.Element, 'core.reactElement', {}>
| 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<JSX_2.Element, 'core.reactElement', {}>
| 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<JSX_2.Element, 'core.reactElement', {}>
| 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<JSX_2.Element, 'core.reactElement', {}>
| 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<JSX_2.Element, 'core.reactElement', {}>
| 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<JSX_2.Element, 'core.reactElement', {}>
| 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<JSX_2.Element, 'core.reactElement', {}>
| 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<JSX_2.Element, 'core.reactElement', {}>
| 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<JSX_2.Element, 'core.reactElement', {}>
| 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<JSX_2.Element, 'core.reactElement', {}>
| ConfigurableExtensionDataRef<string, 'core.routing.path', {}>
| ConfigurableExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>
| ConfigurableExtensionDataRef<string, 'catalog.entity-content-title', {}>
| ConfigurableExtensionDataRef<
(entity: Entity) => boolean,
'catalog.entity-filter-function',
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-filter-expression',
{
optional: true;
}
>,
{
cards: ExtensionInput<
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| 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;
+3 -2
View File
@@ -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",
@@ -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 () => <div />,
},
});
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 () => (
<div data-testid="test">
config: {config.test}
<div data-testid="contents">
{inputs.mock.map((i, k) => (
<div key={k}>{i.get(coreExtensionData.reactElement)}</div>
))}
</div>
</div>
),
});
},
});
const mockExtension = createExtension({
attachTo: { id: 'catalog-filter:test-name', input: 'mock' },
output: [coreExtensionData.reactElement],
factory() {
return [coreExtensionData.reactElement(<div>im a mock</div>)];
},
});
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');
});
});
});
@@ -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<JSX.Element> }, { node }) {
const ExtensionComponent = lazy(() =>
params.loader().then(element => ({ default: () => element })),
);
return [
coreExtensionData.reactElement(
<ExtensionBoundary node={node}>
<ExtensionComponent />
</ExtensionBoundary>,
),
];
},
});
@@ -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';
@@ -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,
+80 -60
View File
@@ -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(<m.AboutCard variant="gridItem" />),
),
params: {
loader: async () =>
import('../components/AboutCard').then(m =>
compatWrapper(<m.AboutCard variant="gridItem" />),
),
},
});
export const catalogLinksEntityCard = createEntityCardExtension({
export const catalogLinksEntityCard = EntityCardBlueprint.make({
name: 'links',
filter: 'has:links',
loader: async () =>
import('../components/EntityLinksCard').then(m =>
compatWrapper(<m.EntityLinksCard variant="gridItem" />),
),
params: {
filter: 'has:links',
loader: async () =>
import('../components/EntityLinksCard').then(m =>
compatWrapper(<m.EntityLinksCard variant="gridItem" />),
),
},
});
export const catalogLabelsEntityCard = createEntityCardExtension({
export const catalogLabelsEntityCard = EntityCardBlueprint.make({
name: 'labels',
filter: 'has:labels',
loader: async () =>
import('../components/EntityLabelsCard').then(m =>
compatWrapper(<m.EntityLabelsCard variant="gridItem" />),
),
params: {
filter: 'has:labels',
loader: async () =>
import('../components/EntityLabelsCard').then(m =>
compatWrapper(<m.EntityLabelsCard variant="gridItem" />),
),
},
});
export const catalogDependsOnComponentsEntityCard = createEntityCardExtension({
export const catalogDependsOnComponentsEntityCard = EntityCardBlueprint.make({
name: 'depends-on-components',
filter: 'kind:component',
loader: async () =>
import('../components/DependsOnComponentsCard').then(m =>
compatWrapper(<m.DependsOnComponentsCard variant="gridItem" />),
),
params: {
filter: 'kind:component',
loader: async () =>
import('../components/DependsOnComponentsCard').then(m =>
compatWrapper(<m.DependsOnComponentsCard variant="gridItem" />),
),
},
});
export const catalogDependsOnResourcesEntityCard = createEntityCardExtension({
export const catalogDependsOnResourcesEntityCard = EntityCardBlueprint.make({
name: 'depends-on-resources',
filter: 'kind:component',
loader: async () =>
import('../components/DependsOnResourcesCard').then(m =>
compatWrapper(<m.DependsOnResourcesCard variant="gridItem" />),
),
params: {
filter: 'kind:component',
loader: async () =>
import('../components/DependsOnResourcesCard').then(m =>
compatWrapper(<m.DependsOnResourcesCard variant="gridItem" />),
),
},
});
export const catalogHasComponentsEntityCard = createEntityCardExtension({
export const catalogHasComponentsEntityCard = EntityCardBlueprint.make({
name: 'has-components',
filter: 'kind:system',
loader: async () =>
import('../components/HasComponentsCard').then(m =>
compatWrapper(<m.HasComponentsCard variant="gridItem" />),
),
params: {
filter: 'kind:system',
loader: async () =>
import('../components/HasComponentsCard').then(m =>
compatWrapper(<m.HasComponentsCard variant="gridItem" />),
),
},
});
export const catalogHasResourcesEntityCard = createEntityCardExtension({
export const catalogHasResourcesEntityCard = EntityCardBlueprint.make({
name: 'has-resources',
filter: 'kind:system',
loader: async () =>
import('../components/HasResourcesCard').then(m =>
compatWrapper(<m.HasResourcesCard variant="gridItem" />),
),
params: {
filter: 'kind:system',
loader: async () =>
import('../components/HasResourcesCard').then(m =>
compatWrapper(<m.HasResourcesCard variant="gridItem" />),
),
},
});
export const catalogHasSubcomponentsEntityCard = createEntityCardExtension({
export const catalogHasSubcomponentsEntityCard = EntityCardBlueprint.make({
name: 'has-subcomponents',
filter: 'kind:component',
loader: async () =>
import('../components/HasSubcomponentsCard').then(m =>
compatWrapper(<m.HasSubcomponentsCard variant="gridItem" />),
),
params: {
filter: 'kind:component',
loader: async () =>
import('../components/HasSubcomponentsCard').then(m =>
compatWrapper(<m.HasSubcomponentsCard variant="gridItem" />),
),
},
});
export const catalogHasSubdomainsEntityCard = createEntityCardExtension({
export const catalogHasSubdomainsEntityCard = EntityCardBlueprint.make({
name: 'has-subdomains',
filter: 'kind:domain',
loader: async () =>
import('../components/HasSubdomainsCard').then(m =>
compatWrapper(<m.HasSubdomainsCard variant="gridItem" />),
),
params: {
filter: 'kind:domain',
loader: async () =>
import('../components/HasSubdomainsCard').then(m =>
compatWrapper(<m.HasSubdomainsCard variant="gridItem" />),
),
},
});
export const catalogHasSystemsEntityCard = createEntityCardExtension({
export const catalogHasSystemsEntityCard = EntityCardBlueprint.make({
name: 'has-systems',
filter: 'kind:domain',
loader: async () =>
import('../components/HasSystemsCard').then(m =>
compatWrapper(<m.HasSystemsCard variant="gridItem" />),
),
params: {
filter: 'kind:domain',
loader: async () =>
import('../components/HasSystemsCard').then(m =>
compatWrapper(<m.HasSystemsCard variant="gridItem" />),
),
},
});
export default [
+32 -21
View File
@@ -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 => (
<m.EntityOverviewPage cards={inputs.cards.map(c => 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 => (
<m.EntityOverviewPage
cards={inputs.cards.map(c => ({
element: c.get(coreExtensionData.reactElement),
filterFunction: c.get(
EntityContentBlueprint.dataRefs.filterFunction,
),
filterExpression: c.get(
EntityContentBlueprint.dataRefs.filterExpression,
),
}))}
/>
)),
});
},
});
export default [catalogOverviewEntityContent];
+3
View File
@@ -16,3 +16,6 @@
export { default } from './plugin';
export { createCatalogFilterExtension } from './createCatalogFilterExtension';
export * from './blueprints';
export * from './translation';
@@ -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';
@@ -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: {
@@ -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';
@@ -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(
@@ -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}.
@@ -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 */
@@ -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 */
@@ -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 */
@@ -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 */
@@ -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';
@@ -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 */
@@ -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:
@@ -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 */
@@ -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 {
@@ -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
@@ -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 => ({
@@ -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 {
@@ -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';
/**
@@ -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) =>
@@ -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) {
@@ -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 */
@@ -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 */
@@ -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 */
@@ -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 */
@@ -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 */
@@ -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 */
@@ -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 */
+15
View File
@@ -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);
};
+48 -4
View File
@@ -6,6 +6,7 @@
/// <reference types="react" />
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 = {}> = T & {
result?: SearchDocument;
} & Omit<ListItemProps, 'button'>;
// @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<SearchResultItemExtensionComponent>;
predicate?: SearchResultItemExtensionPredicate;
}
// (No @packageDocumentation comment for this package)
```
+2 -2
View File
@@ -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"
@@ -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 () => () => <div>Hello</div>,
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 } }) =>
() =>
<div>noTrack: {String(noTrack)}</div>,
},
});
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 (
<div>
{items.map((item, i) => (
<item.component key={i} />
))}
</div>
);
},
});
},
});
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();
});
});
@@ -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<SearchResultItemExtensionComponent>;
/**
* 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) => (
<ExtensionBoundary node={node}>
<SearchResultListItemExtension
rank={props.rank}
result={props.result}
noTrack={config.noTrack}
>
<ExtensionComponent {...props} />
</SearchResultListItemExtension>
</ExtensionBoundary>
),
});
},
});
@@ -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';
@@ -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 = {}> = T & {
rank?: number;
result?: SearchDocument;
} & Omit<ListItemProps, 'button'>;
/** @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' });
@@ -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 () => {
@@ -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 = {}> = T & {
rank?: number;
result?: SearchDocument;
} & Omit<ListItemProps, 'button'>;
/** @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<TConfig>) {
@@ -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;
}
+17
View File
@@ -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';
+15
View File
@@ -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);
};
+2
View File
@@ -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:^"