From 92feb76252647f0a5fa1681b11f3fc15d08b55fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 30 Apr 2020 14:36:25 +0200 Subject: [PATCH 1/3] Make the inventory backend standalone runnable And also make a proposed split of how the loggers are handled. Now logs look like ``` { "type": "incomingRequest", "service": "backstage", "timestamp": "2020-04-30T14:29:04.002Z", "message": "...", "level": "info" } ``` for requests, and then if the plugin logs something properly ``` { "type": "plugin", "service": "backstage", "plugin": "inventory", "timestamp": "2020-04-30T14:29:04.002Z", "message": "...", "level": "info" } ``` --- packages/backend-common/README.md | 2 +- packages/backend/src/index.ts | 42 +++++++------ plugins/inventory-backend/package.json | 18 +++++- .../src/{plugin.test.ts => index.test.ts} | 0 plugins/inventory-backend/src/index.ts | 2 +- .../src/inventory/AggregatorInventory.ts | 35 +++++++++++ .../src/inventory/StaticInventory.ts | 29 +++++++++ .../inventory-backend/src/inventory/index.ts | 19 ++++++ .../src/{plugin.ts => inventory/types.ts} | 20 +++--- plugins/inventory-backend/src/run.ts | 34 +++++++++++ .../src/service/application.ts | 51 ++++++++++++++++ .../inventory-backend/src/service/router.ts | 61 +++++++++++++++++++ .../inventory-backend/src/service/server.ts | 48 +++++++++++++++ plugins/inventory-backend/src/setupTests.ts | 2 + plugins/inventory-backend/tsconfig.json | 1 + 15 files changed, 328 insertions(+), 36 deletions(-) rename plugins/inventory-backend/src/{plugin.test.ts => index.test.ts} (100%) create mode 100644 plugins/inventory-backend/src/inventory/AggregatorInventory.ts create mode 100644 plugins/inventory-backend/src/inventory/StaticInventory.ts create mode 100644 plugins/inventory-backend/src/inventory/index.ts rename plugins/inventory-backend/src/{plugin.ts => inventory/types.ts} (69%) create mode 100644 plugins/inventory-backend/src/run.ts create mode 100644 plugins/inventory-backend/src/service/application.ts create mode 100644 plugins/inventory-backend/src/service/router.ts create mode 100644 plugins/inventory-backend/src/service/server.ts 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/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/plugin.ts b/plugins/inventory-backend/src/inventory/types.ts similarity index 69% rename from plugins/inventory-backend/src/plugin.ts rename to plugins/inventory-backend/src/inventory/types.ts index 88299a5c0f..3d30b11881 100644 --- a/plugins/inventory-backend/src/plugin.ts +++ b/plugins/inventory-backend/src/inventory/types.ts @@ -14,17 +14,11 @@ * limitations under the License. */ -import express from 'express'; +export type Component = { + id: string; +}; -export const router = express.Router(); - -router.get('/', async (_, res) => { - res - .status(200) - .send([ - { id: 'component1' }, - { id: 'component2' }, - { id: 'component3' }, - { id: 'component4' }, - ]); -}); +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"] } } From 8a16cf7067faea2f9135195cf77ae0f3419d7cf5 Mon Sep 17 00:00:00 2001 From: Conal Cosgrove Date: Sat, 2 May 2020 20:39:22 +0100 Subject: [PATCH 2/3] [Storybook] Fix Storybook compilation warnings (#701) * separate type import/exports * update prettier to v2.0 * add explicit type import/exports --- package.json | 2 +- packages/core/src/components/Sequence/index.ts | 3 ++- packages/core/src/index.ts | 3 ++- packages/core/src/layout/BottomLink/index.ts | 3 ++- packages/core/src/layout/Page/index.ts | 3 ++- yarn.lock | 7 ++++++- 6 files changed, 15 insertions(+), 6 deletions(-) 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/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/yarn.lock b/yarn.lock index 79eac2cb21..cb02b3d9c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16634,11 +16634,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" From 7472e9a5c26187522dcc1760bdf32f358065d5d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Sun, 3 May 2020 14:30:51 +0200 Subject: [PATCH 3/3] [Plugin] Explore features in Backstage (#696) * Explore plugin * Fixed styles and props * Real conent * Add Lighthouse and fix tests * Add menu item * Cleanup * Change wording * Update package.json * Remove dead code * Update yarn.lock * Revert change to CODEOWNERS * Reset yarn.lock to master * Fix comments --- .github/CODEOWNERS | 2 +- packages/app/package.json | 1 + packages/app/src/components/Root/Root.tsx | 3 + packages/app/src/plugins.ts | 2 +- plugins/explore/.eslintrc.js | 3 + plugins/explore/README.md | 6 + plugins/explore/package.json | 39 +++++ .../src/components/ExploreCard.test.js | 93 ++++++++++++ .../explore/src/components/ExploreCard.tsx | 134 ++++++++++++++++++ .../src/components/ExplorePluginPage.tsx | 93 ++++++++++++ plugins/explore/src/index.ts | 17 +++ plugins/explore/src/plugin.test.ts | 23 +++ plugins/explore/src/plugin.ts | 25 ++++ plugins/explore/src/setupTests.ts | 18 +++ plugins/explore/tsconfig.json | 7 + 15 files changed, 464 insertions(+), 2 deletions(-) create mode 100644 plugins/explore/.eslintrc.js create mode 100644 plugins/explore/README.md create mode 100644 plugins/explore/package.json create mode 100644 plugins/explore/src/components/ExploreCard.test.js create mode 100644 plugins/explore/src/components/ExploreCard.tsx create mode 100644 plugins/explore/src/components/ExplorePluginPage.tsx create mode 100644 plugins/explore/src/index.ts create mode 100644 plugins/explore/src/plugin.test.ts create mode 100644 plugins/explore/src/plugin.ts create mode 100644 plugins/explore/src/setupTests.ts create mode 100644 plugins/explore/tsconfig.json 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/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/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 + + +
+ {toolsCards.map((card: CardData, ix: any) => ( + + ))} +
+
+ + ); +}; + +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/explore/src/plugin.ts b/plugins/explore/src/plugin.ts new file mode 100644 index 0000000000..66e48a9b15 --- /dev/null +++ b/plugins/explore/src/plugin.ts @@ -0,0 +1,25 @@ +/* + * 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 { createPlugin } from '@backstage/core'; +import ExplorePluginPage from './components/ExplorePluginPage'; + +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" + } +}