feat: creat entity content layout extension

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2025-02-06 20:01:46 +01:00
parent 22ce741a67
commit 761b56605d
14 changed files with 736 additions and 27 deletions
+9 -1
View File
@@ -26,11 +26,14 @@ app:
title: Custom
# Entity page cards
- entity-card:catalog/about
- entity-card:catalog/about:
config:
area: info
- entity-card:catalog/labels
- entity-card:catalog/links:
config:
filter: kind:component has:links
area: info
# - entity-card:linguist/languages
- entity-card:catalog-graph/relations:
config:
@@ -68,6 +71,11 @@ app:
# - entity-content:azure-devops/pull-requests
# - entity-content:azure-devops/git-tags
- catalog-overview-entity-content-layout:app/sticky:
config:
# this layout will apply to entities of kind component
filter: 'kind:component'
# scmAuthExtension: >-
# createScmAuthExtension({
# id: 'apis.scmAuth.addons.ghe',
+2
View File
@@ -44,6 +44,7 @@ import kubernetesPlugin from '@backstage/plugin-kubernetes/alpha';
import { convertLegacyPlugin } from '@backstage/core-compat-api';
import { convertLegacyPageExtension } from '@backstage/core-compat-api';
import { convertLegacyEntityContentExtension } from '@backstage/plugin-catalog-react/alpha';
import { customEntityContentOverviewLayoutModule } from './EntityPages';
/*
@@ -131,6 +132,7 @@ const app = createApp({
kubernetesPlugin,
notFoundErrorPageModule,
customHomePageModule,
customEntityContentOverviewLayoutModule,
...collectedLegacyPlugins,
],
/* Handled through config instead */
+123
View File
@@ -0,0 +1,123 @@
/*
* Copyright 2025 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, { useCallback } from 'react';
import Grid from '@material-ui/core/Grid';
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import {
OverviewEntityContentLauyoutBlueprint,
OverviewEntityContentLayoutProps,
} from '@backstage/plugin-catalog-react/alpha';
import { Entity } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles(theme => ({
[theme.breakpoints.up('sm')]: {
infoArea: {
order: 1,
},
card: {
alignSelf: 'stretch',
'& > *': {
height: '100%',
minHeight: 400,
},
},
},
}));
function StickyEntityContentOverviewLayout(
props: OverviewEntityContentLayoutProps,
) {
const { cards, buildFilterFn } = props;
const classes = useStyles();
const { entity } = useEntity();
const catalogEntityFilter = useCallback(
(options: {
filterFunction?: (entity: Entity) => boolean;
filterExpression?: string;
}) => {
const { filterFunction, filterExpression } = options;
return buildFilterFn(filterFunction, filterExpression)(entity);
},
[entity, buildFilterFn],
);
return (
<Grid container spacing={3}>
<Grid
className={classes.infoArea}
xs={12}
md={4}
style={{
position: 'sticky',
top: -16,
alignSelf: 'flex-start',
}}
item
>
<Grid container spacing={3}>
{cards
.filter(catalogEntityFilter)
.filter(card => card.area === 'info')
.map((card, index) => (
<Grid key={index} xs={12} item>
{card.element}
</Grid>
))}
</Grid>
</Grid>
<Grid xs={12} md={8} item>
<Grid container spacing={3}>
{cards
.filter(catalogEntityFilter)
.filter(card => card.area === 'glance')
.map((card, index) => (
<Grid key={index} className={classes.card} xs={12} md={6} item>
{card.element}
</Grid>
))}
{cards
.filter(catalogEntityFilter)
.filter(card => !card.area || card.area === 'main')
.map((card, index) => (
<Grid key={index} className={classes.card} xs={12} md={6} item>
{card.element}
</Grid>
))}
</Grid>
</Grid>
</Grid>
);
}
export const customEntityContentOverviewLayoutModule = createFrontendModule({
pluginId: 'app',
extensions: [
OverviewEntityContentLauyoutBlueprint.make({
name: 'sticky',
params: {
areas: ['info', 'glance', 'main'],
defaultArea: 'main',
loader: async () => StickyEntityContentOverviewLayout,
},
}),
],
});
+60
View File
@@ -108,9 +108,11 @@ const _default: FrontendPlugin<
name: 'has-apis';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -131,11 +133,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:api-docs/definition': ExtensionDefinition<{
@@ -143,9 +153,11 @@ const _default: FrontendPlugin<
name: 'definition';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -166,11 +178,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:api-docs/consumed-apis': ExtensionDefinition<{
@@ -178,9 +198,11 @@ const _default: FrontendPlugin<
name: 'consumed-apis';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -201,11 +223,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:api-docs/provided-apis': ExtensionDefinition<{
@@ -213,9 +243,11 @@ const _default: FrontendPlugin<
name: 'provided-apis';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -236,11 +268,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:api-docs/consuming-components': ExtensionDefinition<{
@@ -248,9 +288,11 @@ const _default: FrontendPlugin<
name: 'consuming-components';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -271,11 +313,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:api-docs/providing-components': ExtensionDefinition<{
@@ -283,9 +333,11 @@ const _default: FrontendPlugin<
name: 'providing-components';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -306,11 +358,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-content:api-docs/definition': ExtensionDefinition<{
+10
View File
@@ -43,6 +43,7 @@ const _default: FrontendPlugin<
height: number | undefined;
} & {
filter: string | undefined;
area: string | undefined;
};
configInput: {
height?: number | undefined;
@@ -58,6 +59,7 @@ const _default: FrontendPlugin<
relationPairs?: [string, string][] | undefined;
} & {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -78,6 +80,13 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {
[x: string]: ExtensionInput<
@@ -93,6 +102,7 @@ const _default: FrontendPlugin<
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'page:catalog-graph': ExtensionDefinition<{
+104
View File
@@ -12,6 +12,7 @@ import { Entity } from '@backstage/catalog-model';
import { ExtensionBlueprint } from '@backstage/frontend-plugin-api';
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { JSX as JSX_2 } from 'react';
import { default as React_2 } from 'react';
import { ResourcePermission } from '@backstage/plugin-permission-common';
import { RouteRef } from '@backstage/frontend-plugin-api';
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
@@ -113,6 +114,7 @@ export const EntityCardBlueprint: ExtensionBlueprint<{
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
output:
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
@@ -129,13 +131,22 @@ export const EntityCardBlueprint: ExtensionBlueprint<{
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
dataRefs: {
filterFunction: ConfigurableExtensionDataRef<
@@ -148,6 +159,7 @@ export const EntityCardBlueprint: ExtensionBlueprint<{
'catalog.entity-filter-expression',
{}
>;
area: ConfigurableExtensionDataRef<string, 'catalog.entity-card-area', {}>;
};
}>;
@@ -240,6 +252,98 @@ export const EntityContentBlueprint: ExtensionBlueprint<{
// @alpha
export function isOwnerOf(owner: Entity, entity: Entity): boolean;
// @alpha (undocumented)
export const OverviewEntityContentLauyoutBlueprint: ExtensionBlueprint<{
kind: 'catalog-overview-entity-content-layout';
name: undefined;
params: {
areas: Array<string>;
defaultArea: string;
defaultFilter?: string | ((entity: Entity) => boolean) | undefined;
loader: () => Promise<
(props: OverviewEntityContentLayoutProps) => React_2.JSX.Element
>;
};
output:
| ConfigurableExtensionDataRef<string, 'catalog.entity-card-area', {}>
| ConfigurableExtensionDataRef<
string[],
'catalog.overview-entity-content.layout.areas',
{}
>
| ConfigurableExtensionDataRef<
(props: OverviewEntityContentLayoutProps) => React_2.JSX.Element,
'catalog.overview-entity-content.layout.page-component',
{}
>
| ConfigurableExtensionDataRef<
(entity: Entity) => boolean,
'catalog.overview-entity-content.layout.filter-function',
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.overview-entity-content.layout.filter-expression',
{
optional: true;
}
>;
inputs: {};
config: {
area: string | undefined;
filter: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
dataRefs: {
areas: ConfigurableExtensionDataRef<
string[],
'catalog.overview-entity-content.layout.areas',
{}
>;
defaultArea: ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{}
>;
filterFunction: ConfigurableExtensionDataRef<
(entity: Entity) => boolean,
'catalog.overview-entity-content.layout.filter-function',
{}
>;
filterExpression: ConfigurableExtensionDataRef<
string,
'catalog.overview-entity-content.layout.filter-expression',
{}
>;
component: ConfigurableExtensionDataRef<
(props: OverviewEntityContentLayoutProps) => React_2.JSX.Element,
'catalog.overview-entity-content.layout.page-component',
{}
>;
};
}>;
// @alpha (undocumented)
export interface OverviewEntityContentLayoutProps {
// (undocumented)
buildFilterFn: (
filterFunction?: (entity: Entity) => boolean,
filterExpression?: string,
) => (entity: Entity) => boolean;
// (undocumented)
cards: Array<{
area?: string;
element: React_2.JSX.Element;
filterFunction?: (entity: Entity) => boolean;
filterExpression?: string;
}>;
}
// @alpha
export function useEntityPermission(
permission: ResourcePermission<'catalog-entity'>,
@@ -51,6 +51,9 @@ describe('EntityCardBlueprint', () => {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"area": {
"type": "string",
},
"filter": {
"type": "string",
},
@@ -83,6 +86,15 @@ describe('EntityCardBlueprint', () => {
"optional": [Function],
"toString": [Function],
},
{
"$$type": "@backstage/ExtensionDataRef",
"config": {
"optional": true,
},
"id": "catalog.entity-card-area",
"optional": [Function],
"toString": [Function],
},
],
"override": [Function],
"toString": [Function],
@@ -22,6 +22,8 @@ import {
import {
entityFilterFunctionDataRef,
entityFilterExpressionDataRef,
entityCardAreaDataRef,
defaultEntityCardArea,
} from './extensionData';
/**
@@ -35,25 +37,30 @@ export const EntityCardBlueprint = createExtensionBlueprint({
coreExtensionData.reactElement,
entityFilterFunctionDataRef.optional(),
entityFilterExpressionDataRef.optional(),
entityCardAreaDataRef.optional(),
],
dataRefs: {
filterFunction: entityFilterFunctionDataRef,
filterExpression: entityFilterExpressionDataRef,
area: entityCardAreaDataRef,
},
config: {
schema: {
filter: z => z.string().optional(),
area: z => z.string().optional(),
},
},
*factory(
{
loader,
filter,
defaultArea,
}: {
loader: () => Promise<JSX.Element>;
filter?:
| typeof entityFilterFunctionDataRef.T
| typeof entityFilterExpressionDataRef.T;
defaultArea?: (typeof defaultEntityCardArea)[number];
},
{ node, config },
) {
@@ -66,5 +73,10 @@ export const EntityCardBlueprint = createExtensionBlueprint({
} else if (typeof filter === 'function') {
yield entityFilterFunctionDataRef(filter);
}
const area = config.area ?? defaultArea;
if (area) {
yield entityCardAreaDataRef(area);
}
},
});
@@ -0,0 +1,137 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import {
createExtensionDataRef,
createExtensionBlueprint,
ExtensionBoundary,
} from '@backstage/frontend-plugin-api';
import React, { lazy as reactLazy, ComponentProps } from 'react';
import { EntityCardBlueprint } from './EntityCardBlueprint';
/** @alpha */
export interface OverviewEntityContentLayoutProps {
buildFilterFn: (
filterFunction?: (entity: Entity) => boolean,
filterExpression?: string,
) => (entity: Entity) => boolean;
cards: Array<{
area?: string;
element: React.JSX.Element;
filterFunction?: (entity: Entity) => boolean;
filterExpression?: string;
}>;
}
const catalogOverviewEntityContentLayoutAreasDataRef = createExtensionDataRef<
Array<string>
>().with({
id: 'catalog.overview-entity-content.layout.areas',
});
const catalogOverviewEntityContentLayoutFilterFunctionDataRef =
createExtensionDataRef<(entity: Entity) => boolean>().with({
id: 'catalog.overview-entity-content.layout.filter-function',
});
const catalogOverviewEntityContentLayoutFilterExpressionDataRef =
createExtensionDataRef<string>().with({
id: 'catalog.overview-entity-content.layout.filter-expression',
});
const catalogOverviewEntityContentLayoutComponentDataRef =
createExtensionDataRef<
(props: OverviewEntityContentLayoutProps) => React.JSX.Element
>().with({
id: 'catalog.overview-entity-content.layout.page-component',
});
/** @alpha */
export const OverviewEntityContentLauyoutBlueprint = createExtensionBlueprint({
kind: 'catalog-overview-entity-content-layout',
attachTo: { id: 'entity-content:catalog/overview', input: 'layouts' },
output: [
catalogOverviewEntityContentLayoutAreasDataRef,
EntityCardBlueprint.dataRefs.area,
catalogOverviewEntityContentLayoutFilterFunctionDataRef.optional(),
catalogOverviewEntityContentLayoutFilterExpressionDataRef.optional(),
catalogOverviewEntityContentLayoutComponentDataRef,
],
dataRefs: {
areas: catalogOverviewEntityContentLayoutAreasDataRef,
defaultArea: EntityCardBlueprint.dataRefs.area,
filterFunction: catalogOverviewEntityContentLayoutFilterFunctionDataRef,
filterExpression: catalogOverviewEntityContentLayoutFilterExpressionDataRef,
component: catalogOverviewEntityContentLayoutComponentDataRef,
},
config: {
schema: {
area: z => z.string().optional(),
filter: z => z.string().optional(),
},
},
*factory(
{
loader,
areas,
defaultArea,
defaultFilter = () => false, // do not use the layout if a default filter is undefined
}: {
areas: Array<string>;
defaultArea: (typeof areas)[number]; // fixed typescript area type
defaultFilter?:
| typeof catalogOverviewEntityContentLayoutFilterFunctionDataRef.T
| typeof catalogOverviewEntityContentLayoutFilterExpressionDataRef.T;
loader: () => Promise<
(props: OverviewEntityContentLayoutProps) => React.JSX.Element
>;
},
{ node, config },
) {
yield catalogOverviewEntityContentLayoutAreasDataRef(areas);
yield EntityCardBlueprint.dataRefs.area(config.area ?? defaultArea);
if (config.filter) {
yield catalogOverviewEntityContentLayoutFilterExpressionDataRef(
config.filter,
);
} else if (typeof defaultFilter === 'string') {
yield catalogOverviewEntityContentLayoutFilterExpressionDataRef(
defaultFilter,
);
} else if (typeof defaultFilter === 'function') {
yield catalogOverviewEntityContentLayoutFilterFunctionDataRef(
defaultFilter,
);
}
const ExtensionComponent = reactLazy(() =>
loader().then(component => ({ default: component })),
);
yield catalogOverviewEntityContentLayoutComponentDataRef(
(props: ComponentProps<typeof ExtensionComponent>) => {
return (
<ExtensionBoundary node={node}>
<ExtensionComponent {...props} />
</ExtensionBoundary>
);
},
);
},
});
@@ -50,3 +50,14 @@ export const entityContentGroupDataRef = createExtensionDataRef<
>().with({
id: 'catalog.entity-content-group',
});
/**
* @alpha
* Default entity content groups.
*/
export const defaultEntityCardArea = ['info', 'glance', 'main'] as const;
/** @internal */
export const entityCardAreaDataRef = createExtensionDataRef<string>().with({
id: 'catalog.entity-card-area',
});
@@ -15,4 +15,8 @@
*/
export { EntityCardBlueprint } from './EntityCardBlueprint';
export { EntityContentBlueprint } from './EntityContentBlueprint';
export {
OverviewEntityContentLauyoutBlueprint,
type OverviewEntityContentLayoutProps,
} from './OverviewEntityContentLauyoutBlueprint';
export { defaultEntityContentGroups } from './extensionData';
+139
View File
@@ -17,6 +17,7 @@ import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
import { FrontendPlugin } from '@backstage/frontend-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
import { JSX as JSX_2 } from 'react';
import { OverviewEntityContentLayoutProps } from '@backstage/plugin-catalog-react/alpha';
import { RouteRef } from '@backstage/frontend-plugin-api';
import { SearchResultItemExtensionComponent } from '@backstage/plugin-search-react/alpha';
import { SearchResultItemExtensionPredicate } from '@backstage/plugin-search-react/alpha';
@@ -217,9 +218,11 @@ const _default: FrontendPlugin<
name: 'about';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
@@ -236,11 +239,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/links': ExtensionDefinition<{
@@ -248,9 +259,11 @@ const _default: FrontendPlugin<
name: 'links';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
@@ -267,11 +280,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/labels': ExtensionDefinition<{
@@ -279,9 +300,11 @@ const _default: FrontendPlugin<
name: 'labels';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
@@ -298,11 +321,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/depends-on-components': ExtensionDefinition<{
@@ -310,9 +341,11 @@ const _default: FrontendPlugin<
name: 'depends-on-components';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
@@ -329,11 +362,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/depends-on-resources': ExtensionDefinition<{
@@ -341,9 +382,11 @@ const _default: FrontendPlugin<
name: 'depends-on-resources';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
@@ -360,11 +403,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/has-components': ExtensionDefinition<{
@@ -372,9 +423,11 @@ const _default: FrontendPlugin<
name: 'has-components';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
@@ -391,11 +444,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/has-resources': ExtensionDefinition<{
@@ -403,9 +464,11 @@ const _default: FrontendPlugin<
name: 'has-resources';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
@@ -422,11 +485,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/has-subcomponents': ExtensionDefinition<{
@@ -434,9 +505,11 @@ const _default: FrontendPlugin<
name: 'has-subcomponents';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
@@ -453,11 +526,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/has-subdomains': ExtensionDefinition<{
@@ -465,9 +546,11 @@ const _default: FrontendPlugin<
name: 'has-subdomains';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
@@ -484,11 +567,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/has-systems': ExtensionDefinition<{
@@ -496,9 +587,11 @@ const _default: FrontendPlugin<
name: 'has-systems';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
@@ -515,11 +608,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-content:catalog/overview': ExtensionDefinition<{
@@ -572,6 +673,37 @@ const _default: FrontendPlugin<
}
>;
inputs: {
layouts: ExtensionInput<
| ConfigurableExtensionDataRef<string, 'catalog.entity-card-area', {}>
| ConfigurableExtensionDataRef<
string[],
'catalog.overview-entity-content.layout.areas',
{}
>
| ConfigurableExtensionDataRef<
(props: OverviewEntityContentLayoutProps) => JSX_2.Element,
'catalog.overview-entity-content.layout.page-component',
{}
>
| ConfigurableExtensionDataRef<
(entity: Entity) => boolean,
'catalog.overview-entity-content.layout.filter-function',
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.overview-entity-content.layout.filter-expression',
{
optional: true;
}
>,
{
singleton: false;
optional: false;
}
>;
cards: ExtensionInput<
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ConfigurableExtensionDataRef<
@@ -587,6 +719,13 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>,
{
singleton: false;
+73 -26
View File
@@ -19,38 +19,85 @@ import {
coreExtensionData,
createExtensionInput,
} from '@backstage/frontend-plugin-api';
import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha';
import {
EntityCardBlueprint,
EntityContentBlueprint,
OverviewEntityContentLauyoutBlueprint,
} from '@backstage/plugin-catalog-react/alpha';
import { buildFilterFn } from './filter/FilterWrapper';
import { useEntity } from '@backstage/plugin-catalog-react';
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(
const catalogOverviewEntityContent = EntityContentBlueprint.makeWithOverrides({
name: 'overview',
inputs: {
layouts: createExtensionInput([
OverviewEntityContentLauyoutBlueprint.dataRefs.areas,
OverviewEntityContentLauyoutBlueprint.dataRefs.defaultArea,
OverviewEntityContentLauyoutBlueprint.dataRefs.filterFunction.optional(),
OverviewEntityContentLauyoutBlueprint.dataRefs.filterExpression.optional(),
OverviewEntityContentLauyoutBlueprint.dataRefs.component,
]),
cards: createExtensionInput([
coreExtensionData.reactElement,
EntityContentBlueprint.dataRefs.filterFunction.optional(),
EntityContentBlueprint.dataRefs.filterExpression.optional(),
EntityCardBlueprint.dataRefs.area.optional(),
]),
},
factory: (originalFactory, { inputs }) => {
return originalFactory({
defaultPath: '/',
defaultTitle: 'Overview',
loader: async () => {
const defaultLayout = await import('./EntityOverviewPage').then(
m => m.EntityOverviewPage,
);
const Component = () => {
const { entity } = useEntity();
// Use the first layout that matches the entity filter
const layout = inputs.layouts.find(l => {
const filterFunction = l.get(
OverviewEntityContentLauyoutBlueprint.dataRefs.filterFunction,
);
const filterExpression = l.get(
OverviewEntityContentLauyoutBlueprint.dataRefs.filterExpression,
);
return buildFilterFn(filterFunction, filterExpression)(entity);
});
const Layout =
layout?.get(
OverviewEntityContentLauyoutBlueprint.dataRefs.component,
) ?? defaultLayout;
return (
<Layout
buildFilterFn={buildFilterFn}
cards={inputs.cards.map(card => ({
element: card.get(coreExtensionData.reactElement),
filterFunction: card.get(
EntityContentBlueprint.dataRefs.filterFunction,
),
filterExpression: c.get(
filterExpression: card.get(
EntityContentBlueprint.dataRefs.filterExpression,
),
area:
card.get(EntityCardBlueprint.dataRefs.area) ??
layout?.get(
OverviewEntityContentLauyoutBlueprint.dataRefs.defaultArea,
),
}))}
/>
)),
});
},
});
);
};
return <Component />;
},
});
},
});
export default [catalogOverviewEntityContent];
+40
View File
@@ -22,9 +22,11 @@ const _default: FrontendPlugin<
name: 'group-profile';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -45,11 +47,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:org/members-list': ExtensionDefinition<{
@@ -57,9 +67,11 @@ const _default: FrontendPlugin<
name: 'members-list';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -80,11 +92,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:org/ownership': ExtensionDefinition<{
@@ -92,9 +112,11 @@ const _default: FrontendPlugin<
name: 'ownership';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -115,11 +137,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:org/user-profile': ExtensionDefinition<{
@@ -127,9 +157,11 @@ const _default: FrontendPlugin<
name: 'user-profile';
config: {
filter: string | undefined;
area: string | undefined;
};
configInput: {
filter?: string | undefined;
area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -150,11 +182,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-card-area',
{
optional: true;
}
>;
inputs: {};
params: {
loader: () => Promise<JSX.Element>;
filter?: string | ((entity: Entity) => boolean) | undefined;
defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
}