refactor: apply review suggestions
Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com> Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': minor
|
||||
---
|
||||
|
||||
Introduces a new `EntityCardLayoutBlueprint` that creates custom entity content layouts.
|
||||
|
||||
The layout components receive card elements and can render them as they see fit. Cards is an array of objects with the following properties:
|
||||
|
||||
- element: `JSx.Element`;
|
||||
- area: `"peek" | "info" | "full" | undefined`;
|
||||
|
||||
### Usage example
|
||||
|
||||
Creating a custom overview tab layout:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
EntityCardLayoutProps,
|
||||
EntityCardLayoutBlueprint,
|
||||
} from '@backstage/plugin-catalog-react/alpha';
|
||||
// ...
|
||||
|
||||
function StickyEntityContentOverviewLayout(props: EntityCardLayoutProps) {
|
||||
const { cards } = props;
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
<Grid
|
||||
className={classes.infoArea}
|
||||
xs={12}
|
||||
md={4}
|
||||
item
|
||||
>
|
||||
<Grid container spacing={3}>
|
||||
{cards
|
||||
.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(card => card.area === 'peek')
|
||||
.map((card, index) => (
|
||||
<Grid key={index} className={classes.card} xs={12} md={6} item>
|
||||
{card.element}
|
||||
</Grid>
|
||||
))}
|
||||
{cards
|
||||
.filter(card => !card.area || card.area === 'full')
|
||||
.map((card, index) => (
|
||||
<Grid key={index} className={classes.card} xs={12} md={6} item>
|
||||
{card.element}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
export const customEntityContentOverviewStickyLayoutModule = createFrontendModule({
|
||||
pluginId: 'app',
|
||||
extensions: [
|
||||
EntityCardLayoutBlueprint.make({
|
||||
name: 'sticky',
|
||||
params: {
|
||||
// (optional) defaults the `() => false` filter function
|
||||
defaultFilter: 'kind:template'
|
||||
loader: async () => StickyEntityContentOverviewLayout,
|
||||
},
|
||||
}),
|
||||
],
|
||||
```
|
||||
|
||||
Disabling the custom layout:
|
||||
|
||||
```yaml
|
||||
# app-config.yaml
|
||||
app:
|
||||
extensions:
|
||||
- entity-card-layout:app/sticky: false
|
||||
```
|
||||
|
||||
Overriding the custom layout filter:
|
||||
|
||||
```yaml
|
||||
# app-config.yaml
|
||||
app:
|
||||
extensions:
|
||||
- entity-card-layout:app/sticky:
|
||||
config:
|
||||
# This layout will be used only with component entities
|
||||
filter: 'kind:component'
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': minor
|
||||
---
|
||||
|
||||
The `Overview` entity content now supports custom cards grid layouts.
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': minor
|
||||
---
|
||||
|
||||
Add an optional `area` parameter to `EntityCard` extensions. A card's area value determines where it should be rendered by the entity content layout, as well as its maximum size.
|
||||
|
||||
We are initially supporting only three areas:
|
||||
|
||||
- `peek`: used for cards containing infrastucture information (e.g. last builds, deployments, etc.).
|
||||
- `info`: used for cards that contain entity metadata (e.g. about, links);
|
||||
- `full`: Contains information that plugins add to an entity (e.g. PagerDuty incidents and on-call escalation).
|
||||
|
||||
### Usage examples
|
||||
|
||||
Defining a default area when creating a card:
|
||||
|
||||
```diff
|
||||
const myCard = EntityCardBlueprint.make({
|
||||
name: 'myCard',
|
||||
params: {
|
||||
+ defaultArea: 'info',
|
||||
loader: import('./MyCard).then(m => { default: m.MyCard }),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Changing the card area via `app-config.yaml` file:
|
||||
|
||||
```diff
|
||||
app:
|
||||
extensions:
|
||||
+ - entity-card:myPlugin/myCard:
|
||||
+ config:
|
||||
+ area: info
|
||||
```
|
||||
@@ -71,7 +71,7 @@ app:
|
||||
# - entity-content:azure-devops/pull-requests
|
||||
# - entity-content:azure-devops/git-tags
|
||||
|
||||
- catalog-overview-entity-content-layout:app/sticky:
|
||||
- entity-card-layout:app/sticky:
|
||||
config:
|
||||
# this layout will apply to entities of kind component
|
||||
filter: 'kind:component'
|
||||
|
||||
@@ -14,15 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import { createFrontendModule } from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
OverviewEntityContentLauyoutBlueprint,
|
||||
OverviewEntityContentLayoutProps,
|
||||
EntityCardLayoutBlueprint,
|
||||
EntityCardLayoutProps,
|
||||
} 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 => ({
|
||||
@@ -40,26 +38,9 @@ const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
function StickyEntityContentOverviewLayout(
|
||||
props: OverviewEntityContentLayoutProps,
|
||||
) {
|
||||
const { cards, buildFilterFn } = props;
|
||||
|
||||
function StickyEntityContentOverviewLayout(props: EntityCardLayoutProps) {
|
||||
const { cards } = 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
|
||||
@@ -75,7 +56,6 @@ function StickyEntityContentOverviewLayout(
|
||||
>
|
||||
<Grid container spacing={3}>
|
||||
{cards
|
||||
.filter(catalogEntityFilter)
|
||||
.filter(card => card.area === 'info')
|
||||
.map((card, index) => (
|
||||
<Grid key={index} xs={12} item>
|
||||
@@ -87,16 +67,14 @@ function StickyEntityContentOverviewLayout(
|
||||
<Grid xs={12} md={8} item>
|
||||
<Grid container spacing={3}>
|
||||
{cards
|
||||
.filter(catalogEntityFilter)
|
||||
.filter(card => card.area === 'glance')
|
||||
.filter(card => card.area === 'peek')
|
||||
.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')
|
||||
.filter(card => !card.area || card.area === 'full')
|
||||
.map((card, index) => (
|
||||
<Grid key={index} className={classes.card} xs={12} md={6} item>
|
||||
{card.element}
|
||||
@@ -111,11 +89,9 @@ function StickyEntityContentOverviewLayout(
|
||||
export const customEntityContentOverviewLayoutModule = createFrontendModule({
|
||||
pluginId: 'app',
|
||||
extensions: [
|
||||
OverviewEntityContentLauyoutBlueprint.make({
|
||||
EntityCardLayoutBlueprint.make({
|
||||
name: 'sticky',
|
||||
params: {
|
||||
areas: ['info', 'glance', 'main'],
|
||||
defaultArea: 'main',
|
||||
loader: async () => StickyEntityContentOverviewLayout,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -108,11 +108,11 @@ const _default: FrontendPlugin<
|
||||
name: 'has-apis';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<
|
||||
@@ -135,7 +135,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -145,7 +145,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:api-docs/definition': ExtensionDefinition<{
|
||||
@@ -153,11 +153,11 @@ const _default: FrontendPlugin<
|
||||
name: 'definition';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<
|
||||
@@ -180,7 +180,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -190,7 +190,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:api-docs/consumed-apis': ExtensionDefinition<{
|
||||
@@ -198,11 +198,11 @@ const _default: FrontendPlugin<
|
||||
name: 'consumed-apis';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<
|
||||
@@ -225,7 +225,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -235,7 +235,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:api-docs/provided-apis': ExtensionDefinition<{
|
||||
@@ -243,11 +243,11 @@ const _default: FrontendPlugin<
|
||||
name: 'provided-apis';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<
|
||||
@@ -270,7 +270,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -280,7 +280,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:api-docs/consuming-components': ExtensionDefinition<{
|
||||
@@ -288,11 +288,11 @@ const _default: FrontendPlugin<
|
||||
name: 'consuming-components';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<
|
||||
@@ -315,7 +315,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -325,7 +325,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:api-docs/providing-components': ExtensionDefinition<{
|
||||
@@ -333,11 +333,11 @@ const _default: FrontendPlugin<
|
||||
name: 'providing-components';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<
|
||||
@@ -360,7 +360,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -370,7 +370,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-content:api-docs/definition': ExtensionDefinition<{
|
||||
|
||||
@@ -43,7 +43,7 @@ const _default: FrontendPlugin<
|
||||
height: number | undefined;
|
||||
} & {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
height?: number | undefined;
|
||||
@@ -59,7 +59,7 @@ const _default: FrontendPlugin<
|
||||
relationPairs?: [string, string][] | undefined;
|
||||
} & {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<
|
||||
@@ -82,7 +82,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -102,7 +102,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'page:catalog-graph': ExtensionDefinition<{
|
||||
|
||||
@@ -99,6 +99,9 @@ export function convertLegacyEntityContentExtension(
|
||||
},
|
||||
): ExtensionDefinition;
|
||||
|
||||
// @alpha
|
||||
export const defaultEntityCardAreas: readonly ['peek', 'info', 'full'];
|
||||
|
||||
// @alpha
|
||||
export const defaultEntityContentGroups: {
|
||||
documentation: string;
|
||||
@@ -114,7 +117,7 @@ export const EntityCardBlueprint: ExtensionBlueprint<{
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
|
||||
@@ -133,7 +136,7 @@ export const EntityCardBlueprint: ExtensionBlueprint<{
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -142,7 +145,65 @@ export const EntityCardBlueprint: ExtensionBlueprint<{
|
||||
inputs: {};
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
dataRefs: {
|
||||
filterFunction: ConfigurableExtensionDataRef<
|
||||
(entity: Entity) => boolean,
|
||||
'catalog.entity-filter-function',
|
||||
{}
|
||||
>;
|
||||
filterExpression: ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'catalog.entity-filter-expression',
|
||||
{}
|
||||
>;
|
||||
area: ConfigurableExtensionDataRef<
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{}
|
||||
>;
|
||||
};
|
||||
}>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const EntityCardLayoutBlueprint: ExtensionBlueprint<{
|
||||
kind: 'entity-card-layout';
|
||||
name: undefined;
|
||||
params: {
|
||||
defaultFilter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
loader: () => Promise<
|
||||
(props: EntityCardLayoutProps) => React_2.JSX.Element
|
||||
>;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<
|
||||
(entity: Entity) => boolean,
|
||||
'catalog.entity-filter-function',
|
||||
{
|
||||
optional: true;
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'catalog.entity-filter-expression',
|
||||
{
|
||||
optional: true;
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
(props: EntityCardLayoutProps) => React_2.JSX.Element,
|
||||
'catalog.entity-card-layout.component',
|
||||
{}
|
||||
>;
|
||||
inputs: {};
|
||||
config: {
|
||||
area: string | undefined;
|
||||
filter: string | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
@@ -159,10 +220,23 @@ export const EntityCardBlueprint: ExtensionBlueprint<{
|
||||
'catalog.entity-filter-expression',
|
||||
{}
|
||||
>;
|
||||
area: ConfigurableExtensionDataRef<string, 'catalog.entity-card-area', {}>;
|
||||
component: ConfigurableExtensionDataRef<
|
||||
(props: EntityCardLayoutProps) => React_2.JSX.Element,
|
||||
'catalog.entity-card-layout.component',
|
||||
{}
|
||||
>;
|
||||
};
|
||||
}>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface EntityCardLayoutProps {
|
||||
// (undocumented)
|
||||
cards: Array<{
|
||||
area?: (typeof defaultEntityCardAreas)[number];
|
||||
element: React_2.JSX.Element;
|
||||
}>;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export const EntityContentBlueprint: ExtensionBlueprint<{
|
||||
kind: 'entity-content';
|
||||
@@ -252,98 +326,6 @@ 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'>,
|
||||
|
||||
@@ -52,6 +52,11 @@ describe('EntityCardBlueprint', () => {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"area": {
|
||||
"enum": [
|
||||
"peek",
|
||||
"info",
|
||||
"full",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"filter": {
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
entityFilterFunctionDataRef,
|
||||
entityFilterExpressionDataRef,
|
||||
entityCardAreaDataRef,
|
||||
defaultEntityCardArea,
|
||||
defaultEntityCardAreas,
|
||||
} from './extensionData';
|
||||
|
||||
/**
|
||||
@@ -47,7 +47,7 @@ export const EntityCardBlueprint = createExtensionBlueprint({
|
||||
config: {
|
||||
schema: {
|
||||
filter: z => z.string().optional(),
|
||||
area: z => z.string().optional(),
|
||||
area: z => z.enum(defaultEntityCardAreas).optional(),
|
||||
},
|
||||
},
|
||||
*factory(
|
||||
@@ -60,7 +60,7 @@ export const EntityCardBlueprint = createExtensionBlueprint({
|
||||
filter?:
|
||||
| typeof entityFilterFunctionDataRef.T
|
||||
| typeof entityFilterExpressionDataRef.T;
|
||||
defaultArea?: (typeof defaultEntityCardArea)[number];
|
||||
defaultArea?: (typeof defaultEntityCardAreas)[number];
|
||||
},
|
||||
{ node, config },
|
||||
) {
|
||||
@@ -77,6 +77,11 @@ export const EntityCardBlueprint = createExtensionBlueprint({
|
||||
const area = config.area ?? defaultArea;
|
||||
if (area) {
|
||||
yield entityCardAreaDataRef(area);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`DEPRECATION WARNING: Not providing defaultArea for entity cards is deprecated. Missing from '${node.spec.id}'`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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 {
|
||||
createExtensionDataRef,
|
||||
createExtensionBlueprint,
|
||||
ExtensionBoundary,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
entityFilterExpressionDataRef,
|
||||
entityFilterFunctionDataRef,
|
||||
defaultEntityCardAreas,
|
||||
} from './extensionData';
|
||||
import React, { lazy as reactLazy, ComponentProps } from 'react';
|
||||
|
||||
/** @alpha */
|
||||
export interface EntityCardLayoutProps {
|
||||
cards: Array<{
|
||||
area?: (typeof defaultEntityCardAreas)[number];
|
||||
element: React.JSX.Element;
|
||||
}>;
|
||||
}
|
||||
|
||||
const entityCardLayoutComponentDataRef = createExtensionDataRef<
|
||||
(props: EntityCardLayoutProps) => React.JSX.Element
|
||||
>().with({
|
||||
id: 'catalog.entity-card-layout.component',
|
||||
});
|
||||
|
||||
/** @alpha */
|
||||
export const EntityCardLayoutBlueprint = createExtensionBlueprint({
|
||||
kind: 'entity-card-layout',
|
||||
attachTo: { id: 'entity-content:catalog/overview', input: 'layouts' },
|
||||
output: [
|
||||
entityFilterFunctionDataRef.optional(),
|
||||
entityFilterExpressionDataRef.optional(),
|
||||
entityCardLayoutComponentDataRef,
|
||||
],
|
||||
dataRefs: {
|
||||
filterFunction: entityFilterFunctionDataRef,
|
||||
filterExpression: entityFilterExpressionDataRef,
|
||||
component: entityCardLayoutComponentDataRef,
|
||||
},
|
||||
config: {
|
||||
schema: {
|
||||
area: z => z.string().optional(),
|
||||
filter: z => z.string().optional(),
|
||||
},
|
||||
},
|
||||
*factory(
|
||||
{
|
||||
loader,
|
||||
defaultFilter,
|
||||
}: {
|
||||
defaultFilter?:
|
||||
| typeof entityFilterFunctionDataRef.T
|
||||
| typeof entityFilterExpressionDataRef.T;
|
||||
loader: () => Promise<
|
||||
(props: EntityCardLayoutProps) => React.JSX.Element
|
||||
>;
|
||||
},
|
||||
{ node, config },
|
||||
) {
|
||||
if (config.filter) {
|
||||
yield entityFilterExpressionDataRef(config.filter);
|
||||
} else if (typeof defaultFilter === 'string') {
|
||||
yield entityFilterExpressionDataRef(defaultFilter);
|
||||
} else if (typeof defaultFilter === 'function') {
|
||||
yield entityFilterFunctionDataRef(defaultFilter);
|
||||
}
|
||||
|
||||
const ExtensionComponent = reactLazy(() =>
|
||||
loader().then(component => ({ default: component })),
|
||||
);
|
||||
|
||||
yield entityCardLayoutComponentDataRef(
|
||||
(props: ComponentProps<typeof ExtensionComponent>) => {
|
||||
return (
|
||||
<ExtensionBoundary node={node}>
|
||||
<ExtensionComponent {...props} />
|
||||
</ExtensionBoundary>
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -1,137 +0,0 @@
|
||||
/*
|
||||
* 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>
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -55,9 +55,11 @@ export const entityContentGroupDataRef = createExtensionDataRef<
|
||||
* @alpha
|
||||
* Default entity content groups.
|
||||
*/
|
||||
export const defaultEntityCardArea = ['info', 'glance', 'main'] as const;
|
||||
export const defaultEntityCardAreas = ['peek', 'info', 'full'] as const;
|
||||
|
||||
/** @internal */
|
||||
export const entityCardAreaDataRef = createExtensionDataRef<string>().with({
|
||||
export const entityCardAreaDataRef = createExtensionDataRef<
|
||||
(typeof defaultEntityCardAreas)[number]
|
||||
>().with({
|
||||
id: 'catalog.entity-card-area',
|
||||
});
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
export { EntityCardBlueprint } from './EntityCardBlueprint';
|
||||
export { EntityContentBlueprint } from './EntityContentBlueprint';
|
||||
export {
|
||||
OverviewEntityContentLauyoutBlueprint,
|
||||
type OverviewEntityContentLayoutProps,
|
||||
} from './OverviewEntityContentLauyoutBlueprint';
|
||||
export { defaultEntityContentGroups } from './extensionData';
|
||||
EntityCardLayoutBlueprint,
|
||||
type EntityCardLayoutProps,
|
||||
} from './EntityCardLauyoutBlueprint';
|
||||
export {
|
||||
defaultEntityContentGroups,
|
||||
defaultEntityCardAreas,
|
||||
} from './extensionData';
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AnyExtensionDataRef } 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 { EntityCardLayoutProps } from '@backstage/plugin-catalog-react/alpha';
|
||||
import { ExtensionBlueprint } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionInput } from '@backstage/frontend-plugin-api';
|
||||
@@ -17,7 +18,6 @@ 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';
|
||||
@@ -218,11 +218,11 @@ const _default: FrontendPlugin<
|
||||
name: 'about';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
|
||||
@@ -241,7 +241,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -251,7 +251,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:catalog/links': ExtensionDefinition<{
|
||||
@@ -259,11 +259,11 @@ const _default: FrontendPlugin<
|
||||
name: 'links';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
|
||||
@@ -282,7 +282,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -292,7 +292,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:catalog/labels': ExtensionDefinition<{
|
||||
@@ -300,11 +300,11 @@ const _default: FrontendPlugin<
|
||||
name: 'labels';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
|
||||
@@ -323,7 +323,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -333,7 +333,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:catalog/depends-on-components': ExtensionDefinition<{
|
||||
@@ -341,11 +341,11 @@ const _default: FrontendPlugin<
|
||||
name: 'depends-on-components';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
|
||||
@@ -364,7 +364,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -374,7 +374,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:catalog/depends-on-resources': ExtensionDefinition<{
|
||||
@@ -382,11 +382,11 @@ const _default: FrontendPlugin<
|
||||
name: 'depends-on-resources';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
|
||||
@@ -405,7 +405,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -415,7 +415,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:catalog/has-components': ExtensionDefinition<{
|
||||
@@ -423,11 +423,11 @@ const _default: FrontendPlugin<
|
||||
name: 'has-components';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
|
||||
@@ -446,7 +446,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -456,7 +456,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:catalog/has-resources': ExtensionDefinition<{
|
||||
@@ -464,11 +464,11 @@ const _default: FrontendPlugin<
|
||||
name: 'has-resources';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
|
||||
@@ -487,7 +487,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -497,7 +497,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:catalog/has-subcomponents': ExtensionDefinition<{
|
||||
@@ -505,11 +505,11 @@ const _default: FrontendPlugin<
|
||||
name: 'has-subcomponents';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
|
||||
@@ -528,7 +528,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -538,7 +538,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:catalog/has-subdomains': ExtensionDefinition<{
|
||||
@@ -546,11 +546,11 @@ const _default: FrontendPlugin<
|
||||
name: 'has-subdomains';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
|
||||
@@ -569,7 +569,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -579,7 +579,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:catalog/has-systems': ExtensionDefinition<{
|
||||
@@ -587,11 +587,11 @@ const _default: FrontendPlugin<
|
||||
name: 'has-systems';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
|
||||
@@ -610,7 +610,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -620,7 +620,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-content:catalog/overview': ExtensionDefinition<{
|
||||
@@ -674,30 +674,24 @@ 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',
|
||||
'catalog.entity-filter-function',
|
||||
{
|
||||
optional: true;
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'catalog.overview-entity-content.layout.filter-expression',
|
||||
'catalog.entity-filter-expression',
|
||||
{
|
||||
optional: true;
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
(props: EntityCardLayoutProps) => JSX_2.Element,
|
||||
'catalog.entity-card-layout.component',
|
||||
{}
|
||||
>,
|
||||
{
|
||||
singleton: false;
|
||||
@@ -721,7 +715,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import React from 'react';
|
||||
@@ -36,8 +35,6 @@ import {
|
||||
interface EntityOverviewPageProps {
|
||||
cards: Array<{
|
||||
element: React.JSX.Element;
|
||||
filterFunction?: (entity: Entity) => boolean;
|
||||
filterExpression?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* 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, { Fragment } from 'react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
// import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
createExtensionTester,
|
||||
renderInTestApp,
|
||||
TestApiProvider,
|
||||
} from '@backstage/frontend-test-utils';
|
||||
import { catalogOverviewEntityContent } from './entityContents';
|
||||
import {
|
||||
EntityCardBlueprint,
|
||||
EntityCardLayoutBlueprint,
|
||||
} from '@backstage/plugin-catalog-react/alpha';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import {
|
||||
catalogApiRef,
|
||||
EntityProvider,
|
||||
MockStarredEntitiesApi,
|
||||
starredEntitiesApiRef,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
|
||||
describe('Overview content', () => {
|
||||
const entityMock = {
|
||||
metadata: {
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'file:/Users/camilal/Workspace/backstage/packages/catalog-model/examples/components/artist-lookup-component.yaml',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'file:/Users/camilal/Workspace/backstage/packages/catalog-model/examples/all.yaml',
|
||||
'backstage.io/source-template': 'template:default/springboot-template',
|
||||
'backstage.io/linguist':
|
||||
'https://github.com/backstage/backstage/tree/master/plugins/playlist',
|
||||
},
|
||||
name: 'artist-lookup',
|
||||
description: 'Artist Lookup',
|
||||
tags: ['java', 'data'],
|
||||
links: [
|
||||
{
|
||||
url: 'https://example.com/user',
|
||||
title: 'Examples Users',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
url: 'https://example.com/group',
|
||||
title: 'Example Group',
|
||||
icon: 'group',
|
||||
},
|
||||
{
|
||||
url: 'https://example.com/cloud',
|
||||
title: 'Link with Cloud Icon',
|
||||
icon: 'cloud',
|
||||
},
|
||||
{
|
||||
url: 'https://example.com/dashboard',
|
||||
title: 'Dashboard',
|
||||
icon: 'dashboard',
|
||||
},
|
||||
{ url: 'https://example.com/help', title: 'Support', icon: 'help' },
|
||||
{ url: 'https://example.com/web', title: 'Website', icon: 'web' },
|
||||
{
|
||||
url: 'https://example.com/alert',
|
||||
title: 'Alerts',
|
||||
icon: 'alert',
|
||||
},
|
||||
],
|
||||
uid: '0dc69d61-4715-4912-bd7d-a0d44b421db0',
|
||||
etag: 'dcebc518ac79e77356cb34df119a523de51cd522',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
spec: {
|
||||
type: 'service',
|
||||
lifecycle: 'experimental',
|
||||
owner: 'team-a',
|
||||
system: 'artist-engagement-portal',
|
||||
dependsOn: ['resource:artists-db'],
|
||||
apiConsumedBy: ['component:www-artist'],
|
||||
},
|
||||
relations: [
|
||||
{ type: 'apiConsumedBy', targetRef: 'component:default/www-artist' },
|
||||
{ type: 'dependsOn', targetRef: 'resource:default/artists-db' },
|
||||
{ type: 'ownedBy', targetRef: 'group:default/team-a' },
|
||||
{
|
||||
type: 'partOf',
|
||||
targetRef: 'system:default/artist-engagement-portal',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockCatalogApi = catalogApiMock.mock({
|
||||
getEntityByRef: async () => entityMock,
|
||||
});
|
||||
|
||||
const mockStarredEntitiesApi = new MockStarredEntitiesApi();
|
||||
|
||||
const infoCard = EntityCardBlueprint.make({
|
||||
name: 'info-card',
|
||||
params: {
|
||||
defaultArea: 'info',
|
||||
loader: async () => <div>Info card</div>,
|
||||
},
|
||||
});
|
||||
|
||||
const otherCard = EntityCardBlueprint.make({
|
||||
name: 'info',
|
||||
params: {
|
||||
loader: async () => <div>Other card</div>,
|
||||
},
|
||||
});
|
||||
|
||||
const customLayout = EntityCardLayoutBlueprint.make({
|
||||
name: 'custom-layout',
|
||||
params: {
|
||||
loader:
|
||||
async () =>
|
||||
({ cards }) =>
|
||||
(
|
||||
<div>
|
||||
<h3>Custom layout</h3>
|
||||
<div id="info">
|
||||
{cards
|
||||
.filter(card => card.area === 'info')
|
||||
.map((card, index) => (
|
||||
<Fragment key={index}>{card.element}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div id="other">
|
||||
{cards
|
||||
.filter(card => card.area !== 'info')
|
||||
.map((card, index) => (
|
||||
<Fragment key={index}>{card.element}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
it('Should use the default layout', async () => {
|
||||
const tester = createExtensionTester(
|
||||
Object.assign({ namespace: 'catalog' }, catalogOverviewEntityContent),
|
||||
);
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entityMock}>
|
||||
{tester.reactElement()}
|
||||
</EntityProvider>
|
||||
</TestApiProvider>,
|
||||
{
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: /Custom layout/ }),
|
||||
).not.toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should render a custom layout', async () => {
|
||||
const tester = createExtensionTester(
|
||||
Object.assign({ namespace: 'catalog' }, catalogOverviewEntityContent),
|
||||
)
|
||||
.add(customLayout)
|
||||
.add(infoCard)
|
||||
.add(otherCard);
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entityMock}>
|
||||
{tester.reactElement()}
|
||||
</EntityProvider>
|
||||
</TestApiProvider>,
|
||||
{
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole('heading', { name: /Custom layout/ }),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Info card').parentNode).toHaveAttribute(
|
||||
'id',
|
||||
'info',
|
||||
),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Other card').parentNode).toHaveAttribute(
|
||||
'id',
|
||||
'other',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -14,90 +14,101 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { lazy as reactLazy } from 'react';
|
||||
import {
|
||||
coreExtensionData,
|
||||
createExtensionInput,
|
||||
ExtensionBoundary,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
EntityCardBlueprint,
|
||||
EntityContentBlueprint,
|
||||
OverviewEntityContentLauyoutBlueprint,
|
||||
EntityCardLayoutBlueprint,
|
||||
EntityCardLayoutProps,
|
||||
} from '@backstage/plugin-catalog-react/alpha';
|
||||
import { buildFilterFn } from './filter/FilterWrapper';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
|
||||
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: card.get(
|
||||
EntityContentBlueprint.dataRefs.filterExpression,
|
||||
),
|
||||
area:
|
||||
card.get(EntityCardBlueprint.dataRefs.area) ??
|
||||
layout?.get(
|
||||
OverviewEntityContentLauyoutBlueprint.dataRefs.defaultArea,
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
export const catalogOverviewEntityContent =
|
||||
EntityContentBlueprint.makeWithOverrides({
|
||||
name: 'overview',
|
||||
inputs: {
|
||||
layouts: createExtensionInput([
|
||||
EntityCardLayoutBlueprint.dataRefs.filterFunction.optional(),
|
||||
EntityCardLayoutBlueprint.dataRefs.filterExpression.optional(),
|
||||
EntityCardLayoutBlueprint.dataRefs.component,
|
||||
]),
|
||||
cards: createExtensionInput([
|
||||
coreExtensionData.reactElement,
|
||||
EntityContentBlueprint.dataRefs.filterFunction.optional(),
|
||||
EntityContentBlueprint.dataRefs.filterExpression.optional(),
|
||||
EntityCardBlueprint.dataRefs.area.optional(),
|
||||
]),
|
||||
},
|
||||
factory: (originalFactory, { node, inputs }) => {
|
||||
return originalFactory({
|
||||
defaultPath: '/',
|
||||
defaultTitle: 'Overview',
|
||||
loader: async () => {
|
||||
const LazyDefaultLayoutComponent = reactLazy(() =>
|
||||
import('./EntityOverviewPage').then(m => ({
|
||||
default: m.EntityOverviewPage,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
return <Component />;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
const DefaultLayoutComponent = (props: EntityCardLayoutProps) => {
|
||||
return (
|
||||
<ExtensionBoundary node={node}>
|
||||
<LazyDefaultLayoutComponent {...props} />
|
||||
</ExtensionBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
const layouts = [
|
||||
...inputs.layouts.map(layout => ({
|
||||
filter: buildFilterFn(
|
||||
layout.get(EntityCardLayoutBlueprint.dataRefs.filterFunction),
|
||||
layout.get(EntityCardLayoutBlueprint.dataRefs.filterExpression),
|
||||
),
|
||||
Component: layout.get(
|
||||
EntityCardLayoutBlueprint.dataRefs.component,
|
||||
),
|
||||
})),
|
||||
{
|
||||
filter: buildFilterFn(),
|
||||
Component: DefaultLayoutComponent,
|
||||
},
|
||||
];
|
||||
|
||||
const cards = inputs.cards.map(card => ({
|
||||
element: card.get(coreExtensionData.reactElement),
|
||||
area: card.get(EntityCardBlueprint.dataRefs.area),
|
||||
filter: buildFilterFn(
|
||||
card.get(EntityContentBlueprint.dataRefs.filterFunction),
|
||||
card.get(EntityContentBlueprint.dataRefs.filterExpression),
|
||||
),
|
||||
}));
|
||||
|
||||
const Component = () => {
|
||||
const { entity } = useEntity();
|
||||
|
||||
// Use the first layout that matches the entity filter
|
||||
const layout = layouts.find(l => l.filter(entity));
|
||||
if (!layout) {
|
||||
throw new Error('No layout found for entity'); // Shouldn't be able to happen
|
||||
}
|
||||
|
||||
return (
|
||||
<layout.Component
|
||||
cards={cards.filter(card => card.filter(entity))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return <Component />;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default [catalogOverviewEntityContent];
|
||||
|
||||
@@ -22,11 +22,11 @@ const _default: FrontendPlugin<
|
||||
name: 'group-profile';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<
|
||||
@@ -49,7 +49,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -59,7 +59,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:org/members-list': ExtensionDefinition<{
|
||||
@@ -67,11 +67,11 @@ const _default: FrontendPlugin<
|
||||
name: 'members-list';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<
|
||||
@@ -94,7 +94,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -104,7 +104,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:org/ownership': ExtensionDefinition<{
|
||||
@@ -112,11 +112,11 @@ const _default: FrontendPlugin<
|
||||
name: 'ownership';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<
|
||||
@@ -139,7 +139,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -149,7 +149,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
'entity-card:org/user-profile': ExtensionDefinition<{
|
||||
@@ -157,11 +157,11 @@ const _default: FrontendPlugin<
|
||||
name: 'user-profile';
|
||||
config: {
|
||||
filter: string | undefined;
|
||||
area: string | undefined;
|
||||
area: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
filter?: string | undefined;
|
||||
area?: string | undefined;
|
||||
area?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
output:
|
||||
| ConfigurableExtensionDataRef<
|
||||
@@ -184,7 +184,7 @@ const _default: FrontendPlugin<
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'full' | 'info' | 'peek',
|
||||
'catalog.entity-card-area',
|
||||
{
|
||||
optional: true;
|
||||
@@ -194,7 +194,7 @@ const _default: FrontendPlugin<
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | ((entity: Entity) => boolean) | undefined;
|
||||
defaultArea?: 'main' | 'info' | 'glance' | undefined;
|
||||
defaultArea?: 'full' | 'info' | 'peek' | undefined;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user