diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 0c75e1c96c..a46e119ac0 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -10,10 +10,15 @@ app: - apis.plugin.graphiql.browse.gitlab: true # Entity page cards - - 'entity.cards.about' + - entity.cards.about + - entity.cards.labels + - entity.cards.links: + config: + filter: + - isKind: component # Entity page content - - 'entity.content.techdocs' + - entity.content.techdocs # scmAuthExtension: >- # createScmAuthExtension({ diff --git a/plugins/catalog-react/alpha-api-report.md b/plugins/catalog-react/alpha-api-report.md index 120e162c84..ed851f810a 100644 --- a/plugins/catalog-react/alpha-api-report.md +++ b/plugins/catalog-react/alpha-api-report.md @@ -10,13 +10,11 @@ import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { Extension } from '@backstage/frontend-plugin-api'; import { ExtensionInputValues } from '@backstage/frontend-plugin-api'; -import { PortableSchema } from '@backstage/frontend-plugin-api'; import { ResourcePermission } from '@backstage/plugin-permission-common'; import { RouteRef } from '@backstage/frontend-plugin-api'; // @alpha (undocumented) export function createEntityCardExtension< - TConfig, TInputs extends AnyExtensionInputMap, >(options: { id: string; @@ -26,44 +24,47 @@ export function createEntityCardExtension< }; disabled?: boolean; inputs?: TInputs; - configSchema?: PortableSchema; + filter?: (ctx: { entity: Entity }) => boolean; loader: (options: { - config: TConfig; inputs: Expand>; }) => Promise; -}): Extension; +}): Extension<{ + filter?: + | { + isKind?: string | undefined; + isType?: string | undefined; + }[] + | undefined; +}>; // @alpha (undocumented) export function createEntityContentExtension< - TConfig extends { - path: string; - title: string; - }, TInputs extends AnyExtensionInputMap, ->( - options: ( - | { - defaultPath: string; - defaultTitle: string; - } - | { - configSchema: PortableSchema; - } - ) & { +>(options: { + id: string; + attachTo?: { id: string; - attachTo?: { - id: string; - input: string; - }; - disabled?: boolean; - inputs?: TInputs; - routeRef?: RouteRef; - loader: (options: { - config: TConfig; - inputs: Expand>; - }) => Promise; - }, -): Extension; + input: string; + }; + disabled?: boolean; + inputs?: TInputs; + routeRef?: RouteRef; + defaultPath: string; + defaultTitle: string; + filter?: (ctx: { entity: Entity }) => boolean; + loader: (options: { + inputs: Expand>; + }) => Promise; +}): Extension<{ + title: string; + path: string; + filter?: + | { + isKind?: string | undefined; + isType?: string | undefined; + }[] + | undefined; +}>; // @alpha (undocumented) export const entityContentTitleExtensionDataRef: ConfigurableExtensionDataRef< @@ -71,6 +72,12 @@ export const entityContentTitleExtensionDataRef: ConfigurableExtensionDataRef< {} >; +// @alpha (undocumented) +export const entityFilterExtensionDataRef: ConfigurableExtensionDataRef< + (ctx: { entity: Entity }) => boolean, + {} +>; + // @alpha export function isOwnerOf(owner: Entity, entity: Entity): boolean; diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx index 58762b1994..8ea477d573 100644 --- a/plugins/catalog-react/src/alpha.tsx +++ b/plugins/catalog-react/src/alpha.tsx @@ -17,10 +17,8 @@ import React, { lazy } from 'react'; import { AnyExtensionInputMap, - Extension, ExtensionBoundary, ExtensionInputValues, - PortableSchema, RouteRef, coreExtensionData, createExtension, @@ -29,6 +27,7 @@ import { } from '@backstage/frontend-plugin-api'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { Expand } from '../../../packages/frontend-plugin-api/src/types'; +import { Entity } from '@backstage/catalog-model'; export { isOwnerOf } from './utils'; export { useEntityPermission } from './hooks/useEntityPermission'; @@ -37,21 +36,57 @@ export { useEntityPermission } from './hooks/useEntityPermission'; export const entityContentTitleExtensionDataRef = createExtensionDataRef('plugin.catalog.entity.content.title'); +/** @alpha */ +export const entityFilterExtensionDataRef = createExtensionDataRef< + (ctx: { entity: Entity }) => boolean +>('plugin.catalog.entity.filter'); + +function applyFilter(a?: string, b?: string): boolean { + if (!a) { + return true; + } + return a.toLocaleLowerCase('en-US') === b?.toLocaleLowerCase('en-US'); +} + +// TODO: Only two hardcoded isKind and isType filters are available for now +// This is just an initial config filter implementation and needs to be revisited +function buildFilter( + config: { filter?: { isKind?: string; isType?: string }[] }, + filterFunc?: (ctx: { entity: Entity }) => boolean, +) { + return (ctx: { entity: Entity }) => { + const configuredFilterMatch = config.filter?.some(filter => { + const kindMatch = applyFilter(filter.isKind, ctx.entity.kind); + const typeMatch = applyFilter( + filter.isType, + ctx.entity.spec?.type?.toString(), + ); + return kindMatch && typeMatch; + }); + if (configuredFilterMatch) { + return true; + } + if (filterFunc) { + return filterFunc(ctx); + } + return true; + }; +} + +// TODO: Figure out how to merge with provided config schema /** @alpha */ export function createEntityCardExtension< - TConfig, TInputs extends AnyExtensionInputMap, >(options: { id: string; attachTo?: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; - configSchema?: PortableSchema; + filter?: (ctx: { entity: Entity }) => boolean; loader: (options: { - config: TConfig; inputs: Expand>; }) => Promise; -}): Extension { +}) { const id = `entity.cards.${options.id}`; return createExtension({ @@ -63,13 +98,25 @@ export function createEntityCardExtension< disabled: options.disabled ?? true, output: { element: coreExtensionData.reactElement, + filter: entityFilterExtensionDataRef, }, inputs: options.inputs, - configSchema: options.configSchema, + configSchema: createSchemaFromZod(z => + z.object({ + filter: z + .array( + z.object({ + isKind: z.string().optional(), + isType: z.string().optional(), + }), + ) + .optional(), + }), + ), factory({ bind, config, inputs, source }) { const ExtensionComponent = lazy(() => options - .loader({ config, inputs }) + .loader({ inputs }) .then(element => ({ default: () => element })), ); @@ -79,6 +126,7 @@ export function createEntityCardExtension< ), + filter: buildFilter(config, options.filter), }); }, }); @@ -86,41 +134,22 @@ export function createEntityCardExtension< /** @alpha */ export function createEntityContentExtension< - TConfig extends { path: string; title: string }, TInputs extends AnyExtensionInputMap, ->( - options: ( - | { - defaultPath: string; - defaultTitle: string; - } - | { - configSchema: PortableSchema; - } - ) & { - id: string; - attachTo?: { id: string; input: string }; - disabled?: boolean; - inputs?: TInputs; - routeRef?: RouteRef; - loader: (options: { - config: TConfig; - inputs: Expand>; - }) => Promise; - }, -): Extension { +>(options: { + id: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + routeRef?: RouteRef; + defaultPath: string; + defaultTitle: string; + filter?: (ctx: { entity: Entity }) => boolean; + loader: (options: { + inputs: Expand>; + }) => Promise; +}) { const id = `entity.content.${options.id}`; - const configSchema = - 'configSchema' in options - ? options.configSchema - : (createSchemaFromZod(z => - z.object({ - path: z.string().default(options.defaultPath), - title: z.string().default(options.defaultTitle), - }), - ) as PortableSchema); - return createExtension({ id, attachTo: options.attachTo ?? { @@ -133,13 +162,27 @@ export function createEntityContentExtension< path: coreExtensionData.routePath, routeRef: coreExtensionData.routeRef.optional(), title: entityContentTitleExtensionDataRef, + filter: entityFilterExtensionDataRef, }, inputs: options.inputs, - configSchema, + configSchema: createSchemaFromZod(z => + z.object({ + path: z.string().default(options.defaultPath), + title: z.string().default(options.defaultTitle), + filter: z + .array( + z.object({ + isKind: z.string().optional(), + isType: z.string().optional(), + }), + ) + .optional(), + }), + ), factory({ bind, config, inputs, source }) { const ExtensionComponent = lazy(() => options - .loader({ config, inputs }) + .loader({ inputs }) .then(element => ({ default: () => element })), ); @@ -152,6 +195,7 @@ export function createEntityContentExtension< ), + filter: buildFilter(config, options.filter), }); }, }); diff --git a/plugins/catalog/src/alpha/EntityOverviewPage.tsx b/plugins/catalog/src/alpha/EntityOverviewPage.tsx new file mode 100644 index 0000000000..baa93845ae --- /dev/null +++ b/plugins/catalog/src/alpha/EntityOverviewPage.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { Entity } from '@backstage/catalog-model'; +import Grid from '@material-ui/core/Grid'; + +interface EntityOverviewPageProps { + cards: Array<{ + element: React.JSX.Element; + filter: (ctx: { entity: Entity }) => boolean; + }>; +} + +export function EntityOverviewPage(props: EntityOverviewPageProps) { + const { entity } = useEntity(); + return ( + + {props.cards + .filter(card => card.filter({ entity })) + .map(card => ( + + {card.element} + + ))} + + ); +} diff --git a/plugins/catalog/src/alpha/entityCards.tsx b/plugins/catalog/src/alpha/entityCards.tsx index cc30928fcf..c7b75679a7 100644 --- a/plugins/catalog/src/alpha/entityCards.tsx +++ b/plugins/catalog/src/alpha/entityCards.tsx @@ -27,6 +27,7 @@ export const EntityAboutCard = createEntityCardExtension({ export const EntityLinksCard = createEntityCardExtension({ id: 'links', + filter: ({ entity }) => Boolean(entity.metadata.links), loader: async () => import('../components/EntityLinksCard').then(m => { return ; @@ -35,6 +36,7 @@ export const EntityLinksCard = createEntityCardExtension({ export const EntityLabelsCard = createEntityCardExtension({ id: 'labels', + filter: ({ entity }) => Boolean(entity.metadata.labels), loader: async () => import('../components/EntityLabelsCard').then(m => ( diff --git a/plugins/catalog/src/alpha/entityContents.tsx b/plugins/catalog/src/alpha/entityContents.tsx index 0e26373b5e..9bef4a5e0d 100644 --- a/plugins/catalog/src/alpha/entityContents.tsx +++ b/plugins/catalog/src/alpha/entityContents.tsx @@ -15,12 +15,14 @@ */ import React from 'react'; -import { Grid } from '@material-ui/core'; import { coreExtensionData, createExtensionInput, } from '@backstage/frontend-plugin-api'; -import { createEntityContentExtension } from '@backstage/plugin-catalog-react/alpha'; +import { + createEntityContentExtension, + entityFilterExtensionDataRef, +} from '@backstage/plugin-catalog-react/alpha'; export const OverviewEntityContent = createEntityContentExtension({ id: 'overview', @@ -30,17 +32,13 @@ export const OverviewEntityContent = createEntityContentExtension({ inputs: { cards: createExtensionInput({ element: coreExtensionData.reactElement, + filter: entityFilterExtensionDataRef, }), }, - loader: async ({ inputs }) => ( - - {inputs.cards.map(card => ( - - {card.element} - - ))} - - ), + loader: async ({ inputs }) => + import('./EntityOverviewPage').then(m => ( + + )), }); export default [OverviewEntityContent];