Merge branch 'master' of github.com:spotify/backstage into shmidt-i/app-catalog-tabs-routes-everything-is-connected

This commit is contained in:
Ivan Shmidt
2020-09-01 11:39:37 +02:00
13 changed files with 335 additions and 42 deletions
+2
View File
@@ -48,6 +48,8 @@ lighthouse:
baseUrl: http://localhost:3003
catalog:
rules:
allow: [Component, API, Group, Template]
processors:
githubApi:
privateToken:
@@ -32,7 +32,7 @@ For example, given the following configuration:
```yaml
catalog:
rules:
- allow: [Component, API, System]
- allow: [Component, API, Template]
locations:
- type: github
@@ -40,7 +40,7 @@ catalog:
allow: [Group]
```
We are able to add entities of kind `Component`, `API`, or `System` from any
We are able to add entities of kind `Component`, `API`, or `Template` from any
location, and `Group` entities from the `org-data.yaml`, which will also be read
as statically configured location.
@@ -58,6 +58,28 @@ Currently the catalog supports loading definitions from GitHub + Local Files. To
load from other places, not only will there need to be another preparer, but the
support to load the location will also need to be added to the Catalog.
You can add the template files to the catalog through
[static location configuration](../software-catalog/configuration.md#static-location-configuration),
for example
```yaml
catalog:
locations:
- type: github
target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml
allow: [Template]
```
Templates can also be added by posting the to the catalog directly. Note that if
you're doing this, you need to configure the catalog to allow template entities
to be ingested from any source, for example:
```yaml
catalog:
rules:
allow: [Component, API, Template]
```
For loading from a file, the following command should work when the backend is
running:
@@ -2,13 +2,13 @@ import { createPlugin, createRouteRef } from '@backstage/core';
import ExampleComponent from './components/ExampleComponent';
export const rootRouteRef = createRouteRef({
path: '/{{ id }}',
title: '{{ id }}',
path: '/{{ id }}',
title: '{{ id }}',
});
export const plugin = createPlugin({
id: '{{ id }}',
register({ router }) {
router.addRoute(rootRouteRef, ExampleComponent);
},
id: '{{ id }}',
register({ router }) {
router.addRoute(rootRouteRef, ExampleComponent);
},
});
+1 -1
View File
@@ -38,7 +38,7 @@ const useStyles = makeStyles<BackstageTheme, { backgroundImage: string }>(
alignItems: 'center',
backgroundImage: props => props.backgroundImage,
backgroundPosition: 'center',
backgroundSize: '100% 400px',
backgroundSize: 'cover',
},
leftItemsBox: {
flex: '1 1 auto',
@@ -69,11 +69,16 @@ catalog:
# Backstage example templates
- type: github
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml
allow: [Template]
- type: github
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml
allow: [Template]
- type: github
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml
allow: [Template]
- type: github
target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml
allow: [Template]
- type: github
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml
allow: [Template]
+11 -2
View File
@@ -199,8 +199,17 @@ class DevAppBuilder {
for (const plugin of plugins) {
for (const output of plugin.output()) {
if (output.type === 'legacy-route') {
paths.push(output.path);
switch (output.type) {
case 'legacy-route': {
paths.push(output.path);
break;
}
case 'route': {
paths.push(output.target.path);
break;
}
default:
break;
}
}
}
@@ -0,0 +1,46 @@
/*
* 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 { render } from '@testing-library/react';
import { AboutCard } from './AboutCard';
describe('<AboutCard />', () => {
it('renders info and "view source" link', () => {
const entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'software',
annotations: {
'backstage.io/managed-by-location':
'github:https://github.com/spotify/backstage/blob/master/software.yaml',
},
},
spec: {
owner: 'guest',
type: 'service',
lifecycle: 'production',
},
};
const { getByText } = render(<AboutCard entity={entity} />);
expect(getByText('service')).toBeInTheDocument();
expect(getByText('View Source').closest('a')).toHaveAttribute(
'href',
'https://github.com/spotify/backstage/blob/master/software.yaml',
);
});
});
@@ -0,0 +1,183 @@
/*
* 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,
makeStyles,
Chip,
IconButton,
Card,
CardContent,
CardHeader,
Divider,
} from '@material-ui/core';
import { Entity } from '@backstage/catalog-model';
import GitHubIcon from '@material-ui/icons/GitHub';
import { IconLinkVertical } from './IconLinkVertical';
import EditIcon from '@material-ui/icons/Edit';
import DocsIcon from '@material-ui/icons/Description';
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 iconMap: Record<string, React.ReactNode> = {
github: <GitHubIcon />,
};
type CodeLinkInfo = { icon?: React.ReactNode; href?: string };
function getCodeLinkInfo(entity: Entity): CodeLinkInfo {
const location =
entity?.metadata?.annotations?.['backstage.io/managed-by-location'];
if (location) {
// split by first `:`
// e.g. "github:https://github.com/spotify/backstage/blob/master/software.yaml"
const [type, target] = location.split(/:(.+)/);
return { icon: iconMap[type], href: target };
}
return {};
}
type AboutCardProps = {
entity: Entity;
};
export function AboutCard({ entity }: AboutCardProps) {
const classes = useStyles();
const codeLink = getCodeLinkInfo(entity);
return (
<Card>
<CardHeader
title="About"
action={
<IconButton href={codeLink.href || '#'} aria-label="Edit">
<EditIcon />
</IconButton>
}
subheader={
<nav className={classes.links}>
<IconLinkVertical label="View Source" {...codeLink} />
<IconLinkVertical
label="View Techdocs"
icon={<DocsIcon />}
href={`/docs/${''}`}
/>
</nav>
}
/>
<Divider />
<CardContent>
<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?.spec?.owner as string}
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>
</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,53 @@
/*
* 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 * as React from 'react';
import { makeStyles, Link } from '@material-ui/core';
import LinkIcon from '@material-ui/icons/Link';
export type IconLinkVerticalProps = {
icon?: React.ReactNode;
href?: string;
label: string;
};
const useIconStyles = makeStyles({
link: {
display: 'grid',
justifyItems: 'center',
gridGap: 4,
textAlign: 'center',
},
label: {
fontSize: '0.7rem',
textTransform: 'uppercase',
fontWeight: 600,
letterSpacing: 1.2,
},
});
export function IconLinkVertical({
icon = <LinkIcon />,
href = '#',
...props
}: IconLinkVerticalProps) {
const classes = useIconStyles();
return (
<Link className={classes.link} href={href} {...props}>
{icon}
<span className={classes.label}>{props.label}</span>
</Link>
);
}
@@ -14,16 +14,4 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { InfoCard, StructuredMetadataTable } from '@backstage/core';
import React, { FC } from 'react';
type Props = {
entity: Entity;
};
export const EntityMetadataCard: FC<Props> = ({ entity }) => (
<InfoCard title="Information">
<StructuredMetadataTable metadata={entity.metadata} />
</InfoCard>
);
export { IconLinkVertical } from './IconLinkVertical';
@@ -14,19 +14,4 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { render } from '@testing-library/react';
import React from 'react';
import { EntityMetadataCard } from './EntityMetadataCard';
describe('EntityMetadataCard component', () => {
it('should display entity name if provided', async () => {
const testEntity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: { name: 'test' },
};
const rendered = await render(<EntityMetadataCard entity={testEntity} />);
expect(await rendered.findByText('test')).toBeInTheDocument();
});
});
export { AboutCard } from './AboutCard';
@@ -24,14 +24,14 @@ import {
} from '@backstage/plugin-jenkins';
import { Grid } from '@material-ui/core';
import React, { FC } from 'react';
import { EntityMetadataCard } from '../EntityMetadataCard/EntityMetadataCard';
import { AboutCard } from '../AboutCard';
export const EntityPageOverview: FC<{ entity: Entity }> = ({ entity }) => {
return (
<Content>
<Grid container spacing={3}>
<Grid item sm={4}>
<EntityMetadataCard entity={entity} />
<AboutCard entity={entity} />
</Grid>
{entity.metadata?.annotations?.[
'backstage.io/jenkins-github-folder'