Merge pull request #28950 from backstage/camilaibs/entity-page-layout-followup

[NFS]: Default Sticky Entity Page Layout
This commit is contained in:
Camila Belo
2025-03-05 16:46:09 +01:00
committed by GitHub
7 changed files with 180 additions and 188 deletions
+16 -1
View File
@@ -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
```
-5
View File
@@ -75,11 +75,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',
-2
View File
@@ -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 */
-99
View File
@@ -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 (
<Grid container spacing={3}>
<Grid
className={classes.infoArea}
xs={12}
md={4}
style={{
position: 'sticky',
top: -16,
alignSelf: 'flex-start',
}}
item
>
<Grid container spacing={3}>
{cards
.filter(card => card.type === '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.type === 'peek')
.map((card, index) => (
<Grid key={index} className={classes.card} xs={12} md={6} item>
{card.element}
</Grid>
))}
{cards
.filter(card => !card.type || card.type === 'full')
.map((card, index) => (
<Grid key={index} className={classes.card} xs={12} md={6} item>
{card.element}
</Grid>
))}
</Grid>
</Grid>
</Grid>
);
}
export const customEntityContentOverviewLayoutModule = createFrontendModule({
pluginId: 'app',
extensions: [
EntityContentLayoutBlueprint.make({
name: 'sticky',
params: {
loader: async () => StickyEntityContentOverviewLayout,
},
}),
],
});
@@ -0,0 +1,162 @@
/*
* 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, Theme } 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';
import { HorizontalScrollGrid } from '@backstage/core-components';
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',
'& + &': {
marginLeft: theme.spacing(3),
},
},
[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),
},
},
}));
const entityWarningContent = (
<>
<EntitySwitch>
<EntitySwitch.Case if={isOrphan}>
<Grid item xs={12}>
<EntityOrphanWarning />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasRelationWarnings}>
<Grid item xs={12}>
<EntityRelationWarning />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasCatalogProcessingErrors}>
<Grid item xs={12}>
<EntityProcessingErrorsPanel />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
</>
);
export function DefaultEntityContentLayout(props: EntityContentLayoutProps) {
const { cards } = props;
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}
<div className={classes.root}>
{infoCards.length > 0 ? (
<div className={classes.infoArea}>
{infoCards.map(card => card.element)}
</div>
) : null}
{peekCards.length > 0 ? (
<div className={classes.peekArea}>
<HorizontalScrollGrid>
{peekCards.map(card => (
<div className={classes.peekCard}>{card.element}</div>
))}
</HorizontalScrollGrid>
</div>
) : null}
{fullCards.length > 0 ? (
<div className={classes.fullArea}>
{fullCards.map(card => card.element)}
</div>
) : null}
</div>
</>
);
}
@@ -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 = (
<>
<EntitySwitch>
<EntitySwitch.Case if={isOrphan}>
<Grid item xs={12}>
<EntityOrphanWarning />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasRelationWarnings}>
<Grid item xs={12}>
<EntityRelationWarning />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasCatalogProcessingErrors}>
<Grid item xs={12}>
<EntityProcessingErrorsPanel />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
</>
);
export function EntityOverviewPage(props: EntityOverviewPageProps) {
const { entity } = useEntity();
return (
<Grid container spacing={3} alignItems="stretch">
{entityWarningContent}
{props.cards.map((card, index) => (
<FilterWrapper key={index} entity={entity} {...card} />
))}
</Grid>
);
}
+2 -2
View File
@@ -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,
})),
);