From 08b03edac6202d2d9d34ca1eef32b1be50278107 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 24 Feb 2025 16:01:40 +0100 Subject: [PATCH 1/3] refactor: default sticky entity page layout Signed-off-by: Camila Belo --- .changeset/real-hounds-mate.md | 17 ++- packages/app-next/app-config.yaml | 5 - packages/app-next/src/App.tsx | 2 - packages/app-next/src/EntityPages.tsx | 99 -------------- .../src/alpha/DefaultEntityContentLayout.tsx | 126 ++++++++++++++++++ .../catalog/src/alpha/EntityOverviewPage.tsx | 79 ----------- plugins/catalog/src/alpha/entityContents.tsx | 4 +- 7 files changed, 144 insertions(+), 188 deletions(-) delete mode 100644 packages/app-next/src/EntityPages.tsx create mode 100644 plugins/catalog/src/alpha/DefaultEntityContentLayout.tsx delete mode 100644 plugins/catalog/src/alpha/EntityOverviewPage.tsx diff --git a/.changeset/real-hounds-mate.md b/.changeset/real-hounds-mate.md index 31d98601df..9acbe2033b 100644 --- a/.changeset/real-hounds-mate.md +++ b/.changeset/real-hounds-mate.md @@ -2,4 +2,19 @@ '@backstage/plugin-catalog': minor --- -The `Overview` entity content now supports custom cards grid layouts. +The default layout of the entity page can now optionally be customized with 3 card types: info, peek and full. + +- Cards of type `info` are rendered in a fixed area on the right; +- Cards of type `peek` are rendered on top of the main content area; +- Cards of type `full` and cards with undefined type are rendered as they were before, in the main content area, below the peek cards. + +If you want to keep the layout as it was before, you don't need to do anything. But if you want to experiment with the card types and see how they render, here is an example setting the about card to be rendered as an `info` card: + +```diff +app: + extensions: + # Entity page cards ++ - entity-card:catalog/about: ++ config: ++ type: info # or peek or full +``` diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 45f11ae1e2..586b13515e 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -71,11 +71,6 @@ app: # - entity-content:azure-devops/pull-requests # - entity-content:azure-devops/git-tags - - 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', diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index bb2998fb00..1992d5b35f 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -44,7 +44,6 @@ 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'; /* @@ -132,7 +131,6 @@ const app = createApp({ kubernetesPlugin, notFoundErrorPageModule, customHomePageModule, - customEntityContentOverviewLayoutModule, ...collectedLegacyPlugins, ], /* Handled through config instead */ diff --git a/packages/app-next/src/EntityPages.tsx b/packages/app-next/src/EntityPages.tsx deleted file mode 100644 index fd925e68b9..0000000000 --- a/packages/app-next/src/EntityPages.tsx +++ /dev/null @@ -1,99 +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 React from 'react'; -import Grid from '@material-ui/core/Grid'; -import { createFrontendModule } from '@backstage/frontend-plugin-api'; -import { - EntityContentLayoutBlueprint, - EntityContentLayoutProps, -} from '@backstage/plugin-catalog-react/alpha'; -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: EntityContentLayoutProps) { - const { cards } = props; - const classes = useStyles(); - return ( - - - - {cards - .filter(card => card.type === 'info') - .map((card, index) => ( - - {card.element} - - ))} - - - - - {cards - .filter(card => card.type === 'peek') - .map((card, index) => ( - - {card.element} - - ))} - {cards - .filter(card => !card.type || card.type === 'full') - .map((card, index) => ( - - {card.element} - - ))} - - - - ); -} - -export const customEntityContentOverviewLayoutModule = createFrontendModule({ - pluginId: 'app', - extensions: [ - EntityContentLayoutBlueprint.make({ - name: 'sticky', - params: { - loader: async () => StickyEntityContentOverviewLayout, - }, - }), - ], -}); diff --git a/plugins/catalog/src/alpha/DefaultEntityContentLayout.tsx b/plugins/catalog/src/alpha/DefaultEntityContentLayout.tsx new file mode 100644 index 0000000000..1c0572ca22 --- /dev/null +++ b/plugins/catalog/src/alpha/DefaultEntityContentLayout.tsx @@ -0,0 +1,126 @@ +/* + * 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 Grid from '@material-ui/core/Grid'; +import { makeStyles } from '@material-ui/core/styles'; +import { EntityContentLayoutProps } from '@backstage/plugin-catalog-react/alpha'; +import { EntitySwitch } from '../components/EntitySwitch'; +import { + EntityOrphanWarning, + isOrphan, +} from '../components/EntityOrphanWarning'; +import { + EntityRelationWarning, + hasRelationWarnings, +} from '../components/EntityRelationWarning'; +import { + EntityProcessingErrorsPanel, + hasCatalogProcessingErrors, +} from '../components/EntityProcessingErrorsPanel'; + +const useStyles = makeStyles(theme => ({ + [theme.breakpoints.up('sm')]: { + infoArea: { + order: 1, + position: 'sticky', + top: -16, + alignSelf: 'flex-start', + }, + card: { + alignSelf: 'stretch', + '& > *': { + height: '100%', + minHeight: 400, + }, + }, + }, +})); + +const entityWarningContent = ( + <> + + + + + + + + + + + + + + + + + + + + + + + + +); + +export function DefaultEntityContentLayout(props: EntityContentLayoutProps) { + const { cards } = props; + const classes = useStyles(); + + const infoCards = cards.filter(card => card.type === 'info'); + const peekCards = cards.filter(card => card.type === 'peek'); + const fullCards = cards.filter(card => !card.type || card.type === 'full'); + return ( + + {entityWarningContent} + {infoCards.length > 0 ? ( + + + {infoCards.map((card, index) => ( + + {card.element} + + ))} + + + ) : null} + {peekCards.length > 0 || fullCards.length > 0 ? ( + + {peekCards.length > 0 ? ( + + {peekCards.map((card, index) => ( + + {card.element} + + ))} + + ) : null} + {fullCards.length > 0 ? ( + + {fullCards.map((card, index) => ( + + {card.element} + + ))} + + ) : null} + + ) : null} + + ); +} diff --git a/plugins/catalog/src/alpha/EntityOverviewPage.tsx b/plugins/catalog/src/alpha/EntityOverviewPage.tsx deleted file mode 100644 index 64f194604f..0000000000 --- a/plugins/catalog/src/alpha/EntityOverviewPage.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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 { useEntity } from '@backstage/plugin-catalog-react'; -import Grid from '@material-ui/core/Grid'; -import React from 'react'; -import { FilterWrapper } from './filter/FilterWrapper'; -import { EntitySwitch } from '../components/EntitySwitch'; -import { - EntityOrphanWarning, - isOrphan, -} from '../components/EntityOrphanWarning'; -import { - EntityRelationWarning, - hasRelationWarnings, -} from '../components/EntityRelationWarning'; -import { - EntityProcessingErrorsPanel, - hasCatalogProcessingErrors, -} from '../components/EntityProcessingErrorsPanel'; - -interface EntityOverviewPageProps { - cards: Array<{ - element: React.JSX.Element; - }>; -} - -const entityWarningContent = ( - <> - - - - - - - - - - - - - - - - - - - - - - - - -); - -export function EntityOverviewPage(props: EntityOverviewPageProps) { - const { entity } = useEntity(); - return ( - - {entityWarningContent} - {props.cards.map((card, index) => ( - - ))} - - ); -} diff --git a/plugins/catalog/src/alpha/entityContents.tsx b/plugins/catalog/src/alpha/entityContents.tsx index 3e883433a9..1de5436efe 100644 --- a/plugins/catalog/src/alpha/entityContents.tsx +++ b/plugins/catalog/src/alpha/entityContents.tsx @@ -51,8 +51,8 @@ export const catalogOverviewEntityContent = defaultTitle: 'Overview', loader: async () => { const LazyDefaultLayoutComponent = reactLazy(() => - import('./EntityOverviewPage').then(m => ({ - default: m.EntityOverviewPage, + import('./DefaultEntityContentLayout').then(m => ({ + default: m.DefaultEntityContentLayout, })), ); From 8f141357d35ccee6c2cb3d70eff42f5a8d63a2df Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 3 Mar 2025 10:29:36 +0100 Subject: [PATCH 2/3] refactor: add horizontal scroll to peel cards Signed-off-by: Camila Belo --- packages/core-components/report.api.md | 1 + .../HorizontalScrollGrid.tsx | 5 +++- .../src/alpha/DefaultEntityContentLayout.tsx | 24 +++++++++++++++---- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index c706eef2bb..77262e5360 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -20,6 +20,7 @@ import { default as CSS_2 } from 'csstype'; import { CSSProperties } from 'react'; import { ElementType } from 'react'; import { ErrorInfo } from 'react'; +import { GridSpacing } from '@material-ui/core/Grid'; import IconButton from '@material-ui/core/IconButton'; import { IconComponent } from '@backstage/core-plugin-api'; import { Icons } from '@material-table/core'; diff --git a/packages/core-components/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx b/packages/core-components/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx index e89b7896dd..72a596415d 100644 --- a/packages/core-components/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx +++ b/packages/core-components/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import Box from '@material-ui/core/Box'; -import Grid from '@material-ui/core/Grid'; +import Grid, { GridSpacing } from '@material-ui/core/Grid'; import IconButton from '@material-ui/core/IconButton'; import { makeStyles } from '@material-ui/core/styles'; import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; @@ -54,6 +54,7 @@ type Props = { scrollStep?: number; scrollSpeed?: number; // lower is faster minScrollDistance?: number; // limits how small steps the scroll can take in px + spacing?: GridSpacing; }; /** @public */ @@ -209,6 +210,7 @@ export function HorizontalScrollGrid(props: PropsWithChildren) { scrollStep = 100, scrollSpeed = 50, minScrollDistance = 5, + spacing, children, ...otherProps } = props; @@ -234,6 +236,7 @@ export function HorizontalScrollGrid(props: PropsWithChildren) { wrap="nowrap" className={classes.container} ref={ref as any} + spacing={spacing} > {children} diff --git a/plugins/catalog/src/alpha/DefaultEntityContentLayout.tsx b/plugins/catalog/src/alpha/DefaultEntityContentLayout.tsx index 1c0572ca22..708f9bdb5f 100644 --- a/plugins/catalog/src/alpha/DefaultEntityContentLayout.tsx +++ b/plugins/catalog/src/alpha/DefaultEntityContentLayout.tsx @@ -31,8 +31,12 @@ import { EntityProcessingErrorsPanel, hasCatalogProcessingErrors, } from '../components/EntityProcessingErrorsPanel'; +import { HorizontalScrollGrid } from '@backstage/core-components'; const useStyles = makeStyles(theme => ({ + peekCard: { + flex: '0 0 auto', + }, [theme.breakpoints.up('sm')]: { infoArea: { order: 1, @@ -103,11 +107,21 @@ export function DefaultEntityContentLayout(props: EntityContentLayoutProps) { {peekCards.length > 0 ? ( - {peekCards.map((card, index) => ( - - {card.element} - - ))} + + + {peekCards.map((card, index) => ( + + {card.element} + + ))} + + ) : null} {fullCards.length > 0 ? ( From db8ff82cb89a288c95492bc5f9b72a4c2bb9e01a Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 5 Mar 2025 16:06:12 +0100 Subject: [PATCH 3/3] refactor: apply review suggestions Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- packages/core-components/report.api.md | 1 - .../HorizontalScrollGrid.tsx | 5 +- .../src/alpha/DefaultEntityContentLayout.tsx | 142 ++++++++++-------- 3 files changed, 83 insertions(+), 65 deletions(-) diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index 77262e5360..c706eef2bb 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -20,7 +20,6 @@ import { default as CSS_2 } from 'csstype'; import { CSSProperties } from 'react'; import { ElementType } from 'react'; import { ErrorInfo } from 'react'; -import { GridSpacing } from '@material-ui/core/Grid'; import IconButton from '@material-ui/core/IconButton'; import { IconComponent } from '@backstage/core-plugin-api'; import { Icons } from '@material-table/core'; diff --git a/packages/core-components/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx b/packages/core-components/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx index 72a596415d..e89b7896dd 100644 --- a/packages/core-components/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx +++ b/packages/core-components/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import Box from '@material-ui/core/Box'; -import Grid, { GridSpacing } from '@material-ui/core/Grid'; +import Grid from '@material-ui/core/Grid'; import IconButton from '@material-ui/core/IconButton'; import { makeStyles } from '@material-ui/core/styles'; import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; @@ -54,7 +54,6 @@ type Props = { scrollStep?: number; scrollSpeed?: number; // lower is faster minScrollDistance?: number; // limits how small steps the scroll can take in px - spacing?: GridSpacing; }; /** @public */ @@ -210,7 +209,6 @@ export function HorizontalScrollGrid(props: PropsWithChildren) { scrollStep = 100, scrollSpeed = 50, minScrollDistance = 5, - spacing, children, ...otherProps } = props; @@ -236,7 +234,6 @@ export function HorizontalScrollGrid(props: PropsWithChildren) { wrap="nowrap" className={classes.container} ref={ref as any} - spacing={spacing} > {children} diff --git a/plugins/catalog/src/alpha/DefaultEntityContentLayout.tsx b/plugins/catalog/src/alpha/DefaultEntityContentLayout.tsx index 708f9bdb5f..7a73f991bc 100644 --- a/plugins/catalog/src/alpha/DefaultEntityContentLayout.tsx +++ b/plugins/catalog/src/alpha/DefaultEntityContentLayout.tsx @@ -16,7 +16,7 @@ import React from 'react'; import Grid from '@material-ui/core/Grid'; -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles, Theme } from '@material-ui/core/styles'; import { EntityContentLayoutProps } from '@backstage/plugin-catalog-react/alpha'; import { EntitySwitch } from '../components/EntitySwitch'; import { @@ -33,23 +33,61 @@ import { } from '../components/EntityProcessingErrorsPanel'; import { HorizontalScrollGrid } from '@backstage/core-components'; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles< + Theme, + { infoCards: boolean; peekCards: boolean; fullCards: boolean } +>(theme => ({ + root: { + display: 'flex', + flexFlow: 'column nowrap', + gap: theme.spacing(3), + }, + fullArea: { + display: 'flex', + flexFlow: 'column', + gap: theme.spacing(3), + alignItems: 'stretch', + minWidth: 0, + }, + infoArea: { + display: 'flex', + flexFlow: 'column nowrap', + alignItems: 'stretch', + gap: theme.spacing(3), + minWidth: 0, + }, + peekArea: { + margin: theme.spacing(1.5), // To counteract MUI negative grid margin + }, peekCard: { flex: '0 0 auto', - }, - [theme.breakpoints.up('sm')]: { - infoArea: { - order: 1, - position: 'sticky', - top: -16, - alignSelf: 'flex-start', + '& + &': { + marginLeft: theme.spacing(3), }, - card: { - alignSelf: 'stretch', - '& > *': { - height: '100%', - minHeight: 400, - }, + }, + [theme.breakpoints.up('md')]: { + root: { + display: 'grid', + gap: 0, + gridTemplateAreas: ({ peekCards }) => ` + "${peekCards ? 'peek' : 'full'} info" + "full info" + `, + gridTemplateColumns: ({ infoCards }) => (infoCards ? '2fr 1fr' : '1fr'), + alignItems: 'start', + }, + infoArea: { + gridArea: 'info', + position: 'sticky', + top: theme.spacing(3), + marginLeft: theme.spacing(3), + }, + fullArea: { + gridArea: 'full', + }, + peekArea: { + gridArea: 'peek', + marginBottom: theme.spacing(3), }, }, })); @@ -84,57 +122,41 @@ const entityWarningContent = ( export function DefaultEntityContentLayout(props: EntityContentLayoutProps) { const { cards } = props; - const classes = useStyles(); const infoCards = cards.filter(card => card.type === 'info'); const peekCards = cards.filter(card => card.type === 'peek'); const fullCards = cards.filter(card => !card.type || card.type === 'full'); + + const classes = useStyles({ + infoCards: !!infoCards.length, + peekCards: !!peekCards.length, + fullCards: !!fullCards.length, + }); + return ( - + <> {entityWarningContent} - {infoCards.length > 0 ? ( - - - {infoCards.map((card, index) => ( - - {card.element} - - ))} - - - ) : null} - {peekCards.length > 0 || fullCards.length > 0 ? ( - - {peekCards.length > 0 ? ( - - - - {peekCards.map((card, index) => ( - - {card.element} - - ))} - - - - ) : null} - {fullCards.length > 0 ? ( - - {fullCards.map((card, index) => ( - - {card.element} - +
+ {infoCards.length > 0 ? ( +
+ {infoCards.map(card => card.element)} +
+ ) : null} + {peekCards.length > 0 ? ( +
+ + {peekCards.map(card => ( +
{card.element}
))} - - ) : null} - - ) : null} - +
+
+ ) : null} + {fullCards.length > 0 ? ( +
+ {fullCards.map(card => card.element)} +
+ ) : null} +
+ ); }