diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index f544f9594f..35d354fefc 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -4,4 +4,4 @@
# The last matching pattern takes precedence.
# https://help.github.com/articles/about-codeowners/
-* @spotify/backstage-core
+* @spotify/backstage-core
diff --git a/package.json b/package.json
index 03c9b411fe..4f7eb2c5f5 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/packages/app/package.json b/packages/app/package.json
index a7a370b108..39aea525c7 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -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",
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index 2831ff0a72..a8c4cd3382 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -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 }) => (
+
+
diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts
index 06c5084bd0..3669f83dba 100644
--- a/packages/app/src/plugins.ts
+++ b/packages/app/src/plugins.ts
@@ -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';
diff --git a/packages/backend-common/README.md b/packages/backend-common/README.md
index 32bdd3d2a8..82db0454d2 100644
--- a/packages/backend-common/README.md
+++ b/packages/backend-common/README.md
@@ -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}`);
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index 1a2c7b70e6..e9fc15f1c1 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -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();
diff --git a/packages/core/src/components/Sequence/index.ts b/packages/core/src/components/Sequence/index.ts
index 4391c987b5..fe5faa2d86 100644
--- a/packages/core/src/components/Sequence/index.ts
+++ b/packages/core/src/components/Sequence/index.ts
@@ -14,4 +14,5 @@
* limitations under the License.
*/
-export { default, StepType } from './Sequence';
+export { default } from './Sequence';
+export type { StepType } from './Sequence';
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 619e672072..6a43e624ce 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -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';
diff --git a/packages/core/src/layout/BottomLink/index.ts b/packages/core/src/layout/BottomLink/index.ts
index 5d4845064b..6f2055f4f7 100644
--- a/packages/core/src/layout/BottomLink/index.ts
+++ b/packages/core/src/layout/BottomLink/index.ts
@@ -14,4 +14,5 @@
* limitations under the License.
*/
-export { default, Props } from './BottomLink';
+export { default } from './BottomLink';
+export type { Props } from './BottomLink';
diff --git a/packages/core/src/layout/Page/index.ts b/packages/core/src/layout/Page/index.ts
index bb7253bb95..50899fa036 100644
--- a/packages/core/src/layout/Page/index.ts
+++ b/packages/core/src/layout/Page/index.ts
@@ -15,4 +15,5 @@
*/
export { default } from './Page';
-export { gradients, pageTheme, PageTheme } from './PageThemeProvider';
+export { gradients, pageTheme } from './PageThemeProvider';
+export type { PageTheme } from './PageThemeProvider';
diff --git a/plugins/explore/.eslintrc.js b/plugins/explore/.eslintrc.js
new file mode 100644
index 0000000000..13573efa9c
--- /dev/null
+++ b/plugins/explore/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: [require.resolve('@backstage/cli/config/eslint')],
+};
diff --git a/plugins/explore/README.md b/plugins/explore/README.md
new file mode 100644
index 0000000000..12a4e58380
--- /dev/null
+++ b/plugins/explore/README.md
@@ -0,0 +1,6 @@
+# Title
+Welcome to the explore plugin!
+
+## Sub-section 1
+
+## Sub-section 2
diff --git a/plugins/explore/package.json b/plugins/explore/package.json
new file mode 100644
index 0000000000..fb13381321
--- /dev/null
+++ b/plugins/explore/package.json
@@ -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"
+ ]
+}
diff --git a/plugins/explore/src/components/ExploreCard.test.js b/plugins/explore/src/components/ExploreCard.test.js
new file mode 100644
index 0000000000..fde36b529d
--- /dev/null
+++ b/plugins/explore/src/components/ExploreCard.test.js
@@ -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('', () => {
+ it('renders without exploding', () => {
+ const { getByText } = render(
+ wrapInThemedTestApp(),
+ );
+ expect(getByText('Explore')).toBeInTheDocument();
+ });
+
+ it('renders props correctly', () => {
+ const { getByText } = render(
+ wrapInThemedTestApp(),
+ );
+ expect(getByText(minProps.card.title)).toBeInTheDocument();
+ expect(getByText(minProps.card.description)).toBeInTheDocument();
+ });
+
+ it('should link out', () => {
+ const rendered = render(wrapInThemedTestApp());
+ 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(),
+ );
+ 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(),
+ );
+ expect(queryByText('GA')).not.toBeInTheDocument();
+ });
+
+ it('renders tags correctly', () => {
+ const { getByText } = render(
+ wrapInThemedTestApp(),
+ );
+ expect(getByText(minProps.card.tags[0])).toBeInTheDocument();
+ expect(getByText(minProps.card.tags[1])).toBeInTheDocument();
+ });
+});
diff --git a/plugins/explore/src/components/ExploreCard.tsx b/plugins/explore/src/components/ExploreCard.tsx
new file mode 100644
index 0000000000..9c61e9bea8
--- /dev/null
+++ b/plugins/explore/src/components/ExploreCard.tsx
@@ -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(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 = ({ card, objectFit }) => {
+ const classes = useStyles();
+
+ const { title, description, url, image, lifecycle, newsTag, tags } = card;
+
+ return (
+
+
+
+
+ {title}{' '}
+ {lifecycle && lifecycle.toLowerCase() !== 'ga' && (
+
+ )}
+
+
+ {description || 'Description missing'}
+
+ {tags && (
+
+ {tags.map((item, idx) => (
+
+ ))}
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default ExploreCard;
diff --git a/plugins/explore/src/components/ExplorePluginPage.tsx b/plugins/explore/src/components/ExplorePluginPage.tsx
new file mode 100644
index 0000000000..7b5fc81a30
--- /dev/null
+++ b/plugins/explore/src/components/ExplorePluginPage.tsx
@@ -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(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 (
+
+
+
+
+
+ Explore tools available in Backstage
+
+
+
+
+
+ );
+};
+
+export default ExplorePluginPage;
diff --git a/plugins/explore/src/index.ts b/plugins/explore/src/index.ts
new file mode 100644
index 0000000000..3a0a0fe2d3
--- /dev/null
+++ b/plugins/explore/src/index.ts
@@ -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';
diff --git a/plugins/explore/src/plugin.test.ts b/plugins/explore/src/plugin.test.ts
new file mode 100644
index 0000000000..d6503c038b
--- /dev/null
+++ b/plugins/explore/src/plugin.test.ts
@@ -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();
+ });
+});
diff --git a/plugins/inventory-backend/src/plugin.ts b/plugins/explore/src/plugin.ts
similarity index 69%
rename from plugins/inventory-backend/src/plugin.ts
rename to plugins/explore/src/plugin.ts
index 88299a5c0f..66e48a9b15 100644
--- a/plugins/inventory-backend/src/plugin.ts
+++ b/plugins/explore/src/plugin.ts
@@ -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);
+ },
});
diff --git a/plugins/explore/src/setupTests.ts b/plugins/explore/src/setupTests.ts
new file mode 100644
index 0000000000..1a907ab8e6
--- /dev/null
+++ b/plugins/explore/src/setupTests.ts
@@ -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();
diff --git a/plugins/explore/tsconfig.json b/plugins/explore/tsconfig.json
new file mode 100644
index 0000000000..7b73db2f0f
--- /dev/null
+++ b/plugins/explore/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "baseUrl": "src"
+ }
+}
diff --git a/plugins/inventory-backend/package.json b/plugins/inventory-backend/package.json
index 37f3f27e78..4b7fd0ecc7 100644
--- a/plugins/inventory-backend/package.json
+++ b/plugins/inventory-backend/package.json
@@ -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"
+ }
}
diff --git a/plugins/inventory-backend/src/plugin.test.ts b/plugins/inventory-backend/src/index.test.ts
similarity index 100%
rename from plugins/inventory-backend/src/plugin.test.ts
rename to plugins/inventory-backend/src/index.test.ts
diff --git a/plugins/inventory-backend/src/index.ts b/plugins/inventory-backend/src/index.ts
index 92c9090376..7612c392a2 100644
--- a/plugins/inventory-backend/src/index.ts
+++ b/plugins/inventory-backend/src/index.ts
@@ -14,4 +14,4 @@
* limitations under the License.
*/
-export { router } from './plugin';
+export * from './service/router';
diff --git a/plugins/inventory-backend/src/inventory/AggregatorInventory.ts b/plugins/inventory-backend/src/inventory/AggregatorInventory.ts
new file mode 100644
index 0000000000..3f1ca0f5de
--- /dev/null
+++ b/plugins/inventory-backend/src/inventory/AggregatorInventory.ts
@@ -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> {
+ return Promise.all(this.inventories.map(i => i.list())).then(lists =>
+ lists.flat(),
+ );
+ }
+
+ item(id: string): Promise {
+ return this.list().then(items => items.find(i => i.id === id));
+ }
+
+ enlist(inventory: Inventory) {
+ this.inventories.push(inventory);
+ }
+}
diff --git a/plugins/inventory-backend/src/inventory/StaticInventory.ts b/plugins/inventory-backend/src/inventory/StaticInventory.ts
new file mode 100644
index 0000000000..cd4fa2b7d5
--- /dev/null
+++ b/plugins/inventory-backend/src/inventory/StaticInventory.ts
@@ -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> {
+ return Promise.resolve([...this.components]);
+ }
+
+ item(id: string): Promise {
+ return this.list().then(items => items.find(i => i.id === id));
+ }
+}
diff --git a/plugins/inventory-backend/src/inventory/index.ts b/plugins/inventory-backend/src/inventory/index.ts
new file mode 100644
index 0000000000..3affa1109a
--- /dev/null
+++ b/plugins/inventory-backend/src/inventory/index.ts
@@ -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';
diff --git a/plugins/inventory-backend/src/inventory/types.ts b/plugins/inventory-backend/src/inventory/types.ts
new file mode 100644
index 0000000000..3d30b11881
--- /dev/null
+++ b/plugins/inventory-backend/src/inventory/types.ts
@@ -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;
+ item: (id: string) => Promise;
+};
diff --git a/plugins/inventory-backend/src/run.ts b/plugins/inventory-backend/src/run.ts
new file mode 100644
index 0000000000..979827c40f
--- /dev/null
+++ b/plugins/inventory-backend/src/run.ts
@@ -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);
+});
diff --git a/plugins/inventory-backend/src/service/application.ts b/plugins/inventory-backend/src/service/application.ts
new file mode 100644
index 0000000000..e5cf151050
--- /dev/null
+++ b/plugins/inventory-backend/src/service/application.ts
@@ -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 {
+ 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;
+}
diff --git a/plugins/inventory-backend/src/service/router.ts b/plugins/inventory-backend/src/service/router.ts
new file mode 100644
index 0000000000..afa4a543b3
--- /dev/null
+++ b/plugins/inventory-backend/src/service/router.ts
@@ -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 {
+ 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;
+}
diff --git a/plugins/inventory-backend/src/service/server.ts b/plugins/inventory-backend/src/service/server.ts
new file mode 100644
index 0000000000..9715a72fef
--- /dev/null
+++ b/plugins/inventory-backend/src/service/server.ts
@@ -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 {
+ 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);
+ });
+ });
+}
diff --git a/plugins/inventory-backend/src/setupTests.ts b/plugins/inventory-backend/src/setupTests.ts
index f3b69cc361..3fa7cb04b4 100644
--- a/plugins/inventory-backend/src/setupTests.ts
+++ b/plugins/inventory-backend/src/setupTests.ts
@@ -13,3 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+require('jest-fetch-mock').enableMocks();
diff --git a/plugins/inventory-backend/tsconfig.json b/plugins/inventory-backend/tsconfig.json
index b463ac102f..1a3f7ca819 100644
--- a/plugins/inventory-backend/tsconfig.json
+++ b/plugins/inventory-backend/tsconfig.json
@@ -10,6 +10,7 @@
"target": "es5",
"module": "commonjs",
"esModuleInterop": true,
+ "lib": ["es2019"],
"types": ["node", "jest"]
}
}
diff --git a/yarn.lock b/yarn.lock
index 585a0f98ba..e7fa7c2dec 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"