chore: updating catalog react

Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2024-08-14 13:50:46 +02:00
parent 7999f61807
commit 1612837c8e
9 changed files with 646 additions and 9 deletions
+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);
}
},
});
@@ -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 { 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` on the blueprints 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);
};