Merge branch 'master' of github.com:spotify/backstage into blam/scaffolder
* 'master' of github.com:spotify/backstage: [Plugin] Explore features in Backstage (#696) [Storybook] Fix Storybook compilation warnings (#701) Make the inventory backend standalone runnable
This commit is contained in:
+1
-1
@@ -4,4 +4,4 @@
|
||||
# The last matching pattern takes precedence.
|
||||
# https://help.github.com/articles/about-codeowners/
|
||||
|
||||
* @spotify/backstage-core
|
||||
* @spotify/backstage-core
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@
|
||||
"husky": "^4.2.3",
|
||||
"lerna": "^3.20.2",
|
||||
"lint-staged": "^10.1.0",
|
||||
"prettier": "^1.19.1"
|
||||
"prettier": "^2.0.5"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"dependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.4",
|
||||
"@backstage/core": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-explore": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-home-page": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-inventory": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-lighthouse": "^0.1.1-alpha.4",
|
||||
|
||||
@@ -18,6 +18,7 @@ import React, { FC, useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link, makeStyles, Typography } from '@material-ui/core';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import ExploreIcon from '@material-ui/icons/Explore';
|
||||
import AccountCircle from '@material-ui/icons/AccountCircle';
|
||||
import CreateComponentIcon from '@material-ui/icons/Create';
|
||||
import AccountTreeIcon from '@material-ui/icons/AccountTree';
|
||||
@@ -81,7 +82,9 @@ const Root: FC<{}> = ({ children }) => (
|
||||
<SidebarSpacer />
|
||||
<SidebarDivider />
|
||||
<SidebarItem icon={HomeIcon} to="/" text="Home" />
|
||||
<SidebarItem icon={ExploreIcon} to="/explore" text="Explore" />
|
||||
<SidebarItem icon={CreateComponentIcon} to="/scaffolder" text="Create" />
|
||||
<SidebarDivider />
|
||||
<SidebarItem icon={AccountTreeIcon} to="/inventory" text="Inventory" />
|
||||
<SidebarItem icon={AccountCircle} to="/login" text="Login" />
|
||||
<SidebarDivider />
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { plugin as HomePagePlugin } from '@backstage/plugin-home-page';
|
||||
export { plugin as WelcomePlugin } from '@backstage/plugin-welcome';
|
||||
export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse';
|
||||
export { plugin as InventoryPlugin } from '@backstage/plugin-inventory';
|
||||
export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder';
|
||||
export { plugin as TechRadar } from '@backstage/plugin-tech-radar';
|
||||
export { plugin as Explore } from '@backstage/plugin-explore';
|
||||
|
||||
@@ -24,8 +24,8 @@ import {
|
||||
const app = express();
|
||||
app.use(requestLoggingHandler());
|
||||
app.use('/home', myHomeRouter);
|
||||
app.use(errorHandler());
|
||||
app.use(notFoundHandler());
|
||||
app.use(errorHandler());
|
||||
|
||||
app.listen(PORT, () => {
|
||||
getRootLogger().info(`Listening on port ${PORT}`);
|
||||
|
||||
@@ -28,32 +28,38 @@ import {
|
||||
notFoundHandler,
|
||||
requestLoggingHandler,
|
||||
} from '@backstage/backend-common';
|
||||
import { router as inventoryRouter } from '@backstage/plugin-inventory-backend';
|
||||
import { createRouter as inventoryRouter } from '@backstage/plugin-inventory-backend';
|
||||
import { createScaffolder } from '@backstage/plugin-scaffolder-backend';
|
||||
import compression from 'compression';
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import { testRouter } from './test';
|
||||
import { createScaffolder } from '@backstage/plugin-scaffolder-backend';
|
||||
|
||||
const DEFAULT_PORT = 7000;
|
||||
|
||||
const PORT = parseInt(process.env.PORT ?? '', 10) || DEFAULT_PORT;
|
||||
const app = express();
|
||||
const logger = getRootLogger().child({ type: 'plugin' });
|
||||
|
||||
const scaffolder = createScaffolder();
|
||||
async function main() {
|
||||
const inventory = await inventoryRouter({ logger });
|
||||
const scaffolder = createScaffolder();
|
||||
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(compression());
|
||||
app.use(express.json());
|
||||
app.use(requestLoggingHandler());
|
||||
app.use('/test', testRouter);
|
||||
app.use('/inventory', inventoryRouter);
|
||||
app.use('/scaffolder', scaffolder);
|
||||
app.use(errorHandler());
|
||||
app.use(notFoundHandler());
|
||||
const app = express();
|
||||
|
||||
app.listen(PORT, () => {
|
||||
getRootLogger().info(`Listening on port ${PORT}`);
|
||||
});
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(compression());
|
||||
app.use(express.json());
|
||||
app.use(requestLoggingHandler());
|
||||
app.use('/test', testRouter);
|
||||
app.use('/inventory', inventory);
|
||||
app.use('/scaffolder', scaffolder);
|
||||
app.use(notFoundHandler());
|
||||
app.use(errorHandler());
|
||||
|
||||
app.listen(PORT, () => {
|
||||
getRootLogger().info(`Listening on port ${PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -14,4 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { default, StepType } from './Sequence';
|
||||
export { default } from './Sequence';
|
||||
export type { StepType } from './Sequence';
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
|
||||
export * from './api';
|
||||
export { default as Page } from './layout/Page';
|
||||
export { gradients, pageTheme, PageTheme } from './layout/Page';
|
||||
export { gradients, pageTheme } from './layout/Page';
|
||||
export type { PageTheme } from './layout/Page';
|
||||
export { default as Content } from './layout/Content/Content';
|
||||
export { default as ContentHeader } from './layout/ContentHeader/ContentHeader';
|
||||
export { default as Header } from './layout/Header/Header';
|
||||
|
||||
@@ -14,4 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { default, Props } from './BottomLink';
|
||||
export { default } from './BottomLink';
|
||||
export type { Props } from './BottomLink';
|
||||
|
||||
@@ -15,4 +15,5 @@
|
||||
*/
|
||||
|
||||
export { default } from './Page';
|
||||
export { gradients, pageTheme, PageTheme } from './PageThemeProvider';
|
||||
export { gradients, pageTheme } from './PageThemeProvider';
|
||||
export type { PageTheme } from './PageThemeProvider';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
# Title
|
||||
Welcome to the explore plugin!
|
||||
|
||||
## Sub-section 1
|
||||
|
||||
## Sub-section 2
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@backstage/plugin-explore",
|
||||
"version": "0.1.1-alpha.4",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.4",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.4",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "5.0.2",
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.4",
|
||||
"@backstage/theme": "^0.1.1-alpha.4",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"classnames": "^2.2.6",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-use": "^13.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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 { wrapInThemedTestApp } from '@backstage/test-utils';
|
||||
|
||||
import ExploreCard from './ExploreCard';
|
||||
|
||||
const minProps = {
|
||||
card: {
|
||||
title: 'Title',
|
||||
description: 'Something something',
|
||||
url: 'http://spotify.com/',
|
||||
image: 'https://developer.spotify.com/assets/WebAPI_intro.png',
|
||||
tags: ['tag1', 'tag2'],
|
||||
},
|
||||
};
|
||||
|
||||
describe('<ExploreCard />', () => {
|
||||
it('renders without exploding', () => {
|
||||
const { getByText } = render(
|
||||
wrapInThemedTestApp(<ExploreCard {...minProps} />),
|
||||
);
|
||||
expect(getByText('Explore')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders props correctly', () => {
|
||||
const { getByText } = render(
|
||||
wrapInThemedTestApp(<ExploreCard {...minProps} />),
|
||||
);
|
||||
expect(getByText(minProps.card.title)).toBeInTheDocument();
|
||||
expect(getByText(minProps.card.description)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should link out', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<ExploreCard {...minProps} />));
|
||||
const anchor = rendered.container.querySelector('a');
|
||||
expect(anchor.href).toBe(minProps.card.url);
|
||||
});
|
||||
|
||||
it('renders default description when missing', () => {
|
||||
const propsWithoutDescription = {
|
||||
card: {
|
||||
card: {
|
||||
title: 'Title',
|
||||
url: 'http://spotify.com/',
|
||||
image: 'https://developer.spotify.com/assets/WebAPI_intro.png',
|
||||
},
|
||||
},
|
||||
};
|
||||
const { getByText } = render(
|
||||
wrapInThemedTestApp(<ExploreCard {...propsWithoutDescription} />),
|
||||
);
|
||||
expect(getByText('Description missing')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders lifecycle correctly', () => {
|
||||
const propsWithLifecycle = {
|
||||
card: {
|
||||
title: 'Title',
|
||||
url: 'http://spotify.com/',
|
||||
image: 'https://developer.spotify.com/assets/WebAPI_intro.png',
|
||||
lifecycle: 'GA',
|
||||
},
|
||||
};
|
||||
const { queryByText } = render(
|
||||
wrapInThemedTestApp(<ExploreCard {...propsWithLifecycle} />),
|
||||
);
|
||||
expect(queryByText('GA')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders tags correctly', () => {
|
||||
const { getByText } = render(
|
||||
wrapInThemedTestApp(<ExploreCard {...minProps} />),
|
||||
);
|
||||
expect(getByText(minProps.card.tags[0])).toBeInTheDocument();
|
||||
expect(getByText(minProps.card.tags[1])).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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, { FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CardMedia,
|
||||
Chip,
|
||||
Typography,
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
card: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
cardActions: {
|
||||
flexGrow: 1,
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
media: {
|
||||
height: 128,
|
||||
},
|
||||
mediaContain: {
|
||||
backgroundSize: 'contain',
|
||||
},
|
||||
lifecycle: {
|
||||
lineHeight: '0.8em',
|
||||
color: 'white',
|
||||
},
|
||||
ga: {
|
||||
backgroundColor: theme.palette.status.ok,
|
||||
},
|
||||
alpha: {
|
||||
backgroundColor: theme.palette.status.error,
|
||||
},
|
||||
beta: {
|
||||
backgroundColor: theme.palette.status.warning,
|
||||
},
|
||||
domains: {
|
||||
position: 'relative',
|
||||
top: theme.spacing(2),
|
||||
},
|
||||
spaceBetween: {
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
}));
|
||||
|
||||
export type CardData = {
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
image: string;
|
||||
tags?: string[];
|
||||
lifecycle?: string;
|
||||
newsTag?: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
card: CardData;
|
||||
objectFit?: 'cover' | 'contain';
|
||||
};
|
||||
|
||||
const ExploreCard: FC<Props> = ({ card, objectFit }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const { title, description, url, image, lifecycle, newsTag, tags } = card;
|
||||
|
||||
return (
|
||||
<Card key={title} className={classes.card}>
|
||||
<CardMedia
|
||||
image={image}
|
||||
title={title}
|
||||
className={classNames(classes.media, {
|
||||
[classes.mediaContain]: objectFit === 'contain',
|
||||
})}
|
||||
/>
|
||||
<CardContent>
|
||||
<Typography gutterBottom variant="h5">
|
||||
{title}{' '}
|
||||
{lifecycle && lifecycle.toLowerCase() !== 'ga' && (
|
||||
<Chip
|
||||
label={lifecycle}
|
||||
className={classNames(
|
||||
classes.lifecycle,
|
||||
classes[lifecycle.toLowerCase()],
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
<Typography paragraph>
|
||||
{description || 'Description missing'}
|
||||
</Typography>
|
||||
{tags && (
|
||||
<div className={classes.domains}>
|
||||
{tags.map((item, idx) => (
|
||||
<Chip key={idx} label={item} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardActions
|
||||
className={classNames(classes.cardActions, {
|
||||
[classes.spaceBetween]: newsTag,
|
||||
})}
|
||||
>
|
||||
<Button size="small" color="primary" href={url} disabled={!url}>
|
||||
Explore
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreCard;
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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, { FC } from 'react';
|
||||
import { makeStyles, Typography } from '@material-ui/core';
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
Header,
|
||||
Page,
|
||||
pageTheme,
|
||||
SupportButton,
|
||||
} from '@backstage/core';
|
||||
import ExploreCard, { CardData } from './ExploreCard';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
container: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, 296px)',
|
||||
gridGap: theme.spacing(3),
|
||||
marginBottom: theme.spacing(6),
|
||||
},
|
||||
}));
|
||||
|
||||
const toolsCards = [
|
||||
{
|
||||
title: 'Lighthouse',
|
||||
description:
|
||||
"Google's Lighthouse tool is a great resource for benchmarking and improving the accessibility, performance, SEO, and best practices of your website.",
|
||||
url: '/lighthouse',
|
||||
image:
|
||||
'https://raw.githubusercontent.com/GoogleChrome/lighthouse/8b3d7f052b2e64dd857e741d7395647f487697e7/assets/lighthouse-logo.png',
|
||||
tags: ['web', 'seo', 'accessibility', 'performance'],
|
||||
},
|
||||
{
|
||||
title: 'Tech Radar',
|
||||
description:
|
||||
'Tech Radar is a list of technologies, complemented by an assessment result, called ring assignment.',
|
||||
url: '/tech-radar',
|
||||
image:
|
||||
'https://storage.googleapis.com/wf-blogs-engineering-media/2018/09/fe13bb32-wf-tech-radar-hero-1024x597.png',
|
||||
tags: ['standards', 'landscape'],
|
||||
},
|
||||
{
|
||||
title: 'GraphiQL',
|
||||
description:
|
||||
'Integrates GraphiQL as a tool to browse GraphiQL endpoints inside Backstage.',
|
||||
url: 'graphiql',
|
||||
image:
|
||||
'https://camo.githubusercontent.com/517398c3fbe0687d3d4dcbe05da82970b882e75a/68747470733a2f2f64337676366c703535716a6171632e636c6f756466726f6e742e6e65742f6974656d732f33413061324e314c3346324f304c3377326e316a2f477261706869514c382e706e673f582d436c6f75644170702d56697369746f722d49643d3433363432',
|
||||
tags: ['graphql', 'dev'],
|
||||
},
|
||||
];
|
||||
|
||||
const ExplorePluginPage: FC<{}> = () => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Page theme={pageTheme.home}>
|
||||
<Header
|
||||
title="Explore"
|
||||
subtitle="Tools and services available in Backstage"
|
||||
/>
|
||||
<Content>
|
||||
<ContentHeader title="Tools">
|
||||
<SupportButton>
|
||||
<Typography>Explore tools available in Backstage</Typography>
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<div className={classes.container}>
|
||||
{toolsCards.map((card: CardData, ix: any) => (
|
||||
<ExploreCard card={card} key={ix} />
|
||||
))}
|
||||
</div>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExplorePluginPage;
|
||||
@@ -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 { plugin } from './plugin';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 { plugin } from './plugin';
|
||||
|
||||
describe('explore', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(plugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -14,17 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import { createPlugin } from '@backstage/core';
|
||||
import ExplorePluginPage from './components/ExplorePluginPage';
|
||||
|
||||
export const router = express.Router();
|
||||
|
||||
router.get('/', async (_, res) => {
|
||||
res
|
||||
.status(200)
|
||||
.send([
|
||||
{ id: 'component1' },
|
||||
{ id: 'component2' },
|
||||
{ id: 'component3' },
|
||||
{ id: 'component4' },
|
||||
]);
|
||||
export const plugin = createPlugin({
|
||||
id: 'explore',
|
||||
register({ router }) {
|
||||
router.registerRoute('/explore', ExplorePluginPage);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 '@testing-library/jest-dom/extend-expect';
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src"
|
||||
}
|
||||
}
|
||||
@@ -5,18 +5,30 @@
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "tsc-watch --onFirstSuccess \"nodemon dist/run.js\"",
|
||||
"build": "tsc",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.4"
|
||||
"@backstage/cli": "^0.1.1-alpha.4",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"tsc-watch": "^4.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.17.1"
|
||||
"@backstage/backend-common": "0.1.1-alpha.4",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"helmet": "^3.22.0",
|
||||
"morgan": "^1.10.0",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
],
|
||||
"nodemonConfig": {
|
||||
"watch": "./dist"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { router } from './plugin';
|
||||
export * from './service/router';
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 { Component, Inventory } from './types';
|
||||
|
||||
export class AggregatorInventory implements Inventory {
|
||||
inventories: Inventory[] = [];
|
||||
|
||||
list(): Promise<Array<Component>> {
|
||||
return Promise.all(this.inventories.map(i => i.list())).then(lists =>
|
||||
lists.flat(),
|
||||
);
|
||||
}
|
||||
|
||||
item(id: string): Promise<Component | undefined> {
|
||||
return this.list().then(items => items.find(i => i.id === id));
|
||||
}
|
||||
|
||||
enlist(inventory: Inventory) {
|
||||
this.inventories.push(inventory);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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 { Component, Inventory } from './types';
|
||||
|
||||
export class StaticInventory implements Inventory {
|
||||
constructor(private components: Component[]) {}
|
||||
|
||||
list(): Promise<Array<Component>> {
|
||||
return Promise.resolve([...this.components]);
|
||||
}
|
||||
|
||||
item(id: string): Promise<Component | undefined> {
|
||||
return this.list().then(items => items.find(i => i.id === id));
|
||||
}
|
||||
}
|
||||
@@ -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 * from './AggregatorInventory';
|
||||
export * from './StaticInventory';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 Component = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type Inventory = {
|
||||
list: () => Promise<Component[]>;
|
||||
item: (id: string) => Promise<Component | undefined>;
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 { getRootLogger } from '@backstage/backend-common';
|
||||
import { startServer } from './service/server';
|
||||
|
||||
startServer({
|
||||
port: process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 3003,
|
||||
enableCors: process.env.PLUGIN_CORS
|
||||
? Boolean(process.env.PLUGIN_CORS)
|
||||
: false,
|
||||
logger: getRootLogger(),
|
||||
}).catch(err => {
|
||||
getRootLogger().error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
getRootLogger().info('CTRL+C pressed; exiting.');
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 {
|
||||
errorHandler,
|
||||
notFoundHandler,
|
||||
requestLoggingHandler,
|
||||
} from '@backstage/backend-common';
|
||||
import compression from 'compression';
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
|
||||
export interface ApplicationOptions {
|
||||
enableCors: boolean;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export async function createApplication(
|
||||
options: ApplicationOptions,
|
||||
): Promise<express.Application> {
|
||||
const app = express();
|
||||
|
||||
app.use(helmet());
|
||||
if (options.enableCors) {
|
||||
app.use(cors());
|
||||
}
|
||||
app.use(compression());
|
||||
app.use(express.json());
|
||||
app.use(requestLoggingHandler());
|
||||
app.use('/', await createRouter({ logger: options.logger }));
|
||||
app.use(notFoundHandler());
|
||||
app.use(errorHandler());
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { AggregatorInventory, StaticInventory } from '../inventory';
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const logger = options.logger.child({ plugin: 'inventory' });
|
||||
|
||||
const inventory = new AggregatorInventory();
|
||||
inventory.enlist(
|
||||
new StaticInventory([
|
||||
{ id: 'component1' },
|
||||
{ id: 'component2' },
|
||||
{ id: 'component3' },
|
||||
{ id: 'component4' },
|
||||
]),
|
||||
);
|
||||
|
||||
const router = express.Router();
|
||||
router
|
||||
.get('/', async (req, res) => {
|
||||
const components = await inventory.list();
|
||||
res.status(200).send(components);
|
||||
})
|
||||
.get('/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const component = await inventory.item(id);
|
||||
if (component) {
|
||||
res.status(200).send(component);
|
||||
} else {
|
||||
res.status(404).send();
|
||||
}
|
||||
});
|
||||
|
||||
const app = express();
|
||||
app.set('logger', logger);
|
||||
app.use('/', router);
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
import { createApplication } from './application';
|
||||
|
||||
export interface ServerOptions {
|
||||
port: number;
|
||||
enableCors: boolean;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export async function startServer(options: ServerOptions): Promise<Server> {
|
||||
const logger = options.logger.child({ service: 'inventory-backend' });
|
||||
|
||||
logger.debug('Creating application...');
|
||||
const app = await createApplication({
|
||||
enableCors: options.enableCors,
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.debug('Starting application server...');
|
||||
return await new Promise((resolve, reject) => {
|
||||
const server = app.listen(options.port, (err?: Error) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Listening on port ${options.port}`);
|
||||
resolve(server);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -13,3 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"lib": ["es2019"],
|
||||
"types": ["node", "jest"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16680,11 +16680,16 @@ prepend-http@^1.0.0, prepend-http@^1.0.1:
|
||||
resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
||||
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
|
||||
|
||||
prettier@^1.16.4, prettier@^1.18.2, prettier@^1.19.1:
|
||||
prettier@^1.16.4, prettier@^1.18.2:
|
||||
version "1.19.1"
|
||||
resolved "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
||||
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
||||
|
||||
prettier@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4"
|
||||
integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==
|
||||
|
||||
pretty-bytes@5.3.0, pretty-bytes@^5.1.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2"
|
||||
|
||||
Reference in New Issue
Block a user