Merge pull request #3704 from backstage/samiram/aboutCard

Samiram/about card
This commit is contained in:
samiramkr
2020-12-21 15:08:29 +01:00
committed by GitHub
14 changed files with 231 additions and 324 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/core': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-pagerduty': patch
---
Create AboutCard in core and use it in pagerduty and catalog plugin
@@ -14,8 +14,7 @@
* limitations under the License.
*/
import React from 'react';
import { VerticalIcon } from './VerticalIcon';
import { SubHeaderLink } from '../types';
import { IconLinkVertical, IconLinkVerticalProps } from './IconLinkVertical';
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles(theme => ({
@@ -29,21 +28,15 @@ const useStyles = makeStyles(theme => ({
}));
type Props = {
links: SubHeaderLink[];
links: IconLinkVerticalProps[];
};
export const SubHeader = ({ links }: Props) => {
export const HeaderIconLinkRow = ({ links }: Props) => {
const classes = useStyles();
return (
<nav className={classes.links}>
{links.map(link => (
<VerticalIcon
key={link.title}
label={link.title}
href={link.href ?? '#'}
action={link.action}
icon={link.icon}
/>
{links.map((link, index) => (
<IconLinkVertical key={index + 1} {...link} />
))}
</nav>
);
@@ -13,21 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from 'react';
import React from 'react';
import classnames from 'classnames';
import { makeStyles, Link } from '@material-ui/core';
import LinkIcon from '@material-ui/icons/Link';
import { Link as RouterLink } from 'react-router-dom';
import { Link as RouterLink } from '../Link';
export type IconLinkVerticalProps = {
icon?: React.ReactNode;
href?: string;
disabled?: boolean;
title?: string;
label: string;
action?: React.ReactNode;
};
const useIconStyles = makeStyles({
const useIconStyles = makeStyles(theme => ({
link: {
display: 'grid',
justifyItems: 'center',
@@ -43,12 +43,16 @@ const useIconStyles = makeStyles({
fontWeight: 600,
letterSpacing: 1.2,
},
});
linkStyle: {
color: theme.palette.secondary.main,
},
}));
export function IconLinkVertical({
icon = <LinkIcon />,
href = '#',
disabled = false,
action,
...props
}: IconLinkVerticalProps) {
const classes = useIconStyles();
@@ -58,7 +62,6 @@ export function IconLinkVertical({
<Link
className={classnames(classes.link, classes.disabled)}
underline="none"
title={props.title}
{...props}
>
{icon}
@@ -67,12 +70,11 @@ export function IconLinkVertical({
);
}
// Absolute links should not be using RouterLink
if (href?.startsWith('//') || href?.includes('://')) {
if (action) {
return (
<Link className={classes.link} href={href} {...props}>
<Link className={classnames(classes.link, classes.linkStyle)} {...props}>
{icon}
<span className={classes.label}>{props.label}</span>
{action}
</Link>
);
}
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { IconLinkVertical } from './IconLinkVertical';
export { HeaderIconLinkRow } from './HeaderIconLinkRow';
+1
View File
@@ -38,3 +38,4 @@ export * from './TrendLine';
export * from './WarningPanel';
export * from './EmptyState';
export * from './MarkdownContent';
export * from './HeaderIconLinkRow';
@@ -17,56 +17,27 @@
import {
Entity,
ENTITY_DEFAULT_NAMESPACE,
RELATION_OWNED_BY,
RELATION_PROVIDES_API,
serializeEntityRef,
} from '@backstage/catalog-model';
import {
Card,
CardContent,
CardHeader,
Chip,
Divider,
Grid,
IconButton,
makeStyles,
Typography,
} from '@material-ui/core';
import ExtensionIcon from '@material-ui/icons/Extension';
import DocsIcon from '@material-ui/icons/Description';
import EditIcon from '@material-ui/icons/Edit';
import GitHubIcon from '@material-ui/icons/GitHub';
import React from 'react';
import { IconLinkVertical } from './IconLinkVertical';
import { findLocationForEntityMeta } from '../../data/utils';
import { createEditLink, determineUrlType } from '../createEditLink';
import { HeaderIconLinkRow } from '@backstage/core';
import { AboutContent } from './AboutContent';
const useStyles = makeStyles(theme => ({
links: {
margin: theme.spacing(2, 0),
display: 'grid',
gridAutoFlow: 'column',
gridAutoColumns: 'min-content',
gridGap: theme.spacing(3),
},
label: {
color: theme.palette.text.secondary,
textTransform: 'uppercase',
fontSize: '10px',
fontWeight: 'bold',
letterSpacing: 0.5,
overflow: 'hidden',
whiteSpace: 'nowrap',
},
value: {
fontWeight: 'bold',
overflow: 'hidden',
lineHeight: '24px',
wordBreak: 'break-word',
},
description: {
wordBreak: 'break-word',
},
const useStyles = makeStyles({
gridItemCard: {
display: 'flex',
flexDirection: 'column',
@@ -76,7 +47,7 @@ const useStyles = makeStyles(theme => ({
gridItemCardContent: {
flex: 1,
},
}));
});
const iconMap: Record<string, React.ReactNode> = {
github: <GitHubIcon />,
@@ -114,6 +85,25 @@ export function AboutCard({ entity, variant }: AboutCardProps) {
const codeLink = getCodeLinkInfo(entity);
// TODO: Also support RELATION_CONSUMES_API here
const hasApis = entity.relations?.some(r => r.type === RELATION_PROVIDES_API);
const viewInSource = {
label: 'View Source',
...codeLink,
};
const viewInTechDocs = {
label: 'View TechDocs',
disabled: !entity.metadata.annotations?.['backstage.io/techdocs-ref'],
icon: <DocsIcon />,
href: `/docs/${entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE}/${
entity.kind
}/${entity.metadata.name}`,
};
const viewApi = {
title: hasApis ? '' : 'No APIs available',
label: 'View API',
disabled: !hasApis,
icon: <ExtensionIcon />,
href: 'api',
};
return (
<Card className={variant === 'gridItem' ? classes.gridItemCard : ''}>
@@ -131,117 +121,15 @@ export function AboutCard({ entity, variant }: AboutCardProps) {
</IconButton>
}
subheader={
<nav className={classes.links}>
<IconLinkVertical label="View Source" {...codeLink} />
<IconLinkVertical
disabled={
!entity.metadata.annotations?.['backstage.io/techdocs-ref']
}
label="View TechDocs"
title={
!entity.metadata.annotations?.['backstage.io/techdocs-ref']
? 'No TechDocs available'
: ''
}
icon={<DocsIcon />}
href={`/docs/${
entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE
}/${entity.kind}/${entity.metadata.name}`}
/>
<IconLinkVertical
disabled={!hasApis}
label="View API"
title={hasApis ? '' : 'No APIs available'}
icon={<ExtensionIcon />}
href="api"
/>
</nav>
<HeaderIconLinkRow links={[viewInSource, viewInTechDocs, viewApi]} />
}
/>
<Divider />
<CardContent
className={variant === 'gridItem' ? classes.gridItemCardContent : ''}
>
<Grid container>
<AboutField label="Description" gridSizes={{ xs: 12 }}>
<Typography
variant="body2"
paragraph
className={classes.description}
>
{entity?.metadata?.description || 'No description'}
</Typography>
</AboutField>
<AboutField
label="Owner"
value={entity?.relations
?.filter(r => r.type === RELATION_OWNED_BY)
.map(({ target: { kind, name, namespace } }) =>
// TODO(Rugvip): we want to provide some utils for this
serializeEntityRef({
kind,
name,
namespace:
namespace === ENTITY_DEFAULT_NAMESPACE
? undefined
: namespace,
}),
)
.join(', ')}
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
/>
<AboutField
label="Type"
value={entity?.spec?.type as string}
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
/>
<AboutField
label="Lifecycle"
value={entity?.spec?.lifecycle as string}
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
/>
<AboutField
label="Tags"
value="No Tags"
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
>
{(entity?.metadata?.tags || []).map(t => (
<Chip key={t} size="small" label={t} />
))}
</AboutField>
</Grid>
<AboutContent entity={entity} />
</CardContent>
</Card>
);
}
function AboutField({
label,
value,
gridSizes,
children,
}: {
label: string;
value?: string;
gridSizes?: Record<string, number>;
children?: React.ReactNode;
}) {
const classes = useStyles();
// Content is either children or a string prop `value`
const content = React.Children.count(children) ? (
children
) : (
<Typography variant="body2" className={classes.value}>
{value || `unknown`}
</Typography>
);
return (
<Grid item {...gridSizes}>
<Typography variant="subtitle2" className={classes.label}>
{label}
</Typography>
{content}
</Grid>
);
}
@@ -0,0 +1,83 @@
/*
* 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 React from 'react';
import { Grid, Typography, Chip, makeStyles } from '@material-ui/core';
import { AboutField } from './AboutField';
import {
Entity,
ENTITY_DEFAULT_NAMESPACE,
RELATION_OWNED_BY,
serializeEntityRef,
} from '@backstage/catalog-model';
const useStyles = makeStyles({
description: {
wordBreak: 'break-word',
},
});
type Props = {
entity: Entity;
};
export const AboutContent = ({ entity }: Props) => {
const classes = useStyles();
return (
<Grid container>
<AboutField label="Description" gridSizes={{ xs: 12 }}>
<Typography variant="body2" paragraph className={classes.description}>
{entity?.metadata?.description || 'No description'}
</Typography>
</AboutField>
<AboutField
label="Owner"
value={entity?.relations
?.filter(r => r.type === RELATION_OWNED_BY)
.map(({ target: { kind, name, namespace } }) =>
// TODO(Rugvip): we want to provide some utils for this
serializeEntityRef({
kind,
name,
namespace:
namespace === ENTITY_DEFAULT_NAMESPACE ? undefined : namespace,
}),
)
.join(', ')}
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
/>
<AboutField
label="Type"
value={entity?.spec?.type as string}
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
/>
<AboutField
label="Lifecycle"
value={entity?.spec?.lifecycle as string}
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
/>
<AboutField
label="Tags"
value="No Tags"
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
>
{(entity?.metadata?.tags || []).map(t => (
<Chip key={t} size="small" label={t} />
))}
</AboutField>
</Grid>
);
};
@@ -0,0 +1,64 @@
/*
* 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 React from 'react';
import { makeStyles, Typography, Grid } from '@material-ui/core';
const useStyles = makeStyles(theme => ({
value: {
fontWeight: 'bold',
overflow: 'hidden',
lineHeight: '24px',
wordBreak: 'break-word',
},
label: {
color: theme.palette.text.secondary,
textTransform: 'uppercase',
fontSize: '10px',
fontWeight: 'bold',
letterSpacing: 0.5,
overflow: 'hidden',
whiteSpace: 'nowrap',
},
}));
type Props = {
label: string;
value?: string;
gridSizes?: Record<string, number>;
children?: React.ReactNode;
};
export const AboutField = ({ label, value, gridSizes, children }: Props) => {
const classes = useStyles();
// Content is either children or a string prop `value`
const content = React.Children.count(children) ? (
children
) : (
<Typography variant="body2" className={classes.value}>
{value || `unknown`}
</Typography>
);
return (
<Grid item {...gridSizes}>
<Typography variant="subtitle2" className={classes.label}>
{label}
</Typography>
{content}
</Grid>
);
};
@@ -15,4 +15,3 @@
*/
export { AboutCard } from './AboutCard';
export { IconLinkVertical } from './IconLinkVertical';
+1 -1
View File
@@ -15,7 +15,7 @@
*/
export * from '@backstage/catalog-client';
export { AboutCard, IconLinkVertical } from './components/AboutCard';
export { AboutCard } from './components/AboutCard';
export { EntityPageLayout } from './components/EntityPageLayout';
export { Router } from './components/Router';
export { useEntityCompoundName } from './components/useEntityCompoundName';
@@ -1,33 +0,0 @@
/*
* 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 React from 'react';
import { Card, CardContent, CardHeader, Divider } from '@material-ui/core';
import { SubHeader } from './SubHeader';
import { SubHeaderLink } from '../types';
type Props = {
title: string;
links: SubHeaderLink[];
content: React.ReactNode;
};
export const AboutCard = ({ title, links, content }: Props) => (
<Card>
<CardHeader title={title} subheader={<SubHeader links={links} />} />
<Divider />
<CardContent>{content}</CardContent>
</Card>
);
@@ -1,85 +0,0 @@
/*
* 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 React from 'react';
import classnames from 'classnames';
import { makeStyles, Link } from '@material-ui/core';
import LinkIcon from '@material-ui/icons/Link';
import { Link as RouterLink } from 'react-router-dom';
export type VerticalIconProps = {
icon?: React.ReactNode;
href?: string;
title?: string;
label: string;
action?: React.ReactNode;
};
const useIconStyles = makeStyles(theme => ({
link: {
display: 'grid',
justifyItems: 'center',
gridGap: 4,
textAlign: 'center',
},
label: {
fontSize: '0.7rem',
textTransform: 'uppercase',
fontWeight: 600,
letterSpacing: 1.2,
},
linkStyle: {
color: theme.palette.secondary.main,
},
}));
export function VerticalIcon({
icon = <LinkIcon />,
href = '#',
action,
...props
}: VerticalIconProps) {
const classes = useIconStyles();
if (action) {
return (
<Link
className={classnames(classes.link, classes.linkStyle)}
href={href}
{...props}
>
{icon}
{action}
</Link>
);
}
// Absolute links should not be using RouterLink
if (href?.startsWith('//') || href?.includes('://')) {
return (
<Link className={classes.link} href={href} {...props}>
{icon}
<span className={classes.label}>{props.label}</span>
</Link>
);
}
return (
<Link className={classes.link} to={href} component={RouterLink} {...props}>
{icon}
<span className={classes.label}>{props.label}</span>
</Link>
);
}
@@ -1,19 +0,0 @@
/*
* 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 { AboutCard } from './AboutCard';
// export { SubHeader } from './SubHeader';
// export { VerticalIcon } from './VerticalIcon';
@@ -14,9 +14,16 @@
* limitations under the License.
*/
import React, { useState, useCallback } from 'react';
import { useApi, Progress } from '@backstage/core';
import { useApi, Progress, HeaderIconLinkRow } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { Button, makeStyles } from '@material-ui/core';
import {
Button,
makeStyles,
Card,
CardHeader,
Divider,
CardContent,
} from '@material-ui/core';
import { Incidents } from './Incident';
import { EscalationPolicy } from './Escalation';
import { useAsync } from 'react-use';
@@ -26,7 +33,6 @@ import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
import { TriggerDialog } from './TriggerDialog';
import { MissingTokenError } from './Errors/MissingTokenError';
import WebIcon from '@material-ui/icons/Web';
import { AboutCard } from './About/AboutCard';
const useStyles = makeStyles({
triggerAlarm: {
@@ -98,13 +104,13 @@ export const PagerDutyCard = ({ entity }: Props) => {
}
const serviceLink = {
title: 'Service Directory',
label: 'Service Directory',
href: service!.url,
icon: <WebIcon />,
};
const triggerLink = {
title: 'Create Incident',
label: 'Create Incident',
action: (
<Button
data-testid="trigger-button"
@@ -119,25 +125,26 @@ export const PagerDutyCard = ({ entity }: Props) => {
};
return (
<AboutCard
title="PagerDuty"
links={[serviceLink, triggerLink]}
content={
<>
<Incidents
serviceId={service!.id}
refreshIncidents={refreshIncidents}
/>
<EscalationPolicy policyId={service!.policyId} />
<TriggerDialog
showDialog={showDialog}
handleDialog={handleDialog}
name={entity.metadata.name}
integrationKey={integrationKey}
onIncidentCreated={handleRefresh}
/>
</>
}
/>
<Card>
<CardHeader
title="PagerDuty"
subheader={<HeaderIconLinkRow links={[serviceLink, triggerLink]} />}
/>
<Divider />
<CardContent>
<Incidents
serviceId={service!.id}
refreshIncidents={refreshIncidents}
/>
<EscalationPolicy policyId={service!.policyId} />
<TriggerDialog
showDialog={showDialog}
handleDialog={handleDialog}
name={entity.metadata.name}
integrationKey={integrationKey}
onIncidentCreated={handleRefresh}
/>
</CardContent>
</Card>
);
};