feat(catalog): add entity links card component
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Adds an `EntityLinksCard` component to display `entity.metadata.links` on entity pages. The new component is a companion for the new [Entity Links](https://backstage.io/docs/features/software-catalog/descriptor-format#links-optional) catalog model addition.
|
||||
|
||||
Here is an example usage within an `EntityPage.tsx`.
|
||||
|
||||
```tsx
|
||||
// in packages/app/src/components/catalog/EntityPage.tsx
|
||||
const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
<Grid item md={4} sm={6}>
|
||||
<EntityLinksCard entity={entity} />
|
||||
// or ...
|
||||
<EntityLinksCard entity={entity} cols={{ md: 2, lg: 3, xl: 4 }} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
```
|
||||
@@ -28,7 +28,11 @@ import {
|
||||
ProvidedApisCard,
|
||||
ProvidingComponentsCard,
|
||||
} from '@backstage/plugin-api-docs';
|
||||
import { AboutCard, EntityPageLayout } from '@backstage/plugin-catalog';
|
||||
import {
|
||||
AboutCard,
|
||||
EntityLinksCard,
|
||||
EntityPageLayout,
|
||||
} from '@backstage/plugin-catalog';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
isPluginApplicableToEntity as isCircleCIAvailable,
|
||||
@@ -177,6 +181,9 @@ const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
|
||||
<PagerDutyCard entity={entity} />
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item md={4} sm={6}>
|
||||
<EntityLinksCard entity={entity} />
|
||||
</Grid>
|
||||
<RecentCICDRunsSwitcher entity={entity} />
|
||||
{isGitHubAvailable(entity) && (
|
||||
<>
|
||||
|
||||
@@ -7,11 +7,24 @@ metadata:
|
||||
- java
|
||||
- data
|
||||
links:
|
||||
- url: https://example.com/apm/artists-lookup
|
||||
title: APM
|
||||
- url: https://example.com/user
|
||||
title: Examples Users
|
||||
icon: user
|
||||
- url: https://example.com/group
|
||||
title: Example Group
|
||||
icon: group
|
||||
- url: https://example.com/cloud
|
||||
title: Link with Cloud Icon
|
||||
icon: cloud
|
||||
- url: https://example.com/dashboard
|
||||
title: Dashboard
|
||||
icon: dashboard
|
||||
- url: https://example.com/logs/artists-lookup
|
||||
title: Logs
|
||||
- url: https://example.com/help
|
||||
title: Support
|
||||
icon: help
|
||||
- url: https://example.com/web
|
||||
title: Website
|
||||
icon: web
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: experimental
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, EntityLink } from '@backstage/catalog-model';
|
||||
import { EntityContext } from '@backstage/plugin-catalog-react';
|
||||
import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
|
||||
import React from 'react';
|
||||
import { EntityLinksCard } from './EntityLinksCard';
|
||||
|
||||
describe('EntityLinksCard', () => {
|
||||
const createEntity = (links: EntityLink[] = []): Entity =>
|
||||
({
|
||||
metadata: {
|
||||
links,
|
||||
},
|
||||
} as Entity);
|
||||
|
||||
const createLink = ({
|
||||
url = 'https://dashboard.dashexample.com',
|
||||
title = 'admin dashboard',
|
||||
icon = undefined,
|
||||
}: Partial<EntityLink> = {}): EntityLink => ({
|
||||
url,
|
||||
title,
|
||||
icon,
|
||||
});
|
||||
|
||||
it('should render a link', async () => {
|
||||
const links: EntityLink[] = [createLink()];
|
||||
const entityContextValue = {
|
||||
entity: createEntity(links),
|
||||
loading: false,
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
const { queryByText } = await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<EntityContext.Provider value={entityContextValue}>
|
||||
<EntityLinksCard />
|
||||
</EntityContext.Provider>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(queryByText('admin dashboard')).toBeInTheDocument();
|
||||
expect(queryByText('derp')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show empty state', async () => {
|
||||
const entityContextValue = {
|
||||
entity: createEntity([]),
|
||||
loading: false,
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
const { queryByText } = await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<EntityContext.Provider value={entityContextValue}>
|
||||
<EntityLinksCard />
|
||||
</EntityContext.Provider>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
queryByText(/.*No links defined for this entity.*/),
|
||||
).toBeInTheDocument();
|
||||
expect(queryByText('admin dashboard')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { IconComponent, InfoCard, useApp } from '@backstage/core';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import LanguageIcon from '@material-ui/icons/Language';
|
||||
import React from 'react';
|
||||
import { EntityLinksEmptyState } from './EntityLinksEmptyState';
|
||||
import { LinksGridList } from './LinksGridList';
|
||||
import { ColumnBreakpoints } from './types';
|
||||
|
||||
type Props = {
|
||||
/** @deprecated The entity is now grabbed from context instead */
|
||||
entity?: Entity;
|
||||
cols?: ColumnBreakpoints | number;
|
||||
};
|
||||
|
||||
export const EntityLinksCard = ({ cols = undefined }: Props) => {
|
||||
const { entity } = useEntity();
|
||||
const app = useApp();
|
||||
|
||||
// TODO: Refactor App.icons & App.getSystemIcon to support custom icons
|
||||
const iconResolver = (key: string | undefined): IconComponent => {
|
||||
return app.getSystemIcon(key as any) ?? LanguageIcon;
|
||||
};
|
||||
|
||||
const links = entity?.metadata?.links;
|
||||
|
||||
return (
|
||||
<InfoCard title="Links">
|
||||
{!links || links.length === 0 ? (
|
||||
<EntityLinksEmptyState />
|
||||
) : (
|
||||
<LinksGridList
|
||||
cols={cols}
|
||||
items={links.map(({ url, title, icon }) => ({
|
||||
text: title ?? url,
|
||||
href: url,
|
||||
Icon: iconResolver(icon),
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { CodeSnippet } from '@backstage/core';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { Button, makeStyles, Typography } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
|
||||
const ENTITY_YAML = `metadata:
|
||||
name: example
|
||||
links:
|
||||
- url: https://dashboard.example.com
|
||||
title: My Dashboard
|
||||
icon: dashboard`;
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
code: {
|
||||
borderRadius: 6,
|
||||
margin: `${theme.spacing(2)}px 0px`,
|
||||
background: theme.palette.type === 'dark' ? '#444' : '#fff',
|
||||
},
|
||||
}));
|
||||
|
||||
export const EntityLinksEmptyState = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="body1">
|
||||
No links defined for this entity. You can add links to your entity YAML
|
||||
as shown in the highlighted example below:
|
||||
</Typography>
|
||||
<div className={classes.code}>
|
||||
<CodeSnippet
|
||||
text={ENTITY_YAML}
|
||||
language="yaml"
|
||||
showLineNumbers
|
||||
highlightedNumbers={[3, 4, 5, 6]}
|
||||
customStyle={{ background: 'inherit', fontSize: '115%' }}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
target="_blank"
|
||||
href="https://backstage.io/docs/features/software-catalog/descriptor-format#links-optional"
|
||||
>
|
||||
Read more
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { lightTheme } from '@backstage/theme';
|
||||
import { ThemeProvider } from '@material-ui/core';
|
||||
import CloudIcon from '@material-ui/icons/Cloud';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { IconLink } from './IconLink';
|
||||
|
||||
describe('IconLink', () => {
|
||||
it('should render an icon link', () => {
|
||||
const rendered = render(
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<IconLink
|
||||
href="https://example.com"
|
||||
text="I am Link"
|
||||
Icon={CloudIcon}
|
||||
/>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
expect(rendered.queryByText('I am Link')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { Link, IconComponent } from '@backstage/core';
|
||||
import { Grid, makeStyles, Typography } from '@material-ui/core';
|
||||
import LanguageIcon from '@material-ui/icons/Language';
|
||||
import React from 'react';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
svgIcon: {
|
||||
display: 'inline-block',
|
||||
'& svg': {
|
||||
display: 'inline-block',
|
||||
fontSize: 'inherit',
|
||||
verticalAlign: 'baseline',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const IconLink = ({
|
||||
href,
|
||||
text,
|
||||
Icon,
|
||||
}: {
|
||||
href: string;
|
||||
text?: string;
|
||||
Icon?: IconComponent;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Grid container direction="row" spacing={1}>
|
||||
<Grid item>
|
||||
<Typography component="div" className={classes.svgIcon}>
|
||||
{Icon ? <Icon /> : <LanguageIcon />}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Link to={href} target="_blank" rel="noopener">
|
||||
{text || href}
|
||||
</Link>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { IconComponent } from '@backstage/core';
|
||||
import { Grid, GridList, GridListTile } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { IconLink } from './IconLink';
|
||||
import { ColumnBreakpoints } from './types';
|
||||
import { useDynamicColumns } from './useDynamicColumns';
|
||||
|
||||
export type LinksGridListItem = {
|
||||
href: string;
|
||||
text?: string;
|
||||
Icon?: IconComponent;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
items: LinksGridListItem[];
|
||||
cols?: ColumnBreakpoints | number;
|
||||
};
|
||||
|
||||
export const LinksGridList = ({ items, cols = undefined }: Props) => {
|
||||
const numOfCols = useDynamicColumns(cols);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
direction="column"
|
||||
justify="space-evenly"
|
||||
alignItems="flex-start"
|
||||
spacing={0}
|
||||
>
|
||||
<GridList cellHeight="auto" cols={numOfCols}>
|
||||
{items.map(({ text, href, Icon }, i) => (
|
||||
<GridListTile key={i}>
|
||||
<IconLink href={href} text={text ?? href} Icon={Icon} />
|
||||
</GridListTile>
|
||||
))}
|
||||
</GridList>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { EntityLinksCard } from './EntityLinksCard';
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
|
||||
export type ColumnBreakpoints = Record<Breakpoint, number>;
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { Theme, useMediaQuery } from '@material-ui/core';
|
||||
import { Breakpoint, ColumnBreakpoints } from './types';
|
||||
|
||||
const colDefaults: ColumnBreakpoints = {
|
||||
xs: 1,
|
||||
sm: 1,
|
||||
md: 1,
|
||||
lg: 2,
|
||||
xl: 3,
|
||||
};
|
||||
|
||||
export function useDynamicColumns(
|
||||
cols: ColumnBreakpoints | number | undefined,
|
||||
): number {
|
||||
const matches: (Breakpoint | null)[] = [
|
||||
useMediaQuery((theme: Theme) => theme.breakpoints.up('xl')) ? 'xl' : null,
|
||||
useMediaQuery((theme: Theme) => theme.breakpoints.up('lg')) ? 'lg' : null,
|
||||
useMediaQuery((theme: Theme) => theme.breakpoints.up('md')) ? 'md' : null,
|
||||
useMediaQuery((theme: Theme) => theme.breakpoints.up('sm')) ? 'sm' : null,
|
||||
useMediaQuery((theme: Theme) => theme.breakpoints.up('xs')) ? 'xs' : null,
|
||||
];
|
||||
|
||||
let numOfCols = 1;
|
||||
|
||||
if (typeof cols === 'number') {
|
||||
numOfCols = cols;
|
||||
} else {
|
||||
const breakpoint = matches.find(k => k !== null) ?? 'xs';
|
||||
numOfCols = cols?.[breakpoint] ?? colDefaults[breakpoint];
|
||||
}
|
||||
|
||||
return numOfCols;
|
||||
}
|
||||
@@ -24,4 +24,5 @@ export {
|
||||
catalogPlugin as plugin,
|
||||
CatalogIndexPage,
|
||||
CatalogEntityPage,
|
||||
EntityLinksCard,
|
||||
} from './plugin';
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
discoveryApiRef,
|
||||
createComponentExtension,
|
||||
createRoutableExtension,
|
||||
identityApiRef,
|
||||
} from '@backstage/core';
|
||||
@@ -65,3 +66,12 @@ export const CatalogEntityPage = catalogPlugin.provide(
|
||||
mountPoint: entityRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
export const EntityLinksCard = catalogPlugin.provide(
|
||||
createComponentExtension({
|
||||
component: {
|
||||
lazy: () =>
|
||||
import('./components/EntityLinksCard').then(m => m.EntityLinksCard),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user