Merge pull request #33082 from backstage/bui-migrate-catalog-entity-cards
feat(catalog,org): migrate entity cards from MUI/InfoCard to BUI
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': minor
|
||||
---
|
||||
|
||||
Added `EntityInfoCard` component to `@backstage/plugin-catalog-react` as a BUI-based card wrapper for entity page cards.
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': major
|
||||
'@backstage/plugin-org': minor
|
||||
---
|
||||
|
||||
Migrated `EntityAboutCard`, `EntityLinksCard`, `EntityLabelsCard`, `GroupProfileCard`, and `UserProfileCard` from MUI/InfoCard to use the new BUI card layout and BUI components where possible.
|
||||
|
||||
**BREAKING**: Removed `variant` prop from EntityAboutCard, EntityUserProfileCard, EntityGroupProfileCard, EntityLabelsCard, EntityLinksCard. Removed `gridSizes` prop from `AboutField`.
|
||||
|
||||
**Migration:**
|
||||
|
||||
Simply delete the obsolete `variant` and `gridSizes` props, e.g:
|
||||
|
||||
```diff
|
||||
- <EntityAboutCard variant="gridItem" />
|
||||
+ <EntityAboutCard />
|
||||
```
|
||||
|
||||
```diff
|
||||
- <AboutField label="Owner" gridSizes={{ xs: 12, sm: 6, lg: 4 }} />
|
||||
+ <AboutField label="Owner" />
|
||||
```
|
||||
@@ -94,7 +94,7 @@ const systemPage = (
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
<Grid item md={6}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
<EntityAboutCard />
|
||||
</Grid>
|
||||
<Grid item md={6}>
|
||||
<EntityHasComponentsCard variant="gridItem" />
|
||||
|
||||
@@ -166,7 +166,7 @@ const overviewContent = (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
{entityWarningContent}
|
||||
<Grid item md={6} xs={12}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
<EntityAboutCard />
|
||||
</Grid>
|
||||
|
||||
<Grid item md={6} xs={12}>
|
||||
@@ -330,7 +330,7 @@ const userPage = (
|
||||
<Grid container spacing={3}>
|
||||
{entityWarningContent}
|
||||
<Grid item xs={12} md={6}>
|
||||
<EntityUserProfileCard variant="gridItem" />
|
||||
<EntityUserProfileCard />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<EntityOwnershipCard
|
||||
@@ -349,7 +349,7 @@ const groupPage = (
|
||||
<Grid container spacing={3}>
|
||||
{entityWarningContent}
|
||||
<Grid item xs={12} md={6}>
|
||||
<EntityGroupProfileCard variant="gridItem" />
|
||||
<EntityGroupProfileCard />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<EntityOwnershipCard
|
||||
@@ -374,7 +374,7 @@ const systemPage = (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
{entityWarningContent}
|
||||
<Grid item md={6}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
<EntityAboutCard />
|
||||
</Grid>
|
||||
<Grid item md={6} xs={12}>
|
||||
<EntityCatalogGraphCard variant="gridItem" height={400} />
|
||||
@@ -418,7 +418,7 @@ const domainPage = (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
{entityWarningContent}
|
||||
<Grid item md={6}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
<EntityAboutCard />
|
||||
</Grid>
|
||||
<Grid item md={6} xs={12}>
|
||||
<EntityCatalogGraphCard variant="gridItem" height={400} />
|
||||
@@ -437,7 +437,7 @@ const resourcePage = (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
{entityWarningContent}
|
||||
<Grid item md={6}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
<EntityAboutCard />
|
||||
</Grid>
|
||||
<Grid item md={6} xs={12}>
|
||||
<EntityCatalogGraphCard variant="gridItem" height={400} />
|
||||
|
||||
@@ -156,7 +156,7 @@ describe('collectLegacyRoutes', () => {
|
||||
if={isKind('component')}
|
||||
children={
|
||||
<EntityLayout>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
<EntityAboutCard />
|
||||
</EntityLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
+5
-5
@@ -128,7 +128,7 @@ const overviewContent = (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
{entityWarningContent}
|
||||
<Grid item md={6}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
<EntityAboutCard />
|
||||
</Grid>
|
||||
<Grid item md={6} xs={12}>
|
||||
<EntityCatalogGraphCard variant="gridItem" height={400} />
|
||||
@@ -298,7 +298,7 @@ const userPage = (
|
||||
<Grid container spacing={3}>
|
||||
{entityWarningContent}
|
||||
<Grid item xs={12} md={6}>
|
||||
<EntityUserProfileCard variant="gridItem" />
|
||||
<EntityUserProfileCard />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<EntityOwnershipCard variant="gridItem" />
|
||||
@@ -314,7 +314,7 @@ const groupPage = (
|
||||
<Grid container spacing={3}>
|
||||
{entityWarningContent}
|
||||
<Grid item xs={12} md={6}>
|
||||
<EntityGroupProfileCard variant="gridItem" />
|
||||
<EntityGroupProfileCard />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<EntityOwnershipCard variant="gridItem" />
|
||||
@@ -336,7 +336,7 @@ const systemPage = (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
{entityWarningContent}
|
||||
<Grid item md={6}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
<EntityAboutCard />
|
||||
</Grid>
|
||||
<Grid item md={6} xs={12}>
|
||||
<EntityCatalogGraphCard variant="gridItem" height={400} />
|
||||
@@ -383,7 +383,7 @@ const domainPage = (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
{entityWarningContent}
|
||||
<Grid item md={6}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
<EntityAboutCard />
|
||||
</Grid>
|
||||
<Grid item md={6} xs={12}>
|
||||
<EntityCatalogGraphCard variant="gridItem" height={400} />
|
||||
|
||||
@@ -277,6 +277,23 @@ export type EntityFilter = {
|
||||
toQueryValue?: () => string | string[];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export function EntityInfoCard(props: EntityInfoCardProps): JSX_2.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface EntityInfoCardProps {
|
||||
// (undocumented)
|
||||
children?: ReactNode;
|
||||
// (undocumented)
|
||||
className?: string;
|
||||
// (undocumented)
|
||||
footerActions?: ReactNode;
|
||||
// (undocumented)
|
||||
headerActions?: ReactNode;
|
||||
// (undocumented)
|
||||
title?: ReactNode;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class EntityKindFilter implements EntityFilter {
|
||||
constructor(value: string, label: string);
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2026 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 { renderInTestApp } from '@backstage/test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { EntityInfoCard } from './EntityInfoCard';
|
||||
|
||||
describe('<EntityInfoCard />', () => {
|
||||
it('renders children in the card body', async () => {
|
||||
await renderInTestApp(
|
||||
<EntityInfoCard>
|
||||
<div>Body content</div>
|
||||
</EntityInfoCard>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Body content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the title when provided', async () => {
|
||||
await renderInTestApp(
|
||||
<EntityInfoCard title="My Card Title">
|
||||
<div>Body</div>
|
||||
</EntityInfoCard>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole('heading', { name: 'My Card Title' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render a heading when title is not provided', async () => {
|
||||
await renderInTestApp(
|
||||
<EntityInfoCard>
|
||||
<div>Body</div>
|
||||
</EntityInfoCard>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('heading')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders header actions next to the title', async () => {
|
||||
await renderInTestApp(
|
||||
<EntityInfoCard title="Title" headerActions={<button>Edit</button>}>
|
||||
<div>Body</div>
|
||||
</EntityInfoCard>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'Title' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Edit' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders footer actions when provided', async () => {
|
||||
await renderInTestApp(
|
||||
<EntityInfoCard title="Title" footerActions={<button>Next Page</button>}>
|
||||
<div>Body</div>
|
||||
</EntityInfoCard>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Next Page' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render footer when footerActions is not provided', async () => {
|
||||
const { container } = await renderInTestApp(
|
||||
<EntityInfoCard title="Title">
|
||||
<div>Body</div>
|
||||
</EntityInfoCard>,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.bui-CardFooter')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders JSX titles (e.g., icon + text)', async () => {
|
||||
await renderInTestApp(
|
||||
<EntityInfoCard
|
||||
title={
|
||||
<span>
|
||||
<span aria-hidden>🏠</span> Home Card
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div>Body</div>
|
||||
</EntityInfoCard>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Home Card')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2026 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 { ReactNode } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
Text,
|
||||
Flex,
|
||||
} from '@backstage/ui';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
height: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
/** @public */
|
||||
export interface EntityInfoCardProps {
|
||||
title?: ReactNode;
|
||||
headerActions?: ReactNode;
|
||||
footerActions?: ReactNode;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function EntityInfoCard(props: EntityInfoCardProps) {
|
||||
const { title, headerActions, footerActions, children, className } = props;
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Card className={classNames(classes.root, className)}>
|
||||
{title && (
|
||||
<CardHeader>
|
||||
<Flex justify="between" align="center">
|
||||
<Text as="h3" variant="title-x-small" weight="bold">
|
||||
{title}
|
||||
</Text>
|
||||
{headerActions && (
|
||||
<Flex align="center" gap="1">
|
||||
{headerActions}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
)}
|
||||
<CardBody>{children}</CardBody>
|
||||
{footerActions && <CardFooter>{footerActions}</CardFooter>}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { EntityInfoCard } from './EntityInfoCard';
|
||||
export type { EntityInfoCardProps } from './EntityInfoCard';
|
||||
@@ -35,3 +35,4 @@ export * from './EntityProcessingStatusPicker';
|
||||
export * from './EntityNamespacePicker';
|
||||
export * from './EntityAutocompletePicker';
|
||||
export * from './MissingAnnotationEmptyState';
|
||||
export * from './EntityInfoCard';
|
||||
|
||||
@@ -96,6 +96,8 @@ export const catalogTranslationRef: TranslationRef<
|
||||
readonly 'entityContextMenu.moreButtonAriaLabel': 'more';
|
||||
readonly 'entityLabelsCard.title': 'Labels';
|
||||
readonly 'entityLabelsCard.readMoreButtonTitle': 'Read more';
|
||||
readonly 'entityLabelsCard.columnKeyLabel': 'Label';
|
||||
readonly 'entityLabelsCard.columnValueLabel': 'Value';
|
||||
readonly 'entityLabelsCard.emptyDescription': 'No labels defined for this entity. You can add labels to your entity YAML as shown in the highlighted example below:';
|
||||
readonly 'entityLabels.ownerLabel': 'Owner';
|
||||
readonly 'entityLabels.warningPanelTitle': 'Entity not found';
|
||||
|
||||
@@ -40,11 +40,6 @@ import { TableProps } from '@backstage/core-components';
|
||||
import { TabProps } from '@material-ui/core/Tab';
|
||||
import { UserListFilterKind } from '@backstage/plugin-catalog-react';
|
||||
|
||||
// @public
|
||||
export type AboutCardProps = {
|
||||
variant?: InfoCardVariants;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export function AboutContent(props: AboutContentProps): JSX_2.Element;
|
||||
|
||||
@@ -64,8 +59,6 @@ export interface AboutFieldProps {
|
||||
// (undocumented)
|
||||
className?: string;
|
||||
// (undocumented)
|
||||
gridSizes?: Record<string, number>;
|
||||
// (undocumented)
|
||||
label: string;
|
||||
// (undocumented)
|
||||
value?: string;
|
||||
@@ -334,7 +327,7 @@ export interface DependsOnResourcesCardProps {
|
||||
}
|
||||
|
||||
// @public
|
||||
export const EntityAboutCard: (props: AboutCardProps) => JSX.Element;
|
||||
export const EntityAboutCard: () => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export type EntityContextMenuClassKey = 'button';
|
||||
@@ -384,8 +377,6 @@ export const EntityLabelsCard: (props: EntityLabelsCardProps) => JSX_2.Element;
|
||||
export interface EntityLabelsCardProps {
|
||||
// (undocumented)
|
||||
title?: string;
|
||||
// (undocumented)
|
||||
variant?: InfoCardVariants;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -435,8 +426,6 @@ export const EntityLinksCard: (props: EntityLinksCardProps) => JSX_2.Element;
|
||||
export interface EntityLinksCardProps {
|
||||
// (undocumented)
|
||||
cols?: ColumnBreakpoints | number;
|
||||
// (undocumented)
|
||||
variant?: InfoCardVariants;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -66,9 +66,7 @@ export const catalogAboutEntityCard = EntityCardBlueprint.makeWithOverrides({
|
||||
const { InternalAboutCard } = await import(
|
||||
'../components/AboutCard/AboutCard'
|
||||
);
|
||||
return (
|
||||
<InternalAboutCard variant="gridItem" subheader={<Subheader />} />
|
||||
);
|
||||
return <InternalAboutCard iconLinks={<Subheader />} />;
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -80,9 +78,7 @@ export const catalogLinksEntityCard = EntityCardBlueprint.make({
|
||||
type: 'info',
|
||||
filter: { 'metadata.links': { $exists: true } },
|
||||
loader: async () =>
|
||||
import('../components/EntityLinksCard').then(m => (
|
||||
<m.EntityLinksCard variant="gridItem" />
|
||||
)),
|
||||
import('../components/EntityLinksCard').then(m => <m.EntityLinksCard />),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -93,7 +89,7 @@ export const catalogLabelsEntityCard = EntityCardBlueprint.make({
|
||||
filter: { 'metadata.labels': { $exists: true } },
|
||||
loader: async () =>
|
||||
import('../components/EntityLabelsCard').then(m => (
|
||||
<m.EntityLabelsCard variant="gridItem" />
|
||||
<m.EntityLabelsCard />
|
||||
)),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -111,6 +111,8 @@ export const catalogTranslationRef = createTranslationRef({
|
||||
},
|
||||
entityLabelsCard: {
|
||||
title: 'Labels',
|
||||
columnKeyLabel: 'Label',
|
||||
columnValueLabel: 'Value',
|
||||
emptyDescription:
|
||||
'No labels defined for this entity. You can add labels to your entity YAML as shown in the highlighted example below:',
|
||||
readMoreButtonTitle: 'Read more',
|
||||
|
||||
@@ -16,11 +16,6 @@
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import CardHeader from '@material-ui/core/CardHeader';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import CachedIcon from '@material-ui/icons/Cached';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
@@ -31,9 +26,9 @@ import {
|
||||
AppIcon,
|
||||
HeaderIconLinkRow,
|
||||
IconLinkVerticalProps,
|
||||
InfoCardVariants,
|
||||
Link,
|
||||
} from '@backstage/core-components';
|
||||
import { EntityInfoCard } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
alertApiRef,
|
||||
errorApiRef,
|
||||
@@ -77,6 +72,16 @@ import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes';
|
||||
import { catalogTranslationRef } from '../../alpha/translation';
|
||||
import { useSourceTemplateCompoundEntityRef } from './hooks';
|
||||
import { AboutContent } from './AboutContent';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
linkContainer: {
|
||||
border: '1px solid var(--bui-border-1)',
|
||||
borderLeft: 'none',
|
||||
borderRight: 'none',
|
||||
marginBottom: 'var(--bui-space-6)',
|
||||
},
|
||||
});
|
||||
|
||||
export function useCatalogSourceIconLinkProps() {
|
||||
const { entity } = useEntity();
|
||||
@@ -152,41 +157,13 @@ function DefaultAboutCardSubheader() {
|
||||
return <HeaderIconLinkRow links={links} />;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
gridItemCard: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: 'calc(100% - 10px)', // for pages without content header
|
||||
marginBottom: '10px',
|
||||
},
|
||||
fullHeightCard: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
},
|
||||
gridItemCardContent: {
|
||||
flex: 1,
|
||||
},
|
||||
fullHeightCardContent: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Props for {@link EntityAboutCard}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AboutCardProps = {
|
||||
variant?: InfoCardVariants;
|
||||
};
|
||||
|
||||
export interface InternalAboutCardProps extends AboutCardProps {
|
||||
subheader?: JSX.Element;
|
||||
export interface InternalAboutCardProps {
|
||||
/** Icon link row rendered at the top of the card body. */
|
||||
iconLinks?: JSX.Element;
|
||||
}
|
||||
|
||||
export function InternalAboutCard(props: InternalAboutCardProps) {
|
||||
const { variant, subheader } = props;
|
||||
const { iconLinks } = props;
|
||||
const classes = useStyles();
|
||||
const { entity } = useEntity();
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
@@ -202,20 +179,6 @@ export function InternalAboutCard(props: InternalAboutCardProps) {
|
||||
const entityMetadataEditUrl =
|
||||
entity.metadata.annotations?.[ANNOTATION_EDIT_URL];
|
||||
|
||||
let cardClass = '';
|
||||
if (variant === 'gridItem') {
|
||||
cardClass = classes.gridItemCard;
|
||||
} else if (variant === 'fullHeight') {
|
||||
cardClass = classes.fullHeightCard;
|
||||
}
|
||||
|
||||
let cardContentClass = '';
|
||||
if (variant === 'gridItem') {
|
||||
cardContentClass = classes.gridItemCardContent;
|
||||
} else if (variant === 'fullHeight') {
|
||||
cardContentClass = classes.fullHeightCardContent;
|
||||
}
|
||||
|
||||
const entityLocation = entity.metadata.annotations?.[ANNOTATION_LOCATION];
|
||||
// Limiting the ability to manually refresh to the less expensive locations
|
||||
const allowRefresh =
|
||||
@@ -234,60 +197,58 @@ export function InternalAboutCard(props: InternalAboutCardProps) {
|
||||
}, [catalogApi, entity, alertApi, t, errorApi]);
|
||||
|
||||
return (
|
||||
<Card className={cardClass}>
|
||||
<CardHeader
|
||||
title={t('aboutCard.title')}
|
||||
action={
|
||||
<>
|
||||
{allowRefresh && canRefresh && (
|
||||
<IconButton
|
||||
aria-label={t('aboutCard.refreshButtonAriaLabel')}
|
||||
title={t('aboutCard.refreshButtonTitle')}
|
||||
onClick={refreshEntity}
|
||||
>
|
||||
<CachedIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<EntityInfoCard
|
||||
title={t('aboutCard.title')}
|
||||
headerActions={
|
||||
<>
|
||||
{allowRefresh && canRefresh && (
|
||||
<IconButton
|
||||
aria-label={t('aboutCard.refreshButtonAriaLabel')}
|
||||
title={t('aboutCard.refreshButtonTitle')}
|
||||
onClick={refreshEntity}
|
||||
>
|
||||
<CachedIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
component={Link}
|
||||
aria-label={t('aboutCard.editButtonAriaLabel')}
|
||||
disabled={!entityMetadataEditUrl}
|
||||
title={t('aboutCard.editButtonTitle')}
|
||||
to={entityMetadataEditUrl ?? '#'}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
{sourceTemplateRef && templateRoute && (
|
||||
<IconButton
|
||||
component={Link}
|
||||
aria-label={t('aboutCard.editButtonAriaLabel')}
|
||||
disabled={!entityMetadataEditUrl}
|
||||
title={t('aboutCard.editButtonTitle')}
|
||||
to={entityMetadataEditUrl ?? '#'}
|
||||
title={t('aboutCard.createSimilarButtonTitle')}
|
||||
to={templateRoute({
|
||||
namespace: sourceTemplateRef.namespace,
|
||||
templateName: sourceTemplateRef.name,
|
||||
})}
|
||||
>
|
||||
<EditIcon />
|
||||
<AppIcon id="scaffolder" />
|
||||
</IconButton>
|
||||
{sourceTemplateRef && templateRoute && (
|
||||
<IconButton
|
||||
component={Link}
|
||||
title={t('aboutCard.createSimilarButtonTitle')}
|
||||
to={templateRoute({
|
||||
namespace: sourceTemplateRef.namespace,
|
||||
templateName: sourceTemplateRef.name,
|
||||
})}
|
||||
>
|
||||
<AppIcon id="scaffolder" />
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
subheader={subheader ?? <DefaultAboutCardSubheader />}
|
||||
/>
|
||||
<Divider />
|
||||
<CardContent className={cardContentClass}>
|
||||
<AboutContent entity={entity} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={classes.linkContainer}>
|
||||
{iconLinks ?? <DefaultAboutCardSubheader />}
|
||||
</div>
|
||||
<AboutContent entity={entity} />
|
||||
</EntityInfoCard>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exported publicly via the EntityAboutCard
|
||||
*
|
||||
* NOTE: We generally do not accept pull requests to extend this class with more
|
||||
* props and customizability. If you need to tweak it, consider making a bespoke
|
||||
* card in your own repository instead, that is perfect for your own needs.
|
||||
* NOTE: We generally do not accept pull requests to extend this class with props
|
||||
* and customizability. If you need to tweak it, consider making a bespoke card
|
||||
* in your own repository instead, that is perfect for your own needs.
|
||||
*/
|
||||
export function AboutCard(props: AboutCardProps) {
|
||||
return <InternalAboutCard {...props} />;
|
||||
export function AboutCard() {
|
||||
return <InternalAboutCard />;
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ import {
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { JsonArray } from '@backstage/types';
|
||||
import Chip from '@material-ui/core/Chip';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { Grid } from '@backstage/ui';
|
||||
import { MarkdownContent } from '@backstage/core-components';
|
||||
import { AboutField } from './AboutField';
|
||||
import { LinksGridList } from '../EntityLinksCard/LinksGridList';
|
||||
@@ -114,25 +114,25 @@ export function AboutContent(props: AboutContentProps) {
|
||||
entitySourceLocation = undefined;
|
||||
}
|
||||
|
||||
const columns = { initial: '1', sm: '2', lg: '3' } as const;
|
||||
|
||||
return (
|
||||
<Grid container>
|
||||
<AboutField
|
||||
label={t('aboutCard.descriptionField.label')}
|
||||
gridSizes={{ xs: 12 }}
|
||||
>
|
||||
<MarkdownContent
|
||||
className={classes.description}
|
||||
content={
|
||||
entity?.metadata?.description ||
|
||||
t('aboutCard.descriptionField.value')
|
||||
}
|
||||
/>
|
||||
</AboutField>
|
||||
<Grid.Root columns={columns} gap="3">
|
||||
<Grid.Item colSpan={columns}>
|
||||
<AboutField label={t('aboutCard.descriptionField.label')}>
|
||||
<MarkdownContent
|
||||
className={classes.description}
|
||||
content={
|
||||
entity?.metadata?.description ||
|
||||
t('aboutCard.descriptionField.value')
|
||||
}
|
||||
/>
|
||||
</AboutField>
|
||||
</Grid.Item>
|
||||
<AboutField
|
||||
label={t('aboutCard.ownerField.label')}
|
||||
value={t('aboutCard.ownerField.value')}
|
||||
className={classes.description}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
>
|
||||
{ownedByRelations.length > 0 && (
|
||||
<EntityRefLinks entityRefs={ownedByRelations} defaultKind="group" />
|
||||
@@ -142,7 +142,6 @@ export function AboutContent(props: AboutContentProps) {
|
||||
<AboutField
|
||||
label={t('aboutCard.domainField.label')}
|
||||
value={t('aboutCard.domainField.value')}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
>
|
||||
{partOfDomainRelations.length > 0 && (
|
||||
<EntityRefLinks
|
||||
@@ -159,7 +158,6 @@ export function AboutContent(props: AboutContentProps) {
|
||||
<AboutField
|
||||
label={t('aboutCard.systemField.label')}
|
||||
value={t('aboutCard.systemField.value')}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
>
|
||||
{partOfSystemRelations.length > 0 && (
|
||||
<EntityRefLinks
|
||||
@@ -173,7 +171,6 @@ export function AboutContent(props: AboutContentProps) {
|
||||
<AboutField
|
||||
label={t('aboutCard.parentComponentField.label')}
|
||||
value={t('aboutCard.parentComponentField.value')}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
>
|
||||
<EntityRefLinks
|
||||
entityRefs={partOfComponentRelations}
|
||||
@@ -191,7 +188,6 @@ export function AboutContent(props: AboutContentProps) {
|
||||
<AboutField
|
||||
label={t('aboutCard.typeField.label')}
|
||||
value={entity?.spec?.type as string}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
/>
|
||||
)}
|
||||
{(isAPI ||
|
||||
@@ -200,38 +196,37 @@ export function AboutContent(props: AboutContentProps) {
|
||||
<AboutField
|
||||
label={t('aboutCard.lifecycleField.label')}
|
||||
value={entity?.spec?.lifecycle as string}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
/>
|
||||
)}
|
||||
<AboutField
|
||||
label={t('aboutCard.tagsField.label')}
|
||||
value={t('aboutCard.tagsField.value')}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
>
|
||||
{(entity?.metadata?.tags || []).map(tag => (
|
||||
<Chip key={tag} size="small" label={tag} />
|
||||
))}
|
||||
</AboutField>
|
||||
{isLocation && (entity?.spec?.targets || entity?.spec?.target) && (
|
||||
<AboutField
|
||||
label={t('aboutCard.targetsField.label')}
|
||||
gridSizes={{ xs: 12 }}
|
||||
>
|
||||
<LinksGridList
|
||||
cols={1}
|
||||
items={((entity.spec.targets as JsonArray) || [entity.spec.target])
|
||||
.map(target => target as string)
|
||||
.map(target => ({
|
||||
text: target,
|
||||
href: getLocationTargetHref(
|
||||
target,
|
||||
(entity?.spec?.type || t('aboutCard.unknown')) as string,
|
||||
entitySourceLocation!,
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</AboutField>
|
||||
<Grid.Item colSpan={columns}>
|
||||
<AboutField label={t('aboutCard.targetsField.label')}>
|
||||
<LinksGridList
|
||||
cols={1}
|
||||
items={(
|
||||
(entity.spec.targets as JsonArray) || [entity.spec.target]
|
||||
)
|
||||
.map(target => target as string)
|
||||
.map(target => ({
|
||||
text: target,
|
||||
href: getLocationTargetHref(
|
||||
target,
|
||||
(entity?.spec?.type || t('aboutCard.unknown')) as string,
|
||||
entitySourceLocation!,
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</AboutField>
|
||||
</Grid.Item>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import { useElementFilter } from '@backstage/core-plugin-api';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { ReactNode } from 'react';
|
||||
@@ -48,14 +47,13 @@ const useStyles = makeStyles(theme => ({
|
||||
export interface AboutFieldProps {
|
||||
label: string;
|
||||
value?: string;
|
||||
gridSizes?: Record<string, number>;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function AboutField(props: AboutFieldProps) {
|
||||
const { label, value, gridSizes, children, className } = props;
|
||||
const { label, value, children, className } = props;
|
||||
const classes = useStyles();
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
@@ -71,11 +69,11 @@ export function AboutField(props: AboutFieldProps) {
|
||||
</Typography>
|
||||
);
|
||||
return (
|
||||
<Grid item {...gridSizes} className={className}>
|
||||
<div className={className}>
|
||||
<Typography variant="h2" className={classes.label}>
|
||||
{label}
|
||||
</Typography>
|
||||
{content}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
export { AboutCard } from './AboutCard';
|
||||
export type { AboutCardProps } from './AboutCard';
|
||||
export { AboutContent } from './AboutContent';
|
||||
export type { AboutContentProps } from './AboutContent';
|
||||
export { AboutField } from './AboutField';
|
||||
|
||||
@@ -14,77 +14,77 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
InfoCard,
|
||||
InfoCardVariants,
|
||||
Table,
|
||||
TableColumn,
|
||||
} from '@backstage/core-components';
|
||||
import { useEntity, EntityInfoCard } from '@backstage/plugin-catalog-react';
|
||||
import { EntityLabelsEmptyState } from './EntityLabelsEmptyState';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import {
|
||||
Table,
|
||||
CellText,
|
||||
useTable,
|
||||
type ColumnConfig,
|
||||
type TableItem,
|
||||
} from '@backstage/ui';
|
||||
import { catalogTranslationRef } from '../../alpha/translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
/** @public */
|
||||
export interface EntityLabelsCardProps {
|
||||
variant?: InfoCardVariants;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(_ => ({
|
||||
key: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
}));
|
||||
interface LabelItem extends TableItem {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const EntityLabelsCard = (props: EntityLabelsCardProps) => {
|
||||
const { variant, title } = props;
|
||||
const { title } = props;
|
||||
const { entity } = useEntity();
|
||||
const classes = useStyles();
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
const columns: TableColumn<{ key: string; value: string }>[] = [
|
||||
{
|
||||
render: row => {
|
||||
return (
|
||||
<Typography className={classes.key} variant="body2">
|
||||
{row.key}
|
||||
</Typography>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
},
|
||||
];
|
||||
|
||||
const labels = entity?.metadata?.labels;
|
||||
|
||||
const columnConfig: ColumnConfig<LabelItem>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'key',
|
||||
label: t('entityLabelsCard.columnKeyLabel'),
|
||||
isRowHeader: true,
|
||||
cell: item => <CellText title={item.key} />,
|
||||
},
|
||||
{
|
||||
id: 'value',
|
||||
label: t('entityLabelsCard.columnValueLabel'),
|
||||
cell: item => <CellText title={item.value} />,
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
const data = useMemo(
|
||||
() =>
|
||||
Object.keys(labels ?? {}).map(labelKey => ({
|
||||
id: labelKey,
|
||||
key: labelKey,
|
||||
value: labels![labelKey],
|
||||
})),
|
||||
[labels],
|
||||
);
|
||||
|
||||
const { tableProps } = useTable({
|
||||
mode: 'complete',
|
||||
data,
|
||||
paginationOptions: { pageSize: 5 },
|
||||
});
|
||||
|
||||
return (
|
||||
<InfoCard title={title || t('entityLabelsCard.title')} variant={variant}>
|
||||
<EntityInfoCard title={title || t('entityLabelsCard.title')}>
|
||||
{!labels || Object.keys(labels).length === 0 ? (
|
||||
<EntityLabelsEmptyState />
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
data={Object.keys(labels).map(labelKey => ({
|
||||
key: labelKey,
|
||||
value: labels[labelKey],
|
||||
}))}
|
||||
options={{
|
||||
search: false,
|
||||
showTitle: true,
|
||||
loadingType: 'linear',
|
||||
header: false,
|
||||
padding: 'dense',
|
||||
pageSize: 5,
|
||||
toolbar: false,
|
||||
paging: Object.keys(labels).length > 5,
|
||||
}}
|
||||
/>
|
||||
<Table columnConfig={columnConfig} {...tableProps} />
|
||||
)}
|
||||
</InfoCard>
|
||||
</EntityInfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,24 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { useEntity, EntityInfoCard } from '@backstage/plugin-catalog-react';
|
||||
import LanguageIcon from '@material-ui/icons/Language';
|
||||
import { EntityLinksEmptyState } from './EntityLinksEmptyState';
|
||||
import { LinksGridList } from './LinksGridList';
|
||||
import { ColumnBreakpoints } from './types';
|
||||
import { IconComponent, useApp } from '@backstage/core-plugin-api';
|
||||
import { InfoCard, InfoCardVariants } from '@backstage/core-components';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { catalogTranslationRef } from '../../alpha/translation';
|
||||
|
||||
/** @public */
|
||||
export interface EntityLinksCardProps {
|
||||
cols?: ColumnBreakpoints | number;
|
||||
variant?: InfoCardVariants;
|
||||
}
|
||||
|
||||
export const EntityLinksCard = (props: EntityLinksCardProps) => {
|
||||
const { cols = undefined, variant } = props;
|
||||
const { cols = undefined } = props;
|
||||
const { entity } = useEntity();
|
||||
const app = useApp();
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
@@ -42,7 +40,7 @@ export const EntityLinksCard = (props: EntityLinksCardProps) => {
|
||||
const links = entity?.metadata?.links;
|
||||
|
||||
return (
|
||||
<InfoCard title={t('entityLinksCard.title')} variant={variant}>
|
||||
<EntityInfoCard title={t('entityLinksCard.title')}>
|
||||
{!links || links.length === 0 ? (
|
||||
<EntityLinksEmptyState />
|
||||
) : (
|
||||
@@ -55,6 +53,6 @@ export const EntityLinksCard = (props: EntityLinksCardProps) => {
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</InfoCard>
|
||||
</EntityInfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ImageList from '@material-ui/core/ImageList';
|
||||
import ImageListItem from '@material-ui/core/ImageListItem';
|
||||
import { Grid, type Columns } from '@backstage/ui';
|
||||
import { IconLink } from './IconLink';
|
||||
import { ColumnBreakpoints } from './types';
|
||||
import { useDynamicColumns } from './useDynamicColumns';
|
||||
@@ -37,12 +36,13 @@ export function LinksGridList(props: LinksGridListProps) {
|
||||
const numOfCols = useDynamicColumns(cols);
|
||||
|
||||
return (
|
||||
<ImageList rowHeight="auto" cols={numOfCols}>
|
||||
<Grid.Root
|
||||
columns={String(Math.min(Math.max(numOfCols, 1), 12)) as Columns}
|
||||
gap="2"
|
||||
>
|
||||
{items.map(({ text, href, Icon }, i) => (
|
||||
<ImageListItem key={i}>
|
||||
<IconLink href={href} text={text ?? href} Icon={Icon} />
|
||||
</ImageListItem>
|
||||
<IconLink key={i} href={href} text={text ?? href} Icon={Icon} />
|
||||
))}
|
||||
</ImageList>
|
||||
</Grid.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
export * from './apis';
|
||||
|
||||
export type {
|
||||
AboutCardProps,
|
||||
AboutContentProps,
|
||||
AboutFieldProps,
|
||||
} from './components/AboutCard';
|
||||
|
||||
@@ -43,7 +43,6 @@ import {
|
||||
SearchResultListItemExtensionProps,
|
||||
} from '@backstage/plugin-search-react';
|
||||
import { DefaultStarredEntitiesApi } from './apis';
|
||||
import { AboutCardProps } from './components/AboutCard';
|
||||
import { DefaultCatalogPageProps } from './components/CatalogPage';
|
||||
import { DependencyOfComponentsCardProps } from './components/DependencyOfComponentsCard';
|
||||
import { DependsOnComponentsCardProps } from './components/DependsOnComponentsCard';
|
||||
@@ -128,15 +127,14 @@ export const CatalogEntityPage: () => JSX.Element = catalogPlugin.provide(
|
||||
* not extremely customizable; feel free to make a copy of it as a starting
|
||||
* point if you like.
|
||||
*/
|
||||
export const EntityAboutCard: (props: AboutCardProps) => JSX.Element =
|
||||
catalogPlugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'EntityAboutCard',
|
||||
component: {
|
||||
lazy: () => import('./components/AboutCard').then(m => m.AboutCard),
|
||||
},
|
||||
}),
|
||||
);
|
||||
export const EntityAboutCard: () => JSX.Element = catalogPlugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'EntityAboutCard',
|
||||
component: {
|
||||
lazy: () => import('./components/AboutCard').then(m => m.AboutCard),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
/** @public */
|
||||
export const EntityLinksCard = catalogPlugin.provide(
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"@backstage/frontend-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-catalog-common": "workspace:^",
|
||||
"@backstage/plugin-catalog-react": "workspace:^",
|
||||
"@backstage/ui": "workspace:^",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
|
||||
@@ -18,7 +18,6 @@ export type ComponentsGridClassKey =
|
||||
|
||||
// @public (undocumented)
|
||||
export const EntityGroupProfileCard: (props: {
|
||||
variant?: InfoCardVariants;
|
||||
showLinks?: boolean;
|
||||
}) => JSX_2.Element;
|
||||
|
||||
@@ -48,7 +47,6 @@ export type EntityRelationAggregation = 'direct' | 'aggregated';
|
||||
|
||||
// @public (undocumented)
|
||||
export const EntityUserProfileCard: (props: {
|
||||
variant?: InfoCardVariants;
|
||||
showLinks?: boolean;
|
||||
maxRelations?: number;
|
||||
hideIcons?: boolean;
|
||||
@@ -56,7 +54,6 @@ export const EntityUserProfileCard: (props: {
|
||||
|
||||
// @public (undocumented)
|
||||
export const GroupProfileCard: (props: {
|
||||
variant?: InfoCardVariants;
|
||||
showLinks?: boolean;
|
||||
}) => JSX_2.Element;
|
||||
|
||||
@@ -116,7 +113,6 @@ export type OwnershipCardClassKey =
|
||||
|
||||
// @public (undocumented)
|
||||
export const UserProfileCard: (props: {
|
||||
variant?: InfoCardVariants;
|
||||
showLinks?: boolean;
|
||||
maxRelations?: number;
|
||||
hideIcons?: boolean;
|
||||
|
||||
@@ -22,13 +22,7 @@ import {
|
||||
RELATION_PARENT_OF,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import {
|
||||
Avatar,
|
||||
InfoCard,
|
||||
InfoCardVariants,
|
||||
Link,
|
||||
} from '@backstage/core-components';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import { Link } from '@backstage/core-components';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
@@ -36,6 +30,7 @@ import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import {
|
||||
EntityInfoCard,
|
||||
EntityRefLinks,
|
||||
catalogApiRef,
|
||||
getEntityRelations,
|
||||
@@ -57,6 +52,7 @@ import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { orgTranslationRef } from '../../../../translation';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { Avatar, Flex, Box, Text } from '@backstage/ui';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
@@ -71,18 +67,21 @@ const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const CardTitle = (props: { title: string }) => (
|
||||
<Box display="flex" alignItems="center">
|
||||
<GroupIcon fontSize="inherit" />
|
||||
<Box ml={1}>{props.title}</Box>
|
||||
</Box>
|
||||
const CardTitle = (props: { title: string; pictureSrc?: string }) => (
|
||||
<Flex align="center" gap="2">
|
||||
<Avatar
|
||||
size="small"
|
||||
purpose="decoration"
|
||||
name={props.title}
|
||||
src={props.pictureSrc || ''}
|
||||
/>
|
||||
{props.title}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
/** @public */
|
||||
export const GroupProfileCard = (props: {
|
||||
variant?: InfoCardVariants;
|
||||
showLinks?: boolean;
|
||||
}) => {
|
||||
export const GroupProfileCard = (props: { showLinks?: boolean }) => {
|
||||
const { showLinks } = props;
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const { entity: group } = useEntity<GroupEntity>();
|
||||
@@ -148,11 +147,9 @@ export const GroupProfileCard = (props: {
|
||||
);
|
||||
|
||||
return (
|
||||
<InfoCard
|
||||
title={<CardTitle title={displayName} />}
|
||||
subheader={description}
|
||||
variant={props.variant}
|
||||
action={
|
||||
<EntityInfoCard
|
||||
title={<CardTitle title={displayName} pictureSrc={profile?.picture} />}
|
||||
headerActions={
|
||||
<>
|
||||
{allowRefresh && canRefresh && (
|
||||
<IconButton
|
||||
@@ -167,8 +164,8 @@ export const GroupProfileCard = (props: {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Box className={classes.container}>
|
||||
<Avatar displayName={displayName} picture={profile?.picture} />
|
||||
{description && <Text color="secondary">{description}</Text>}
|
||||
<Box p="2">
|
||||
<List className={classes.list}>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
@@ -234,9 +231,9 @@ export const GroupProfileCard = (props: {
|
||||
secondary={t('groupProfileCard.listItemTitle.childGroups')}
|
||||
/>
|
||||
</ListItem>
|
||||
{props?.showLinks && <LinksGroup links={links} />}
|
||||
{showLinks && <LinksGroup links={links} />}
|
||||
</List>
|
||||
</Box>
|
||||
</InfoCard>
|
||||
</EntityInfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ export const Default = () => (
|
||||
<EntityProvider entity={defaultEntity}>
|
||||
<Grid container spacing={4}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<UserProfileCard variant="gridItem" />
|
||||
<UserProfileCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</EntityProvider>
|
||||
@@ -82,7 +82,7 @@ export const NoImage = () => (
|
||||
<EntityProvider entity={noImageEntity}>
|
||||
<Grid container spacing={4}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<UserProfileCard variant="gridItem" />
|
||||
<UserProfileCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</EntityProvider>
|
||||
@@ -134,7 +134,7 @@ export const ExtraDetails = () => (
|
||||
<EntityProvider entity={extraDetailsEntity}>
|
||||
<Grid container spacing={4}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<UserProfileCard variant="gridItem" />
|
||||
<UserProfileCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</EntityProvider>
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('UserSummary Test', () => {
|
||||
it('Display Profile Card', async () => {
|
||||
await renderInTestApp(
|
||||
<EntityProvider entity={userEntity}>
|
||||
<UserProfileCard variant="gridItem" />
|
||||
<UserProfileCard />
|
||||
</EntityProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
@@ -61,10 +61,10 @@ describe('UserSummary Test', () => {
|
||||
);
|
||||
|
||||
expect(screen.getByText('calum-leavy@example.com')).toBeInTheDocument();
|
||||
expect(screen.getByAltText('Calum Leavy')).toHaveAttribute(
|
||||
'src',
|
||||
'https://example.com/staff/calum.jpeg',
|
||||
);
|
||||
// BUI Avatar is decorative (aria-hidden), because the name is
|
||||
// present as text right beside it.
|
||||
expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
|
||||
expect(screen.getByText('Calum Leavy')).toBeInTheDocument();
|
||||
expect(screen.getByText('examplegroup').closest('a')).toHaveAttribute(
|
||||
'href',
|
||||
'/catalog/default/group/examplegroup',
|
||||
@@ -87,7 +87,7 @@ describe('UserSummary Test', () => {
|
||||
it('Should limit the number of displayed groups in the card', async () => {
|
||||
await renderInTestApp(
|
||||
<EntityProvider entity={entity}>
|
||||
<UserProfileCard variant="gridItem" maxRelations={3} />
|
||||
<UserProfileCard maxRelations={3} />
|
||||
</EntityProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
@@ -109,7 +109,7 @@ describe('UserSummary Test', () => {
|
||||
it('Show more groups button when there are more user relations than the maximum', async () => {
|
||||
await renderInTestApp(
|
||||
<EntityProvider entity={entity}>
|
||||
<UserProfileCard variant="gridItem" maxRelations={3} />
|
||||
<UserProfileCard maxRelations={3} />
|
||||
</EntityProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
@@ -128,7 +128,7 @@ describe('UserSummary Test', () => {
|
||||
it('Hide more groups button when limit value is less than or equal to user relations', async () => {
|
||||
await renderInTestApp(
|
||||
<EntityProvider entity={entity}>
|
||||
<UserProfileCard variant="gridItem" maxRelations={5} />
|
||||
<UserProfileCard maxRelations={5} />
|
||||
</EntityProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
@@ -148,7 +148,7 @@ describe('UserSummary Test', () => {
|
||||
it('Hide all groups when max relations is equals to zero', async () => {
|
||||
await renderInTestApp(
|
||||
<EntityProvider entity={entity}>
|
||||
<UserProfileCard variant="gridItem" maxRelations={0} />
|
||||
<UserProfileCard maxRelations={0} />
|
||||
</EntityProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
@@ -193,7 +193,7 @@ describe('Edit Button', () => {
|
||||
|
||||
await renderInTestApp(
|
||||
<EntityProvider entity={userEntity}>
|
||||
<UserProfileCard variant="gridItem" />
|
||||
<UserProfileCard />
|
||||
</EntityProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
@@ -234,7 +234,7 @@ describe('Edit Button', () => {
|
||||
|
||||
await renderInTestApp(
|
||||
<EntityProvider entity={userEntity}>
|
||||
<UserProfileCard variant="gridItem" />
|
||||
<UserProfileCard />
|
||||
</EntityProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
@@ -285,7 +285,7 @@ describe('Edit Button', () => {
|
||||
|
||||
await renderInTestApp(
|
||||
<EntityProvider entity={userEntity}>
|
||||
<UserProfileCard variant="gridItem" />
|
||||
<UserProfileCard />
|
||||
</EntityProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
@@ -337,7 +337,7 @@ describe('Edit Button', () => {
|
||||
|
||||
await renderInTestApp(
|
||||
<EntityProvider entity={userEntity}>
|
||||
<UserProfileCard showLinks variant="gridItem" />
|
||||
<UserProfileCard showLinks />
|
||||
</EntityProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
|
||||
@@ -19,15 +19,10 @@ import {
|
||||
RELATION_MEMBER_OF,
|
||||
UserEntity,
|
||||
} from '@backstage/catalog-model';
|
||||
import {
|
||||
Avatar,
|
||||
InfoCard,
|
||||
InfoCardVariants,
|
||||
Link,
|
||||
} from '@backstage/core-components';
|
||||
import { Link } from '@backstage/core-components';
|
||||
import { EntityInfoCard } from '@backstage/plugin-catalog-react';
|
||||
import { Avatar, Box, Flex, Text } from '@backstage/ui';
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import BaseButton from '@material-ui/core/ButtonBase';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import List from '@material-ui/core/List';
|
||||
@@ -52,7 +47,6 @@ import EditIcon from '@material-ui/icons/Edit';
|
||||
import EmailIcon from '@material-ui/icons/Email';
|
||||
import GroupIcon from '@material-ui/icons/Group';
|
||||
import { LinksGroup } from '../../Meta';
|
||||
import PersonIcon from '@material-ui/icons/Person';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
@@ -84,17 +78,21 @@ const useStyles = makeStyles(
|
||||
{ name: 'PluginOrgUserProfileCard' },
|
||||
);
|
||||
|
||||
const CardTitle = (props: { title?: string }) =>
|
||||
const CardTitle = (props: { title: string; pictureSrc?: string }) =>
|
||||
props.title ? (
|
||||
<Box display="flex" alignItems="center">
|
||||
<PersonIcon fontSize="inherit" />
|
||||
<Box ml={1}>{props.title}</Box>
|
||||
</Box>
|
||||
<Flex align="center" gap="2">
|
||||
<Avatar
|
||||
size="small"
|
||||
purpose="decoration"
|
||||
name={props.title}
|
||||
src={props.pictureSrc || ''}
|
||||
/>
|
||||
{props.title}
|
||||
</Flex>
|
||||
) : null;
|
||||
|
||||
/** @public */
|
||||
export const UserProfileCard = (props: {
|
||||
variant?: InfoCardVariants;
|
||||
showLinks?: boolean;
|
||||
maxRelations?: number;
|
||||
hideIcons?: boolean;
|
||||
@@ -132,11 +130,9 @@ export const UserProfileCard = (props: {
|
||||
});
|
||||
|
||||
return (
|
||||
<InfoCard
|
||||
title={<CardTitle title={displayName} />}
|
||||
subheader={description}
|
||||
variant={props.variant}
|
||||
action={
|
||||
<EntityInfoCard
|
||||
title={<CardTitle title={displayName} pictureSrc={profile?.picture} />}
|
||||
headerActions={
|
||||
<>
|
||||
{entityMetadataEditUrl && (
|
||||
<IconButton
|
||||
@@ -151,62 +147,55 @@ export const UserProfileCard = (props: {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Grid container spacing={3} alignItems="flex-start">
|
||||
<Grid item xs={12} sm={2} xl={1}>
|
||||
<Avatar displayName={displayName} picture={profile?.picture} />
|
||||
</Grid>
|
||||
{description && <Text color="secondary">{description}</Text>}
|
||||
<Box p="2">
|
||||
<List>
|
||||
{profile?.email && (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Tooltip title={t('userProfileCard.listItemTitle.email')}>
|
||||
<EmailIcon />
|
||||
</Tooltip>
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<Link to={emailHref ?? ''}>{profile.email}</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
<Grid item md={10} xl={11}>
|
||||
<List>
|
||||
{profile?.email && (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Tooltip title={t('userProfileCard.listItemTitle.email')}>
|
||||
<EmailIcon />
|
||||
</Tooltip>
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<Link to={emailHref ?? ''}>{profile.email}</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
{maxRelations === undefined || maxRelations > 0 ? (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Tooltip title={t('userProfileCard.listItemTitle.memberOf')}>
|
||||
<GroupIcon />
|
||||
</Tooltip>
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<EntityRefLinks
|
||||
entityRefs={memberOfRelations.slice(0, maxRelations)}
|
||||
defaultKind="Group"
|
||||
hideIcons={hideIcons}
|
||||
/>
|
||||
{maxRelations && memberOfRelations.length > maxRelations ? (
|
||||
<>
|
||||
,
|
||||
<BaseButton
|
||||
className={classes.moreButton}
|
||||
onClick={toggleAllGroupsDialog}
|
||||
disableRipple
|
||||
>
|
||||
{t('userProfileCard.moreGroupButtonTitle', {
|
||||
number: String(
|
||||
memberOfRelations.length - maxRelations,
|
||||
),
|
||||
})}
|
||||
</BaseButton>
|
||||
</>
|
||||
) : null}
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
) : null}
|
||||
{props?.showLinks && <LinksGroup links={links} />}
|
||||
</List>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{maxRelations === undefined || maxRelations > 0 ? (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Tooltip title={t('userProfileCard.listItemTitle.memberOf')}>
|
||||
<GroupIcon />
|
||||
</Tooltip>
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<EntityRefLinks
|
||||
entityRefs={memberOfRelations.slice(0, maxRelations)}
|
||||
defaultKind="Group"
|
||||
hideIcons={hideIcons}
|
||||
/>
|
||||
{maxRelations && memberOfRelations.length > maxRelations ? (
|
||||
<>
|
||||
,
|
||||
<BaseButton
|
||||
className={classes.moreButton}
|
||||
onClick={toggleAllGroupsDialog}
|
||||
disableRipple
|
||||
>
|
||||
{t('userProfileCard.moreGroupButtonTitle', {
|
||||
number: String(memberOfRelations.length - maxRelations),
|
||||
})}
|
||||
</BaseButton>
|
||||
</>
|
||||
) : null}
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
) : null}
|
||||
{props?.showLinks && <LinksGroup links={links} />}
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
<Dialog
|
||||
classes={{ paper: classes.dialogPaper }}
|
||||
@@ -238,6 +227,6 @@ export const UserProfileCard = (props: {
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</InfoCard>
|
||||
</EntityInfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ const serviceEntityPage = (
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
<Grid item md={6}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
<EntityAboutCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</EntityLayout.Route>
|
||||
|
||||
@@ -10,7 +10,7 @@ const serviceEntityPage = (
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
<Grid item md={6}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
<EntityAboutCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</EntityLayout.Route>
|
||||
|
||||
@@ -6525,6 +6525,7 @@ __metadata:
|
||||
"@backstage/plugin-permission-react": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@backstage/ui": "workspace:^"
|
||||
"@material-ui/core": "npm:^4.12.2"
|
||||
"@material-ui/icons": "npm:^4.9.1"
|
||||
"@material-ui/lab": "npm:4.0.0-alpha.61"
|
||||
|
||||
Reference in New Issue
Block a user