From 9b6b51931419766a3fb53d66a0f2145d1b3b96e4 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Fri, 3 Jul 2020 09:44:31 +0200 Subject: [PATCH 01/13] Update migrations to support postgres --- .../src/identity/DatabaseKeyStore.ts | 1 + ...7114117_location_update_log_latest_view.js | 27 +- .../migrations/20200702153613_entities.js | 240 ++++++++++++++++++ .../src/database/DatabaseManager.ts | 2 + 4 files changed, 258 insertions(+), 12 deletions(-) create mode 100644 plugins/catalog-backend/migrations/20200702153613_entities.js diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts index c175cd6e12..a9fc29b397 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts @@ -42,6 +42,7 @@ export class DatabaseKeyStore implements KeyStore { await database.migrate.latest({ directory: migrationsDir, + tableName: 'knex_migrations_auth', }); return new DatabaseKeyStore(options); diff --git a/plugins/catalog-backend/migrations/20200527114117_location_update_log_latest_view.js b/plugins/catalog-backend/migrations/20200527114117_location_update_log_latest_view.js index e51be30b81..083d0a38aa 100644 --- a/plugins/catalog-backend/migrations/20200527114117_location_update_log_latest_view.js +++ b/plugins/catalog-backend/migrations/20200527114117_location_update_log_latest_view.js @@ -20,18 +20,21 @@ * @param {import('knex')} knex */ exports.up = async function up(knex) { - // Need to first order by date of creation - const query = knex - .select() - .from('location_update_log') - .orderBy('location_update_log.created_at', 'desc'); - - // And only then to do the grouping to get the latest per location - const groupedQuery = knex(query).groupBy('location_id').select(); - - await knex.schema.raw( - `CREATE VIEW location_update_log_latest AS ${groupedQuery.toString()};`, - ); + // Get list sorted by created_at timestamp in descending order + // Grouped by location_id + return knex.schema.raw(` + CREATE VIEW location_update_log_latest AS + SELECT t1.* FROM location_update_log t1 + JOIN + ( + SELECT location_id, MAX(created_at) AS MAXDATE + FROM location_update_log + GROUP BY location_id + ) t2 + ON t1.location_id = t2.location_id + AND t1.created_at = t2.MAXDATE + ORDER BY created_at DESC; + `); }; /** diff --git a/plugins/catalog-backend/migrations/20200702153613_entities.js b/plugins/catalog-backend/migrations/20200702153613_entities.js new file mode 100644 index 0000000000..c97331796d --- /dev/null +++ b/plugins/catalog-backend/migrations/20200702153613_entities.js @@ -0,0 +1,240 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @param {import('knex')} knex + */ +exports.up = async function up(knex) { + // Drop constraints (Postgres) + try { + await knex.schema.alterTable('entities_search', table => { + table.dropForeign(['entity_id']); + }); + await knex.schema.alterTable('entities', table => { + table.dropPrimary('entities_pkey'); + }); + } catch (e) { + // SQLite does not support FK and PK, carry on + } + await knex.schema.alterTable('entities', table => { + table.dropUnique([], 'entities_unique_name'); + }); + // Setup temporary tables + await knex.schema.renameTable('entities_search', 'tmp_entities_search'); + await knex.schema.renameTable('entities', 'tmp_entities'); + + // + // entities + // + await knex.schema + .createTable('entities', table => { + table.comment('All entities currently stored in the catalog'); + table.uuid('id').primary().comment('Auto-generated ID of the entity'); + table + .uuid('location_id') + .references('id') + .inTable('locations') + .nullable() + .comment('The location that originated the entity'); + table + .string('etag') + .notNullable() + .comment( + 'An opaque string that changes for each update operation to any part of the entity, including metadata.', + ); + table + .string('generation') + .notNullable() + .unsigned() + .comment( + 'A positive nonzero number that indicates the current generation of data for this entity; the value is incremented each time the spec changes.', + ); + table + .string('api_version') + .notNullable() + .comment('The apiVersion field of the entity'); + table + .string('kind') + .notNullable() + .comment('The kind field of the entity'); + table + .string('name') + .nullable() + .comment('The metadata.name field of the entity'); + table + .string('namespace') + .nullable() + .comment('The metadata.namespace field of the entity'); + table + .text('metadata') + .notNullable() + .comment('The entire metadata JSON blob of the entity'); + table + .text('spec') + .nullable() + .comment('The entire spec JSON blob of the entity'); + }) + .alterTable('entities', table => { + // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta + table.unique(['kind', 'name', 'namespace'], 'entities_unique_name'); + }); + + await knex.schema.raw(`INSERT INTO entities SELECT * FROM tmp_entities`); + + // + // entities_search + // + await knex.schema.createTable('entities_search', table => { + table.comment( + 'Flattened key-values from the entities, used for quick filtering', + ); + table + .uuid('entity_id') + .references('id') + .inTable('entities') + .onDelete('CASCADE') + .comment('The entity that matches this key/value'); + table + .string('key') + .notNullable() + .comment('A key that occurs in the entity'); + table + .string('value') + .nullable() + .comment('The corresponding value to match on'); + }); + await knex.schema.raw( + `INSERT INTO entities_search SELECT * FROM tmp_entities_search`, + ); + + // Clean up + await knex.schema.dropTable('tmp_entities'); + return knex.schema.dropTable('tmp_entities_search'); +}; + +/** + * @param {import('knex')} knex + */ +exports.down = async function down(knex) { + // Drop constraints (Postgres) + try { + await knex.schema.alterTable('entities_search', table => { + table.dropForeign(['entity_id']); + }); + await knex.schema.alterTable('entities', table => { + table.dropPrimary('entities_pkey'); + }); + } catch (e) { + // SQLite does not support FK and PK, carry on + } + await knex.schema.alterTable('entities', table => { + table.dropUnique([], 'entities_unique_name'); + }); + + // Setup temporary tables + await knex.schema.renameTable('entities_search', 'tmp_entities_search'); + await knex.schema.renameTable('entities', 'tmp_entities'); + + // + // entities + // + await knex.schema + .createTable('entities', table => { + table.comment('All entities currently stored in the catalog'); + table.uuid('id').primary().comment('Auto-generated ID of the entity'); + table + .uuid('location_id') + .references('id') + .inTable('locations') + .nullable() + .comment('The location that originated the entity'); + table + .string('etag') + .notNullable() + .comment( + 'An opaque string that changes for each update operation to any part of the entity, including metadata.', + ); + table + .string('generation') + .notNullable() + .unsigned() + .comment( + 'A positive nonzero number that indicates the current generation of data for this entity; the value is incremented each time the spec changes.', + ); + table + .string('api_version') + .notNullable() + .comment('The apiVersion field of the entity'); + table + .string('kind') + .notNullable() + .comment('The kind field of the entity'); + table + .string('name') + .nullable() + .comment('The metadata.name field of the entity'); + table + .string('namespace') + .nullable() + .comment('The metadata.namespace field of the entity'); + table + .string('metadata') + .notNullable() + .comment('The entire metadata JSON blob of the entity'); + table + .string('spec') + .nullable() + .comment('The entire spec JSON blob of the entity'); + }) + .alterTable('entities', table => { + // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta + table.unique(['kind', 'name', 'namespace'], 'entities_unique_name'); + }); + + await knex.schema.raw(`INSERT INTO entities SELECT * FROM tmp_entities`); + + // + // entities_search + // + await knex.schema.createTable('entities_search', table => { + table.comment( + 'Flattened key-values from the entities, used for quick filtering', + ); + table + .uuid('entity_id') + .references('id') + .inTable('entities') + .onDelete('CASCADE') + .comment('The entity that matches this key/value'); + table + .string('key') + .notNullable() + .comment('A key that occurs in the entity'); + table + .string('value') + .nullable() + .comment('The corresponding value to match on'); + }); + await knex.schema.raw( + `INSERT INTO entities_search SELECT * FROM tmp_entities_search`, + ); + + // Clean up + await knex.schema.dropTable('tmp_entities'); + return knex.schema.dropTable('tmp_entities_search'); +}; diff --git a/plugins/catalog-backend/src/database/DatabaseManager.ts b/plugins/catalog-backend/src/database/DatabaseManager.ts index 28751f4830..02a72f8f6e 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.ts @@ -44,6 +44,7 @@ export class DatabaseManager { ): Promise { await knex.migrate.latest({ directory: migrationsDir, + tableName: 'knex_migrations_catalog', }); const { logger, fieldNormalizer } = { ...defaultOptions, ...options }; return new CommonDatabase(knex, fieldNormalizer, logger); @@ -74,6 +75,7 @@ export class DatabaseManager { }); await knex.migrate.latest({ directory: migrationsDir, + tableName: 'knex_migrations_catalog', }); const { logger, fieldNormalizer } = defaultOptions; return new CommonDatabase(knex, fieldNormalizer, logger); From 597f9b8a70f3b9aaec77e7fc98521b5cd6fef543 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Wed, 8 Jul 2020 11:55:47 +0200 Subject: [PATCH 02/13] remove tableName-key from migrate.latest --- plugins/auth-backend/src/identity/DatabaseKeyStore.ts | 1 - plugins/catalog-backend/src/database/DatabaseManager.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts index a9fc29b397..c175cd6e12 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts @@ -42,7 +42,6 @@ export class DatabaseKeyStore implements KeyStore { await database.migrate.latest({ directory: migrationsDir, - tableName: 'knex_migrations_auth', }); return new DatabaseKeyStore(options); diff --git a/plugins/catalog-backend/src/database/DatabaseManager.ts b/plugins/catalog-backend/src/database/DatabaseManager.ts index 02a72f8f6e..28751f4830 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.ts @@ -44,7 +44,6 @@ export class DatabaseManager { ): Promise { await knex.migrate.latest({ directory: migrationsDir, - tableName: 'knex_migrations_catalog', }); const { logger, fieldNormalizer } = { ...defaultOptions, ...options }; return new CommonDatabase(knex, fieldNormalizer, logger); @@ -75,7 +74,6 @@ export class DatabaseManager { }); await knex.migrate.latest({ directory: migrationsDir, - tableName: 'knex_migrations_catalog', }); const { logger, fieldNormalizer } = defaultOptions; return new CommonDatabase(knex, fieldNormalizer, logger); From 904941125e4318a08bbef738368a3c181472f5fd Mon Sep 17 00:00:00 2001 From: Bilawal Hameed Date: Wed, 8 Jul 2020 15:46:46 +0200 Subject: [PATCH 03/13] feat(cli): introduce backstage-cli plugin:export --- packages/cli/src/commands/plugin/export.ts | 30 ++++++++++++++++++++++ packages/cli/src/index.ts | 6 +++++ 2 files changed, 36 insertions(+) create mode 100644 packages/cli/src/commands/plugin/export.ts diff --git a/packages/cli/src/commands/plugin/export.ts b/packages/cli/src/commands/plugin/export.ts new file mode 100644 index 0000000000..b6db0d8ddd --- /dev/null +++ b/packages/cli/src/commands/plugin/export.ts @@ -0,0 +1,30 @@ +/* + * 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 { Command } from 'commander'; +import { loadConfig } from '@backstage/config-loader'; +import { ConfigReader } from '@backstage/config'; +import { buildBundle } from '../../lib/bundler'; + +export default async (cmd: Command) => { + const appConfigs = await loadConfig(); + await buildBundle({ + entry: 'dev/index', + statsJsonEnabled: cmd.stats, + config: ConfigReader.fromConfigs(appConfigs), + appConfigs, + }); +}; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 9a102ab7d3..7521549d96 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -103,6 +103,12 @@ const main = (argv: string[]) => { .option('--check', 'Enable type checking and linting') .action(lazyAction(() => import('./commands/plugin/serve'), 'default')); + program + .command('plugin:export') + .description('Exports the dev/ folder of a plugin') + .option('--stats', 'Write bundle stats to output directory') + .action(lazyAction(() => import('./commands/plugin/export'), 'default')); + program .command('plugin:diff') .option('--check', 'Fail if changes are required') From 617d1b94465a6e152e9e190a12967b919198e4e7 Mon Sep 17 00:00:00 2001 From: Bilawal Hameed Date: Wed, 8 Jul 2020 16:22:33 +0200 Subject: [PATCH 04/13] fix: re-ran yarn install --- yarn.lock | 7 ------- 1 file changed, 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8f5d9e8cec..0641f5ed77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3960,13 +3960,6 @@ "@types/node" "*" rollup "^0.63.4" -"@types/sanitize-html@^1.23.3": - version "1.23.3" - resolved "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-1.23.3.tgz#26527783aba3bf195ad8a3c3e51bd3713526fc0d" - integrity sha512-Isg8N0ifKdDq6/kaNlIcWfapDXxxquMSk2XC5THsOICRyOIhQGds95XH75/PL/g9mExi4bL8otIqJM/Wo96WxA== - dependencies: - htmlparser2 "^4.1.0" - "@types/serve-static@*": version "1.13.3" resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" From c639909e78827eef6d1df10e9b5aa31a2eb15cee Mon Sep 17 00:00:00 2001 From: Bilawal Hameed Date: Wed, 8 Jul 2020 16:39:46 +0200 Subject: [PATCH 05/13] fix: add yarn export to techdocs plugin --- plugins/techdocs/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index d8ee613407..98fbac1492 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -12,6 +12,7 @@ }, "scripts": { "build": "backstage-cli plugin:build", + "export": "backstage-cli plugin:export", "start": "backstage-cli plugin:serve", "lint": "backstage-cli lint", "test": "backstage-cli test", From 82f7f02cd809954cad6d257e8f28de5749c6c857 Mon Sep 17 00:00:00 2001 From: "toban@dfds.com" Date: Wed, 8 Jul 2020 23:19:38 +0200 Subject: [PATCH 06/13] =fixing-windows-mock-data --- plugins/catalog-backend/package.json | 2 +- plugins/catalog-backend/scripts/{mock-data => mock-data.sh} | 0 plugins/scaffolder-backend/package.json | 2 +- plugins/scaffolder-backend/scripts/{mock-data => mock-data.sh} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename plugins/catalog-backend/scripts/{mock-data => mock-data.sh} (100%) mode change 100755 => 100644 rename plugins/scaffolder-backend/scripts/{mock-data => mock-data.sh} (100%) mode change 100755 => 100644 diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 631109db10..48d0aa2ef1 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -18,7 +18,7 @@ "prepack": "backstage-cli prepack", "postpack": "backstage-cli postpack", "clean": "backstage-cli clean", - "mock-data": "./scripts/mock-data" + "mock-data": "./scripts/mock-data.sh" }, "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.13", diff --git a/plugins/catalog-backend/scripts/mock-data b/plugins/catalog-backend/scripts/mock-data.sh old mode 100755 new mode 100644 similarity index 100% rename from plugins/catalog-backend/scripts/mock-data rename to plugins/catalog-backend/scripts/mock-data.sh diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 24cb88d980..df4f64060f 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -18,7 +18,7 @@ "prepack": "backstage-cli prepack", "postpack": "backstage-cli postpack", "clean": "backstage-cli clean", - "mock-data": "./scripts/mock-data" + "mock-data": "./scripts/mock-data.sh" }, "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.13", diff --git a/plugins/scaffolder-backend/scripts/mock-data b/plugins/scaffolder-backend/scripts/mock-data.sh old mode 100755 new mode 100644 similarity index 100% rename from plugins/scaffolder-backend/scripts/mock-data rename to plugins/scaffolder-backend/scripts/mock-data.sh From 8502e7f01f9ffa3e07c7dff239154a0af856eef4 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 9 Jul 2020 00:58:01 +0200 Subject: [PATCH 07/13] chore: add github action to run dockerfile build for Cookiecutter --- .github/workflows/scaffolder.yml | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/scaffolder.yml diff --git a/.github/workflows/scaffolder.yml b/.github/workflows/scaffolder.yml new file mode 100644 index 0000000000..41786e2f27 --- /dev/null +++ b/.github/workflows/scaffolder.yml @@ -0,0 +1,33 @@ +name: TechDocs + +on: + pull_request: + paths: + - '.github/workflows/scaffolder.yml' + - './plugins/scaffolder-backend/scripts' + push: + branches: [master] +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.7] + + name: Build Container + steps: + - uses: actions/checkout@v2 + # Build Docker Image + - name: Build and push Docker images + uses: docker/build-push-action@v1.1.0 + with: + path: plugins/scaffolder-backend/scripts + dockerfile: Cookiecutter.dockerfile + registry: docker.pkg.github.com + repository: ${{ github.repository }}/cookiecutter + username: ${{ github.actor }} + password: ${{ github.token }} + tag_with_ref: true + push: true From f9a51d0a2845e30c785d0f4fed80bfb4d85836ec Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 9 Jul 2020 00:59:53 +0200 Subject: [PATCH 08/13] chore: updating name of build script --- .github/workflows/scaffolder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scaffolder.yml b/.github/workflows/scaffolder.yml index 41786e2f27..27213fb706 100644 --- a/.github/workflows/scaffolder.yml +++ b/.github/workflows/scaffolder.yml @@ -1,4 +1,4 @@ -name: TechDocs +name: Scaffolder on: pull_request: From 69dae11ea73ef9b8d3b90adf536b3a172aa62055 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 9 Jul 2020 01:02:34 +0200 Subject: [PATCH 09/13] chore: fixing relative path --- .github/workflows/scaffolder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scaffolder.yml b/.github/workflows/scaffolder.yml index 27213fb706..e6fad58b28 100644 --- a/.github/workflows/scaffolder.yml +++ b/.github/workflows/scaffolder.yml @@ -24,7 +24,7 @@ jobs: uses: docker/build-push-action@v1.1.0 with: path: plugins/scaffolder-backend/scripts - dockerfile: Cookiecutter.dockerfile + dockerfile: ./Cookiecutter.dockerfile registry: docker.pkg.github.com repository: ${{ github.repository }}/cookiecutter username: ${{ github.actor }} From 423b52fe9624db82a301d92a57dc0fbc9cc44265 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 9 Jul 2020 01:04:16 +0200 Subject: [PATCH 10/13] chore: fixing path for dockerifle again --- .github/workflows/scaffolder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scaffolder.yml b/.github/workflows/scaffolder.yml index e6fad58b28..bbfd52009d 100644 --- a/.github/workflows/scaffolder.yml +++ b/.github/workflows/scaffolder.yml @@ -24,7 +24,7 @@ jobs: uses: docker/build-push-action@v1.1.0 with: path: plugins/scaffolder-backend/scripts - dockerfile: ./Cookiecutter.dockerfile + dockerfile: plugins/scaffolder-backend/scripts/Cookiecutter.dockerfile registry: docker.pkg.github.com repository: ${{ github.repository }}/cookiecutter username: ${{ github.actor }} From 8885ebee89271b82534b0fa57c1fb18394cfe48c Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Wed, 8 Jul 2020 21:43:53 -0400 Subject: [PATCH 11/13] adding rollbar plugin & backend This is the first commit of the rollbar plugin. It is fully functional but will require some additional features to make it more usable and feature complete. It currently includes a backend wraps the rollbar REST API and provides data for the frontend plugin. --- packages/app/package.json | 1 + packages/app/src/apis.ts | 10 + packages/app/src/plugins.ts | 1 + packages/backend/package.json | 1 + packages/backend/src/index.ts | 7 + packages/backend/src/plugins/rollbar.ts | 22 +++ plugins/rollbar-backend/.eslintrc.js | 3 + plugins/rollbar-backend/README.md | 12 ++ plugins/rollbar-backend/package.json | 47 +++++ .../src/api/RollbarApi.test.ts | 29 +++ plugins/rollbar-backend/src/api/RollbarApi.ts | 183 ++++++++++++++++++ plugins/rollbar-backend/src/api/index.ts | 17 ++ plugins/rollbar-backend/src/api/types.ts | 135 +++++++++++++ plugins/rollbar-backend/src/index.ts | 18 ++ plugins/rollbar-backend/src/run.ts | 33 ++++ .../src/service/router.test.ts | 122 ++++++++++++ plugins/rollbar-backend/src/service/router.ts | 135 +++++++++++++ .../src/service/standaloneServer.ts | 44 +++++ plugins/rollbar-backend/src/setupTests.ts | 19 ++ plugins/rollbar-backend/src/util/index.ts | 25 +++ plugins/rollbar/.eslintrc.js | 3 + plugins/rollbar/README.md | 56 ++++++ plugins/rollbar/dev/index.tsx | 20 ++ plugins/rollbar/package.json | 53 +++++ plugins/rollbar/src/api/RollbarApi.ts | 37 ++++ plugins/rollbar/src/api/RollbarClient.ts | 73 +++++++ plugins/rollbar/src/api/RollbarMockClient.ts | 70 +++++++ plugins/rollbar/src/api/types.ts | 135 +++++++++++++ .../RollbarLayout/RollbarLayout.tsx | 52 +++++ .../RollbarPage/RollbarPage.test.tsx | 47 +++++ .../components/RollbarPage/RollbarPage.tsx | 33 ++++ .../RollbarPage/RollbarProjectTable.tsx | 72 +++++++ .../RollbarProjectPage.test.tsx | 60 ++++++ .../RollbarProjectPage/RollbarProjectPage.tsx | 43 ++++ .../RollbarTopItemsTable.test.tsx | 57 ++++++ .../RollbarTopItemsTable.tsx | 102 ++++++++++ .../RollbarTrendGraph.test.tsx | 32 +++ .../RollbarTrendGraph/RollbarTrendGraph.tsx | 30 +++ plugins/rollbar/src/index.ts | 20 ++ plugins/rollbar/src/plugin.test.ts | 23 +++ plugins/rollbar/src/plugin.ts | 28 +++ plugins/rollbar/src/routes.ts | 27 +++ plugins/rollbar/src/setupTests.ts | 19 ++ yarn.lock | 19 ++ 44 files changed, 1975 insertions(+) create mode 100644 packages/backend/src/plugins/rollbar.ts create mode 100644 plugins/rollbar-backend/.eslintrc.js create mode 100644 plugins/rollbar-backend/README.md create mode 100644 plugins/rollbar-backend/package.json create mode 100644 plugins/rollbar-backend/src/api/RollbarApi.test.ts create mode 100644 plugins/rollbar-backend/src/api/RollbarApi.ts create mode 100644 plugins/rollbar-backend/src/api/index.ts create mode 100644 plugins/rollbar-backend/src/api/types.ts create mode 100644 plugins/rollbar-backend/src/index.ts create mode 100644 plugins/rollbar-backend/src/run.ts create mode 100644 plugins/rollbar-backend/src/service/router.test.ts create mode 100644 plugins/rollbar-backend/src/service/router.ts create mode 100644 plugins/rollbar-backend/src/service/standaloneServer.ts create mode 100644 plugins/rollbar-backend/src/setupTests.ts create mode 100644 plugins/rollbar-backend/src/util/index.ts create mode 100644 plugins/rollbar/.eslintrc.js create mode 100644 plugins/rollbar/README.md create mode 100644 plugins/rollbar/dev/index.tsx create mode 100644 plugins/rollbar/package.json create mode 100644 plugins/rollbar/src/api/RollbarApi.ts create mode 100644 plugins/rollbar/src/api/RollbarClient.ts create mode 100644 plugins/rollbar/src/api/RollbarMockClient.ts create mode 100644 plugins/rollbar/src/api/types.ts create mode 100644 plugins/rollbar/src/components/RollbarLayout/RollbarLayout.tsx create mode 100644 plugins/rollbar/src/components/RollbarPage/RollbarPage.test.tsx create mode 100644 plugins/rollbar/src/components/RollbarPage/RollbarPage.tsx create mode 100644 plugins/rollbar/src/components/RollbarPage/RollbarProjectTable.tsx create mode 100644 plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx create mode 100644 plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx create mode 100644 plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx create mode 100644 plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx create mode 100644 plugins/rollbar/src/components/RollbarTrendGraph/RollbarTrendGraph.test.tsx create mode 100644 plugins/rollbar/src/components/RollbarTrendGraph/RollbarTrendGraph.tsx create mode 100644 plugins/rollbar/src/index.ts create mode 100644 plugins/rollbar/src/plugin.test.ts create mode 100644 plugins/rollbar/src/plugin.ts create mode 100644 plugins/rollbar/src/routes.ts create mode 100644 plugins/rollbar/src/setupTests.ts diff --git a/packages/app/package.json b/packages/app/package.json index 35e78a8793..ed256e6c2d 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -13,6 +13,7 @@ "@backstage/plugin-graphiql": "^0.1.1-alpha.13", "@backstage/plugin-lighthouse": "^0.1.1-alpha.13", "@backstage/plugin-register-component": "^0.1.1-alpha.13", + "@backstage/plugin-rollbar": "^0.1.1-alpha.13", "@backstage/plugin-scaffolder": "^0.1.1-alpha.13", "@backstage/plugin-sentry": "^0.1.1-alpha.13", "@backstage/plugin-tech-radar": "^0.1.1-alpha.13", diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index bc7170c93a..abc490b658 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -57,6 +57,8 @@ import { } from '@backstage/plugin-graphiql'; import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder'; +import { rollbarApiRef, RollbarClient } from '@backstage/plugin-rollbar'; + export const apis = (config: ConfigApi) => { // eslint-disable-next-line no-console console.log(`Creating APIs for ${config.getString('app.title')}`); @@ -170,5 +172,13 @@ export const apis = (config: ConfigApi) => { ]), ); + builder.add( + rollbarApiRef, + new RollbarClient({ + apiOrigin: backendUrl, + basePath: '/rollbar', + }), + ); + return builder.build(); }; diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index a5461c1bea..46c8f23ef0 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -26,3 +26,4 @@ export { plugin as GitopsProfiles } from '@backstage/plugin-gitops-profiles'; export { plugin as TechDocs } from '@backstage/plugin-techdocs'; export { plugin as GraphiQL } from '@backstage/plugin-graphiql'; export { plugin as GithubActions } from '@backstage/plugin-github-actions'; +export { plugin as Rollbar } from '@backstage/plugin-rollbar'; diff --git a/packages/backend/package.json b/packages/backend/package.json index 2e57282a07..2e9cd1ff49 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -25,6 +25,7 @@ "@backstage/plugin-auth-backend": "^0.1.1-alpha.13", "@backstage/plugin-catalog-backend": "^0.1.1-alpha.13", "@backstage/plugin-identity-backend": "^0.1.1-alpha.13", + "@backstage/plugin-rollbar-backend": "^0.1.1-alpha.13", "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.13", "@backstage/plugin-sentry-backend": "^0.1.1-alpha.13", "@backstage/plugin-techdocs-backend": "^0.1.1-alpha.13", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 6841758be8..5562ffa242 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -33,6 +33,7 @@ import knex from 'knex'; import auth from './plugins/auth'; import catalog from './plugins/catalog'; import identity from './plugins/identity'; +import rollbar from './plugins/rollbar'; import scaffolder from './plugins/scaffolder'; import sentry from './plugins/sentry'; import techdocs from './plugins/techdocs'; @@ -69,6 +70,12 @@ async function main() { const service = createServiceBuilder(module) .loadConfig(configReader) .addRouter('/catalog', await catalog(catalogEnv)) + .addRouter( + '/rollbar', + await rollbar( + getRootLogger().child({ type: 'plugin', plugin: 'rollbar' }), + ), + ) .addRouter('/scaffolder', await scaffolder(scaffolderEnv)) .addRouter( '/sentry', diff --git a/packages/backend/src/plugins/rollbar.ts b/packages/backend/src/plugins/rollbar.ts new file mode 100644 index 0000000000..4c8ae9bfeb --- /dev/null +++ b/packages/backend/src/plugins/rollbar.ts @@ -0,0 +1,22 @@ +/* + * 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 { createRouter } from '@backstage/plugin-rollbar-backend'; +import { Logger } from 'winston'; + +export default async function createPlugin(logger: Logger) { + return await createRouter({ logger }); +} diff --git a/plugins/rollbar-backend/.eslintrc.js b/plugins/rollbar-backend/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/rollbar-backend/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/rollbar-backend/README.md b/plugins/rollbar-backend/README.md new file mode 100644 index 0000000000..39a3d93526 --- /dev/null +++ b/plugins/rollbar-backend/README.md @@ -0,0 +1,12 @@ +# Rollbar Backend + +Simple plugin that proxies requests to the [Rollbar](https://rollbar.com) API. + +## Setup + +A `ROLLBAR_TOKEN` environment variable must be set to a read access account token. + +## Links + +- (Frontend part of the plugin)[https://github.com/spotify/backstage/tree/master/plugins/rollbar] +- (The Backstage homepage)[https://backstage.io] diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json new file mode 100644 index 0000000000..5033bdbdea --- /dev/null +++ b/plugins/rollbar-backend/package.json @@ -0,0 +1,47 @@ +{ + "name": "@backstage/plugin-rollbar-backend", + "version": "0.1.1-alpha.13", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.1.1-alpha.13", + "@types/express": "^4.17.6", + "axios": "^0.19.2", + "camelcase-keys": "^6.2.2", + "compression": "^1.7.4", + "cors": "^2.8.5", + "express": "^4.17.1", + "express-promise-router": "^3.0.3", + "fs-extra": "^9.0.0", + "helmet": "^3.22.0", + "lodash": "^4.17.15", + "morgan": "^1.10.0", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.13", + "@types/supertest": "^2.0.8", + "jest-fetch-mock": "^3.0.3", + "supertest": "^4.0.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/rollbar-backend/src/api/RollbarApi.test.ts b/plugins/rollbar-backend/src/api/RollbarApi.test.ts new file mode 100644 index 0000000000..b79d8d1ec7 --- /dev/null +++ b/plugins/rollbar-backend/src/api/RollbarApi.test.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 { getRequestHeaders } from './RollbarApi'; + +describe('RollbarApi', () => { + describe('getRequestHeaders', () => { + it('should generate headers based on token passed in constructor', () => { + expect(getRequestHeaders('testtoken')).toEqual({ + headers: { + 'X-Rollbar-Access-Token': `testtoken`, + }, + }); + }); + }); +}); diff --git a/plugins/rollbar-backend/src/api/RollbarApi.ts b/plugins/rollbar-backend/src/api/RollbarApi.ts new file mode 100644 index 0000000000..acf0583cf0 --- /dev/null +++ b/plugins/rollbar-backend/src/api/RollbarApi.ts @@ -0,0 +1,183 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import axios from 'axios'; +import { Logger } from 'winston'; +import camelcaseKeys from 'camelcase-keys'; +import { buildQuery } from '../util'; +import { + RollbarItemCount, + RollbarItemsResponse, + RollbarProject, + RollbarProjectAccessToken, + RollbarTopActiveItem, +} from './types'; + +const baseUrl = 'https://api.rollbar.com/api/1'; + +const buildUrl = (url: string) => `${baseUrl}${url}`; + +export class RollbarApi { + private projectMap: ProjectMetadataMap | undefined; + + constructor( + private readonly accessToken: string, + private readonly logger: Logger, + ) {} + + async getAllProjects() { + return this.get('/projects').then(projects => + projects.filter(p => p.name), + ); + } + + async getProject(projectName: string) { + return this.getForProject( + `/project/:projectId`, + projectName, + false, + ); + } + + async getProjectItems(projectName: string) { + return this.getForProject( + `/items`, + projectName, + true, + ); + } + + async getTopActiveItems( + projectName: string, + options: { hours: number; environment: string } = { + hours: 24, + environment: 'production', + }, + ) { + return this.getForProject( + `/reports/top_active_items?${buildQuery(options)}`, + projectName, + ); + } + + async getOccuranceCounts( + projectName: string, + options: { environment: string; item_id?: number } = { + environment: 'production', + }, + ) { + return this.getForProject( + `/reports/occurrence_counts?${buildQuery(options as any)}`, + projectName, + ); + } + + async getActivatedCounts( + projectName: string, + options: { environment: string; item_id?: number } = { + environment: 'production', + }, + ) { + return this.getForProject( + `/reports/activated_counts?${buildQuery(options as any)}`, + projectName, + ); + } + + private async getProjectAccessTokens(projectId: number) { + return this.get( + `/project/${projectId}/access_tokens`, + ); + } + + private async get(url: string, accessToken?: string) { + const fullUrl = buildUrl(url); + + if (this.logger) { + this.logger.info(`Calling Rollbar REST API, ${fullUrl}`); + } + + return axios + .get(fullUrl, getRequestHeaders(accessToken || this.accessToken || '')) + .then(response => + camelcaseKeys(response?.data?.result, { deep: true }), + ); + } + + private async getForProject( + url: string, + projectName: string, + useProjectToken = true, + ) { + const project = await this.getProjectMetadata(projectName); + const resolvedUrl = url.replace(':projectId', project.id.toString()); + return this.get(resolvedUrl, useProjectToken ? project.accessToken : ''); + } + + private async getProjectMetadata(name: string) { + const projectMap = await this.getProjectMap(); + const project = projectMap[name]; + + if (!project) { + throw Error(`Invalid project: '${name}'`); + } + + if (!project.accessToken) { + const tokens = await this.getProjectAccessTokens(project.id); + const token = tokens.find(t => t.scopes.includes('read')); + project.accessToken = token ? token.accessToken : undefined; + } + + if (!project.accessToken) { + throw Error(`Could not find project read access token for '${name}'`); + } + + return project; + } + + private async getProjectMap() { + if (this.projectMap) { + return this.projectMap; + } + + const projects = await this.getAllProjects(); + + this.projectMap = projects.reduce((accum: ProjectMetadataMap, i) => { + accum[i.name] = { id: i.id, name: i.name }; + return accum; + }, {}); + + return this.projectMap; + } +} + +export function getRequestHeaders(token: string) { + return { + headers: { + 'X-Rollbar-Access-Token': `${token}`, + }, + }; +} + +type ProjectMetadata = { + name: string; + id: number; + accessToken?: string | undefined; +}; + +interface ProjectMetadataMap { + [name: string]: ProjectMetadata; +} diff --git a/plugins/rollbar-backend/src/api/index.ts b/plugins/rollbar-backend/src/api/index.ts new file mode 100644 index 0000000000..706fa92b1c --- /dev/null +++ b/plugins/rollbar-backend/src/api/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 { RollbarApi, getRequestHeaders } from './RollbarApi'; diff --git a/plugins/rollbar-backend/src/api/types.ts b/plugins/rollbar-backend/src/api/types.ts new file mode 100644 index 0000000000..8a673593d3 --- /dev/null +++ b/plugins/rollbar-backend/src/api/types.ts @@ -0,0 +1,135 @@ +/* + * 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. + */ + +// TODO: Make this re-usable with backend + +export type RollbarProjectAccessTokenScope = 'read' | 'write'; +export type RollbarEnvironment = 'production' | string; + +export enum RollbarLevel { + debug = 10, + info = 20, + warning = 30, + error = 40, + critical = 50, +} + +export enum RollbarFrameworkId { + 'unknown' = 0, + 'rails' = 1, + 'django' = 2, + 'pyramid' = 3, + 'node-js' = 4, + 'pylons' = 5, + 'php' = 6, + 'browser-js' = 7, + 'rollbar-system' = 8, + 'android' = 9, + 'ios' = 10, + 'mailgun' = 11, + 'logentries' = 12, + 'python' = 13, + 'ruby' = 14, + 'sidekiq' = 15, + 'flask' = 16, + 'celery' = 17, + 'rq' = 18, +} + +export enum RollbarPlatformId { + 'unknown' = 0, + 'browser' = 1, + 'flash' = 2, + 'android' = 3, + 'ios' = 4, + 'heroku' = 5, + 'google-app-engine' = 6, + 'client' = 7, +} + +export type RollbarProject = { + id: number; + name: string; + accountId: number; + status: 'enabled' | string; +}; + +export type RollbarProjectAccessToken = { + projectId: number; + name: string; + scopes: RollbarProjectAccessTokenScope[]; + accessToken: string; + status: 'enabled' | string; +}; + +export type RollbarItem = { + publicItemId: number; + integrationsData: null; + levelLock: number; + controllingId: number; + lastActivatedTimestamp: number; + assignedUserId: number; + groupStatus: number; + hash: string; + id: number; + environment: RollbarEnvironment; + titleLock: number; + title: string; + lastOccurrenceId: number; + lastOccurrenceTimestamp: number; + platform: RollbarPlatformId; + firstOccurrenceTimestamp: number; + project_id: number; + resolvedInVersion: string; + status: 'enabled' | string; + uniqueOccurrences: number; + groupItemId: number; + framework: RollbarFrameworkId; + totalOccurrences: number; + level: RollbarLevel; + counter: number; + lastModifiedBy: number; + firstOccurrenceId: number; + activatingOccurrenceId: number; + lastResolvedTimestamp: number; +}; + +export type RollbarItemsResponse = { + items: RollbarItem[]; + page: number; + totalCount: number; +}; + +export type RollbarItemCount = { + timestamp: number; + count: number; +}; + +export type RollbarTopActiveItem = { + item: { + id: number; + counter: number; + environment: RollbarEnvironment; + framework: RollbarFrameworkId; + lastOccurrenceTimestamp: number; + level: number; + occurrences: number; + projectId: number; + title: string; + uniqueOccurrences: number; + }; + counts: number[]; +}; diff --git a/plugins/rollbar-backend/src/index.ts b/plugins/rollbar-backend/src/index.ts new file mode 100644 index 0000000000..e5023ebea4 --- /dev/null +++ b/plugins/rollbar-backend/src/index.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. + */ + +export * from './api'; +export * from './service/router'; diff --git a/plugins/rollbar-backend/src/run.ts b/plugins/rollbar-backend/src/run.ts new file mode 100644 index 0000000000..b96989e4b8 --- /dev/null +++ b/plugins/rollbar-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * 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 yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/rollbar-backend/src/service/router.test.ts b/plugins/rollbar-backend/src/service/router.test.ts new file mode 100644 index 0000000000..9ec0dbc7b5 --- /dev/null +++ b/plugins/rollbar-backend/src/service/router.test.ts @@ -0,0 +1,122 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; +import { RollbarApi } from '../api'; +import { createRouter } from './router'; +import { RollbarProject, RollbarTopActiveItem } from '../api/types'; + +describe('createRouter', () => { + let rollbarApi: jest.Mocked; + let app: express.Express; + + beforeAll(async () => { + rollbarApi = { + getAllProjects: jest.fn(), + getProject: jest.fn(), + getProjectItems: jest.fn(), + getTopActiveItems: jest.fn(), + getOccuranceCounts: jest.fn(), + getActivatedCounts: jest.fn(), + } as any; + const router = await createRouter({ + rollbarApi, + logger: getVoidLogger(), + }); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /projects', () => { + it('lists projects', async () => { + const projects: RollbarProject[] = [ + { id: 123, name: 'abc', accountId: 1, status: 'enabled' }, + { id: 456, name: 'xyz', accountId: 1, status: 'enabled' }, + ]; + + rollbarApi.getAllProjects.mockResolvedValueOnce(projects); + + const response = await request(app).get('/projects'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(projects); + }); + + it('throws an error', async () => { + rollbarApi.getAllProjects.mockImplementationOnce(() => { + throw Error('error'); + }); + + const response = await request(app).get('/projects'); + + expect(response.status).toEqual(500); + }); + }); + + describe('GET /projects/:id', () => { + it('fetches a single project', async () => { + const project: RollbarProject = { + id: 123, + name: 'abc', + accountId: 1, + status: 'enabled', + }; + + rollbarApi.getProject.mockResolvedValueOnce(project); + + const response = await request(app).get(`/projects/${123}`); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(project); + }); + }); + + describe('GET /projects/:id/top_active_items', () => { + it('fetches a single project', async () => { + const items: RollbarTopActiveItem[] = [ + { + item: { + id: 9898989, + counter: 1234, + environment: 'production', + framework: 2, + lastOccurrenceTimestamp: new Date().getTime() / 1000, + level: 50, + occurrences: 100, + projectId: 12345, + title: 'error occured', + uniqueOccurrences: 10, + }, + counts: [10, 10, 10, 10, 10, 50], + }, + ]; + + rollbarApi.getTopActiveItems.mockResolvedValueOnce(items); + + const response = await request(app).get( + `/projects/${123}/top_active_items`, + ); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(items); + }); + }); +}); diff --git a/plugins/rollbar-backend/src/service/router.ts b/plugins/rollbar-backend/src/service/router.ts new file mode 100644 index 0000000000..cee3297c2a --- /dev/null +++ b/plugins/rollbar-backend/src/service/router.ts @@ -0,0 +1,135 @@ +/* + * 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 } from '@backstage/backend-common'; +import { Logger } from 'winston'; +import Router from 'express-promise-router'; +import express from 'express'; +import { RollbarApi } from '../api'; + +export interface RouterOptions { + rollbarApi?: RollbarApi; + logger: Logger; +} + +export async function createRouter( + options: RouterOptions, +): Promise { + const router = Router(); + const logger = options.logger.child({ plugin: 'rollbar' }); + const accessToken = !options.rollbarApi ? getRollbarToken(logger) : ''; + + if (options.rollbarApi || accessToken) { + const rollbarApi = + options.rollbarApi || new RollbarApi(accessToken, logger); + + router.use(express.json()); + + const runAsync = createRunAsyncWrapper(logger); + + router.get( + '/projects', + runAsync(async (_req, res) => { + const projects = await rollbarApi.getAllProjects(); + res.status(200).header('').send(projects); + }), + ); + + router.get( + '/projects/:id', + runAsync(async (req, res) => { + const { id } = req.params; + const projects = await rollbarApi.getProject(id); + res.status(200).send(projects); + }), + ); + + router.get( + '/projects/:id/items', + runAsync(async (req, res) => { + const { id } = req.params; + const projects = await rollbarApi.getProjectItems(id); + res.status(200).send(projects); + }), + ); + + router.get( + '/projects/:id/top_active_items', + runAsync(async (req, res) => { + const { id } = req.params; + const query = req.query; + const items = await rollbarApi.getTopActiveItems(id, query as any); + res.status(200).send(items); + }), + ); + + router.get( + '/projects/:id/occurance_counts', + runAsync(async (req, res) => { + const { id } = req.params; + const query = req.query; + const items = await rollbarApi.getOccuranceCounts(id, query as any); + res.status(200).send(items); + }), + ); + + router.get( + '/projects/:id/activated_item_counts', + runAsync(async (req, res) => { + const { id } = req.params; + const query = req.query; + const items = await rollbarApi.getActivatedCounts(id, query as any); + res.status(200).send(items); + }), + ); + } + + router.use(errorHandler()); + + return router; +} + +function createRunAsyncWrapper(logger: Logger) { + return function runAsyncWrapper(callback: express.RequestHandler) { + return function runAsync( + req: express.Request, + res: express.Response, + next: express.NextFunction, + ) { + return Promise.resolve(callback(req, res, next)).catch(error => { + logger.error(error); + next(error); + }); + }; + }; +} + +function getRollbarToken(logger: Logger) { + const token = process.env.ROLLBAR_TOKEN || ''; + + if (!token) { + if (process.env.NODE_ENV !== 'development') { + throw new Error( + 'Rollbar token must be provided in ROLLBAR_TOKEN environment variable to start the API.', + ); + } + logger.warn( + 'Failed to initialize rollbar backend, set ROLLBAR_TOKEN environment variable to start the API.', + ); + } + + return token; +} diff --git a/plugins/rollbar-backend/src/service/standaloneServer.ts b/plugins/rollbar-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..ecd5c072fb --- /dev/null +++ b/plugins/rollbar-backend/src/service/standaloneServer.ts @@ -0,0 +1,44 @@ +/* + * 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 { createServiceBuilder } from '@backstage/backend-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: 'rollbar-backend' }); + + logger.debug('Creating application...'); + + const router = await createRouter({ logger }); + + const service = createServiceBuilder(module) + .enableCors({ origin: 'http://localhost:3000' }) + .addRouter('/catalog', router); + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} diff --git a/plugins/rollbar-backend/src/setupTests.ts b/plugins/rollbar-backend/src/setupTests.ts new file mode 100644 index 0000000000..f7b6ca962d --- /dev/null +++ b/plugins/rollbar-backend/src/setupTests.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. + */ + +require('jest-fetch-mock').enableMocks(); + +export {}; diff --git a/plugins/rollbar-backend/src/util/index.ts b/plugins/rollbar-backend/src/util/index.ts new file mode 100644 index 0000000000..e8b328d73d --- /dev/null +++ b/plugins/rollbar-backend/src/util/index.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. + */ + +interface PrimitiveMap { + [name: string]: number | string | boolean; +} + +export const buildQuery = (obj: PrimitiveMap) => { + return Object.entries(obj) + .map(pair => pair.map(encodeURIComponent).join('=')) + .join('&'); +}; diff --git a/plugins/rollbar/.eslintrc.js b/plugins/rollbar/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/rollbar/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/rollbar/README.md b/plugins/rollbar/README.md new file mode 100644 index 0000000000..d0cfae00cc --- /dev/null +++ b/plugins/rollbar/README.md @@ -0,0 +1,56 @@ +# Rollbar Plugin + +Website: [https://rollbar.com/](https://rollbar.com/) + +## Setup + +1. Configure the [rollbar backend plugin](https://github.com/spotify/backstage/tree/master/plugins/rollbar-backend/README.md) + +2. If you have standalone app (you didn't clone this repo), then do + +```bash +yarn add @backstage/plugin-rollbar +``` + +3. Add plugin to the list of plugins: + +```ts +// packages/app/src/plugins.ts +export { plugin as Rollbar } from '@backstage/plugin-rollbar'; +``` + +4. Add plugin API to your Backstage instance: + +```ts +// packages/app/src/api.ts +import { RollbarClient, rollbarApiRef } from '@backstage/plugin-rollbar'; + +// ... + +builder.add( + rollbarApiRef, + new RollbarClient({ + apiOrigin: backendUrl, + basePath: '/rollbar', + }), +); + +// Alternatively you can use the mock client +// builder.add(rollbarApiRef, new RollbarMockClient()); +``` + +5. Run app with `yarn start` and navigate to `/rollbar` + +## Features + +- List rollbar projects +- View top active items for each project + +## Limitations + +- Rollbar has rate limits per token + +## Links + +- (Backend part of the plugin)[https://github.com/spotify/backstage/tree/master/plugins/rollbar-backend] +- (The Backstage homepage)[https://backstage.io] diff --git a/plugins/rollbar/dev/index.tsx b/plugins/rollbar/dev/index.tsx new file mode 100644 index 0000000000..812a5585d4 --- /dev/null +++ b/plugins/rollbar/dev/index.tsx @@ -0,0 +1,20 @@ +/* + * 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 { createDevApp } from '@backstage/dev-utils'; +import { plugin } from '../src/plugin'; + +createDevApp().registerPlugin(plugin).render(); diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json new file mode 100644 index 0000000000..0645d62c9c --- /dev/null +++ b/plugins/rollbar/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-rollbar", + "version": "0.1.1-alpha.13", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core": "^0.1.1-alpha.13", + "@backstage/theme": "^0.1.1-alpha.13", + "@material-ui/core": "^4.9.1", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "lodash": "^4.17.15", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", + "react-sparklines": "^1.7.0", + "react-use": "^14.2.0" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.13", + "@backstage/dev-utils": "^0.1.1-alpha.13", + "@backstage/test-utils": "^0.1.1-alpha.13", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/react-hooks": "^3.3.0", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^25.2.2", + "@types/node": "^12.0.0", + "@types/react": "^16.9", + "jest-fetch-mock": "^3.0.3" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/rollbar/src/api/RollbarApi.ts b/plugins/rollbar/src/api/RollbarApi.ts new file mode 100644 index 0000000000..c08bc08d48 --- /dev/null +++ b/plugins/rollbar/src/api/RollbarApi.ts @@ -0,0 +1,37 @@ +/* + * 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 { createApiRef } from '@backstage/core'; +import { + RollbarItemsResponse, + RollbarProject, + RollbarTopActiveItem, +} from './types'; + +export const rollbarApiRef = createApiRef({ + id: 'plugin.rollbar.service', + description: + 'Used by the Rollbar plugin to make requests to accompanying backend', +}); + +export interface RollbarApi { + getAllProjects(): Promise; + getTopActiveItems( + project: string, + hours?: number, + ): Promise; + getProjectItems(project: string): Promise; +} diff --git a/plugins/rollbar/src/api/RollbarClient.ts b/plugins/rollbar/src/api/RollbarClient.ts new file mode 100644 index 0000000000..5cabbcbc24 --- /dev/null +++ b/plugins/rollbar/src/api/RollbarClient.ts @@ -0,0 +1,73 @@ +/* + * 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 { RollbarApi } from './RollbarApi'; +import { + RollbarItemsResponse, + RollbarProject, + RollbarTopActiveItem, +} from './types'; + +export class RollbarClient implements RollbarApi { + private apiOrigin: string; + private basePath: string; + + constructor({ + apiOrigin, + basePath, + }: { + apiOrigin: string; + basePath: string; + }) { + this.apiOrigin = apiOrigin; + this.basePath = basePath; + } + + async getAllProjects(): Promise { + const path = `/projects`; + + return await this.get(path); + } + + async getTopActiveItems( + project: string, + hours = 24, + environment = 'production', + ): Promise { + const path = `/projects/${project}/top_active_items?environment=${environment}&hours=${hours}`; + + return await this.get(path); + } + + async getProjectItems(project: string): Promise { + const path = `/projects/${project}/items`; + + return await this.get(path); + } + + private async get(path: string): Promise { + const url = `${this.apiOrigin}${this.basePath}${path}`; + const response = await fetch(url); + + if (!response.ok) { + const payload = await response.text(); + const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`; + throw new Error(message); + } + + return await response.json(); + } +} diff --git a/plugins/rollbar/src/api/RollbarMockClient.ts b/plugins/rollbar/src/api/RollbarMockClient.ts new file mode 100644 index 0000000000..cab9a0d23a --- /dev/null +++ b/plugins/rollbar/src/api/RollbarMockClient.ts @@ -0,0 +1,70 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import { RollbarApi } from './RollbarApi'; +import { + RollbarItemsResponse, + RollbarProject, + RollbarTopActiveItem, +} from './types'; + +export class RollbarMockClient implements RollbarApi { + async getAllProjects(): Promise { + return Promise.resolve([ + { id: 123, name: 'project-a', accountId: 1, status: 'enabled' }, + { id: 356, name: 'project-b', accountId: 1, status: 'enabled' }, + { id: 789, name: 'project-c', accountId: 1, status: 'enabled' }, + ]); + } + + async getTopActiveItems( + _project: string, + _hours = 24, + _environment = 'production', + ): Promise { + const createItem = (id: number): RollbarTopActiveItem => ({ + item: { + id, + counter: id, + environment: 'production', + framework: 2, + lastOccurrenceTimestamp: new Date().getTime() / 1000, + level: 50, + occurrences: 100, + projectId: 12345, + title: `Some error occurred in service - ${id}`, + uniqueOccurrences: 10, + }, + counts: Array.from({ length: 168 }, () => + Math.floor(Math.random() * 100), + ), + }); + + const items = Array.from({ length: 10 }, (_, i) => createItem(i)); + + return Promise.resolve(items); + } + + async getProjectItems(_project: string): Promise { + return Promise.resolve({ + items: [], + page: 0, + totalCount: 0, + }); + } +} diff --git a/plugins/rollbar/src/api/types.ts b/plugins/rollbar/src/api/types.ts new file mode 100644 index 0000000000..de4cb43aef --- /dev/null +++ b/plugins/rollbar/src/api/types.ts @@ -0,0 +1,135 @@ +/* + * 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. + */ + +// TODO: Make this shared/dry with backend + +export type RollbarProjectAccessTokenScope = 'read' | 'write'; +export type RollbarEnvironment = 'production' | string; + +export enum RollbarLevel { + debug = 10, + info = 20, + warning = 30, + error = 40, + critical = 50, +} + +export enum RollbarFrameworkId { + 'unknown' = 0, + 'rails' = 1, + 'django' = 2, + 'pyramid' = 3, + 'node-js' = 4, + 'pylons' = 5, + 'php' = 6, + 'browser-js' = 7, + 'rollbar-system' = 8, + 'android' = 9, + 'ios' = 10, + 'mailgun' = 11, + 'logentries' = 12, + 'python' = 13, + 'ruby' = 14, + 'sidekiq' = 15, + 'flask' = 16, + 'celery' = 17, + 'rq' = 18, +} + +export enum RollbarPlatformId { + 'unknown' = 0, + 'browser' = 1, + 'flash' = 2, + 'android' = 3, + 'ios' = 4, + 'heroku' = 5, + 'google-app-engine' = 6, + 'client' = 7, +} + +export type RollbarProject = { + id: number; + name: string; + accountId: number; + status: 'enabled' | string; +}; + +export type RollbarProjectAccessToken = { + projectId: number; + name: string; + scopes: RollbarProjectAccessTokenScope[]; + accessToken: string; + status: 'enabled' | string; +}; + +export type RollbarItem = { + publicItemId: number; + integrationsData: null; + levelLock: number; + controllingId: number; + lastActivatedTimestamp: number; + assignedUserId: number; + groupStatus: number; + hash: string; + id: number; + environment: RollbarEnvironment; + titleLock: number; + title: string; + lastOccurrenceId: number; + lastOccurrenceTimestamp: number; + platform: RollbarPlatformId; + firstOccurrenceTimestamp: number; + project_id: number; + resolvedInVersion: string; + status: 'enabled' | string; + uniqueOccurrences: number; + groupItemId: number; + framework: RollbarFrameworkId; + totalOccurrences: number; + level: RollbarLevel; + counter: number; + lastModifiedBy: number; + firstOccurrenceId: number; + activatingOccurrenceId: number; + lastResolvedTimestamp: number; +}; + +export type RollbarItemsResponse = { + items: RollbarItem[]; + page: number; + totalCount: number; +}; + +export type RollbarItemCount = { + timestamp: number; + count: number; +}; + +export type RollbarTopActiveItem = { + item: { + id: number; + counter: number; + environment: RollbarEnvironment; + framework: RollbarFrameworkId; + lastOccurrenceTimestamp: number; + level: number; + occurrences: number; + projectId: number; + title: string; + uniqueOccurrences: number; + }; + counts: number[]; +}; diff --git a/plugins/rollbar/src/components/RollbarLayout/RollbarLayout.tsx b/plugins/rollbar/src/components/RollbarLayout/RollbarLayout.tsx new file mode 100644 index 0000000000..7bda63ea5f --- /dev/null +++ b/plugins/rollbar/src/components/RollbarLayout/RollbarLayout.tsx @@ -0,0 +1,52 @@ +/* + * 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, { ReactNode } from 'react'; +import { + Header, + Page, + pageTheme, + Content, + ContentHeader, + SupportButton, +} from '@backstage/core'; +import { Grid } from '@material-ui/core'; + +type Props = { + title?: string; + children: ReactNode; +}; + +export const RollbarLayout = ({ title = 'Dashboard', children }: Props) => { + return ( + +
+ + + + Rollbar plugin allows you to preview issues and navigate to rollbar. + + + + {children} + + + + ); +}; diff --git a/plugins/rollbar/src/components/RollbarPage/RollbarPage.test.tsx b/plugins/rollbar/src/components/RollbarPage/RollbarPage.test.tsx new file mode 100644 index 0000000000..2b0268500d --- /dev/null +++ b/plugins/rollbar/src/components/RollbarPage/RollbarPage.test.tsx @@ -0,0 +1,47 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as React from 'react'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { render } from '@testing-library/react'; +import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi'; +import { RollbarProject } from '../../api/types'; +import { RollbarPage } from './RollbarPage'; + +describe('RollbarPage component', () => { + const projects: RollbarProject[] = [ + { id: 123, name: 'abc', accountId: 1, status: 'enabled' }, + { id: 456, name: 'xyz', accountId: 1, status: 'enabled' }, + ]; + const rollbarApi: Partial = { + getAllProjects: () => Promise.resolve(projects), + }; + + const renderWrapped = (children: React.ReactNode) => + render( + wrapInTestApp( + + {children} + , + ), + ); + + it('should render rollbar landing page', async () => { + const rendered = renderWrapped(); + expect(rendered.getByText(/Rollbar/)).toBeInTheDocument(); + }); +}); diff --git a/plugins/rollbar/src/components/RollbarPage/RollbarPage.tsx b/plugins/rollbar/src/components/RollbarPage/RollbarPage.tsx new file mode 100644 index 0000000000..ec07c0fa48 --- /dev/null +++ b/plugins/rollbar/src/components/RollbarPage/RollbarPage.tsx @@ -0,0 +1,33 @@ +/* + * 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 { useAsync } from 'react-use'; +import { useApi } from '@backstage/core'; +import { rollbarApiRef } from '../../api/RollbarApi'; +import { RollbarLayout } from '../RollbarLayout/RollbarLayout'; +import { RollbarProjectTable } from './RollbarProjectTable'; + +export const RollbarPage = () => { + const rollbarApi = useApi(rollbarApiRef); + const { value, loading } = useAsync(() => rollbarApi.getAllProjects()); + + return ( + + + + ); +}; diff --git a/plugins/rollbar/src/components/RollbarPage/RollbarProjectTable.tsx b/plugins/rollbar/src/components/RollbarPage/RollbarProjectTable.tsx new file mode 100644 index 0000000000..bb8299b57e --- /dev/null +++ b/plugins/rollbar/src/components/RollbarPage/RollbarProjectTable.tsx @@ -0,0 +1,72 @@ +/* + * 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 { Link as RouterLink } from 'react-router-dom'; +import { Link } from '@material-ui/core'; +import { Table, TableColumn } from '@backstage/core'; +import { RollbarProject } from '../../api/types'; + +const columns: TableColumn[] = [ + { + title: 'ID', + field: 'id', + type: 'numeric', + align: 'left', + width: '100px', + }, + { + title: 'Name', + field: 'name', + type: 'string', + align: 'left', + highlight: true, + render: (row: Partial) => ( + + {row.name} + + ), + }, + { + title: 'Status', + field: 'status', + type: 'string', + align: 'left', + }, +]; + +type Props = { + projects: RollbarProject[]; + loading: boolean; +}; + +export const RollbarProjectTable = ({ projects, loading }: Props) => { + return ( + + ); +}; diff --git a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx new file mode 100644 index 0000000000..135075bf82 --- /dev/null +++ b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as React from 'react'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { render } from '@testing-library/react'; +import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi'; +import { RollbarTopActiveItem } from '../../api/types'; +import { RollbarProjectPage } from './RollbarProjectPage'; + +describe('RollbarProjectPage component', () => { + const items: RollbarTopActiveItem[] = [ + { + item: { + id: 9898989, + counter: 1234, + environment: 'production', + framework: 2, + lastOccurrenceTimestamp: new Date().getTime() / 1000, + level: 50, + occurrences: 100, + projectId: 12345, + title: 'error occured', + uniqueOccurrences: 10, + }, + counts: [10, 10, 10, 10, 10, 50], + }, + ]; + const rollbarApi: Partial = { + getTopActiveItems: () => Promise.resolve(items), + }; + + const renderWrapped = (children: React.ReactNode) => + render( + wrapInTestApp( + + {children} + , + ), + ); + + it('should render rollbar project page', async () => { + const rendered = renderWrapped(); + expect(rendered.getByText(/Top Active Items/)).toBeInTheDocument(); + }); +}); diff --git a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx new file mode 100644 index 0000000000..d5641e58ce --- /dev/null +++ b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx @@ -0,0 +1,43 @@ +/* + * 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 { useParams } from 'react-router-dom'; +import { useAsync } from 'react-use'; +import { useApi } from '@backstage/core'; +import { rollbarApiRef } from '../../api/RollbarApi'; +import { RollbarLayout } from '../RollbarLayout/RollbarLayout'; +import { RollbarTopItemsTable } from '../RollbarTopItemsTable/RollbarTopItemsTable'; + +export const RollbarProjectPage = () => { + const rollbarApi = useApi(rollbarApiRef); + const { componentId } = useParams() as { + componentId: string; + }; + const { value, loading } = useAsync(() => + rollbarApi + .getTopActiveItems(componentId, 168) + .then(data => + data.sort((a, b) => b.item.occurrences - a.item.occurrences), + ), + ); + + return ( + + + + ); +}; diff --git a/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx new file mode 100644 index 0000000000..724426ac8f --- /dev/null +++ b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx @@ -0,0 +1,57 @@ +/* + * 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 { wrapInTestApp } from '@backstage/test-utils'; +import { render } from '@testing-library/react'; +import * as React from 'react'; +import { RollbarTopItemsTable } from './RollbarTopItemsTable'; +import { RollbarTopActiveItem } from '../../api/types'; + +const items: RollbarTopActiveItem[] = [ + { + item: { + id: 89898989, + counter: 1234, + environment: 'production', + framework: 0, + lastOccurrenceTimestamp: new Date().getTime() / 1000, + level: 50, + occurrences: 150, + title: 'Error in foo', + uniqueOccurrences: 40, + projectId: 12345, + }, + counts: [10, 20, 30, 40, 50], + }, +]; + +describe('RollbarTopItemsTable component', () => { + it('should render empty data message when loaded and no data', async () => { + const rendered = render( + wrapInTestApp(), + ); + expect(rendered.getByText(/No records to display/)).toBeInTheDocument(); + }); + + it('should display item attributes when loading has finished', async () => { + const rendered = render( + wrapInTestApp(), + ); + expect(rendered.getByText(/1234/)).toBeInTheDocument(); + expect(rendered.getByText(/Error in foo/)).toBeInTheDocument(); + expect(rendered.getByText(/critical/)).toBeInTheDocument(); + }); +}); diff --git a/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx new file mode 100644 index 0000000000..44dd5ed3f0 --- /dev/null +++ b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx @@ -0,0 +1,102 @@ +/* + * 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 { Table, TableColumn } from '@backstage/core'; +import { + RollbarFrameworkId, + RollbarLevel, + RollbarTopActiveItem, +} from '../../api/types'; +import { RollbarTrendGraph } from '../RollbarTrendGraph/RollbarTrendGraph'; + +const columns: TableColumn[] = [ + { + title: 'ID', + field: 'item.counter', + type: 'string', + align: 'left', + width: '70px', + }, + { + title: 'Title', + field: 'item.title', + type: 'string', + align: 'left', + }, + { + title: 'Trend', + sorting: false, + render: data => ( + + ), + }, + { + title: 'Occurrences', + field: 'item.occurrences', + type: 'numeric', + align: 'right', + }, + { + title: 'Environment', + field: 'item.environment', + type: 'string', + }, + { + title: 'Level', + field: 'item.level', + type: 'string', + render: data => RollbarLevel[(data as RollbarTopActiveItem).item.level], + }, + { + title: 'Framework', + field: 'item.framework', + type: 'string', + render: data => + RollbarFrameworkId[(data as RollbarTopActiveItem).item.framework], + }, + { + title: 'Last Occurrence', + field: 'item.lastOccurrenceTimestamp', + type: 'datetime', + render: data => + new Date( + (data as RollbarTopActiveItem).item.lastOccurrenceTimestamp * 1000, + ).toLocaleDateString(), + }, +]; + +type Props = { + items: RollbarTopActiveItem[]; + loading: boolean; +}; + +export const RollbarTopItemsTable = ({ items, loading }: Props) => { + return ( +
+ ); +}; diff --git a/plugins/rollbar/src/components/RollbarTrendGraph/RollbarTrendGraph.test.tsx b/plugins/rollbar/src/components/RollbarTrendGraph/RollbarTrendGraph.test.tsx new file mode 100644 index 0000000000..1f9d317e5b --- /dev/null +++ b/plugins/rollbar/src/components/RollbarTrendGraph/RollbarTrendGraph.test.tsx @@ -0,0 +1,32 @@ +/* + * 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 { wrapInTestApp } from '@backstage/test-utils'; +import { RollbarTrendGraph } from './RollbarTrendGraph'; + +describe('RollbarTrendGraph component', () => { + it('should render a trend graph sparkline', async () => { + const mockCounts = [1, 2, 3, 4]; + const rendered = render( + wrapInTestApp( + , + ), + ); + expect(rendered).toBeTruthy(); + }); +}); diff --git a/plugins/rollbar/src/components/RollbarTrendGraph/RollbarTrendGraph.tsx b/plugins/rollbar/src/components/RollbarTrendGraph/RollbarTrendGraph.tsx new file mode 100644 index 0000000000..4f0b53eb4a --- /dev/null +++ b/plugins/rollbar/src/components/RollbarTrendGraph/RollbarTrendGraph.tsx @@ -0,0 +1,30 @@ +/* + * 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 { Sparklines, SparklinesBars } from 'react-sparklines'; + +type Props = { + counts: number[]; +}; + +export const RollbarTrendGraph = ({ counts }: Props) => { + return ( + + + + ); +}; diff --git a/plugins/rollbar/src/index.ts b/plugins/rollbar/src/index.ts new file mode 100644 index 0000000000..3a8288a545 --- /dev/null +++ b/plugins/rollbar/src/index.ts @@ -0,0 +1,20 @@ +/* + * 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'; +export * from './api/RollbarApi'; +export { RollbarClient } from './api/RollbarClient'; +export { RollbarMockClient } from './api/RollbarMockClient'; diff --git a/plugins/rollbar/src/plugin.test.ts b/plugins/rollbar/src/plugin.test.ts new file mode 100644 index 0000000000..44c2168c78 --- /dev/null +++ b/plugins/rollbar/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('rollbar', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/rollbar/src/plugin.ts b/plugins/rollbar/src/plugin.ts new file mode 100644 index 0000000000..d682750a83 --- /dev/null +++ b/plugins/rollbar/src/plugin.ts @@ -0,0 +1,28 @@ +/* + * 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 { RollbarPage } from './components/RollbarPage/RollbarPage'; +import { RollbarProjectPage } from './components/RollbarProjectPage/RollbarProjectPage'; +import { rootRoute, rootProjectRoute } from './routes'; + +export const plugin = createPlugin({ + id: 'rollbar', + register({ router }) { + router.addRoute(rootRoute, RollbarPage); + router.addRoute(rootProjectRoute, RollbarProjectPage); + }, +}); diff --git a/plugins/rollbar/src/routes.ts b/plugins/rollbar/src/routes.ts new file mode 100644 index 0000000000..ecf20f3167 --- /dev/null +++ b/plugins/rollbar/src/routes.ts @@ -0,0 +1,27 @@ +/* + * 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 { createRouteRef } from '@backstage/core'; + +export const rootRoute = createRouteRef({ + path: '/rollbar', + title: 'Rollbar Home', +}); + +export const rootProjectRoute = createRouteRef({ + path: '/rollbar/:componentId/*', + title: 'Rollbar', +}); diff --git a/plugins/rollbar/src/setupTests.ts b/plugins/rollbar/src/setupTests.ts new file mode 100644 index 0000000000..8553642152 --- /dev/null +++ b/plugins/rollbar/src/setupTests.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. + */ + +import '@testing-library/jest-dom'; + +require('jest-fetch-mock').enableMocks(); diff --git a/yarn.lock b/yarn.lock index 0641f5ed77..29fe56eb44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5839,6 +5839,15 @@ camelcase-keys@^4.0.0: map-obj "^2.0.0" quick-lru "^1.0.0" +camelcase-keys@^6.2.2: + version "6.2.2" + resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" + integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== + dependencies: + camelcase "^5.3.1" + map-obj "^4.0.0" + quick-lru "^4.0.1" + camelcase@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" @@ -12499,6 +12508,11 @@ map-obj@^2.0.0: resolved "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk= +map-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5" + integrity sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g== + map-or-similar@^1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08" @@ -15240,6 +15254,11 @@ quick-lru@^1.0.0: resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= +quick-lru@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" + integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== + raf-schd@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" From 9382e24839e62a2e80ff489eeaee75c7d46d3388 Mon Sep 17 00:00:00 2001 From: "Emil H. Clausen" Date: Thu, 9 Jul 2020 10:22:21 +0200 Subject: [PATCH 12/13] Reverse file permissions for mock-data.sh scripts --- plugins/catalog-backend/scripts/mock-data.sh | 0 plugins/scaffolder-backend/scripts/mock-data.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 plugins/catalog-backend/scripts/mock-data.sh mode change 100644 => 100755 plugins/scaffolder-backend/scripts/mock-data.sh diff --git a/plugins/catalog-backend/scripts/mock-data.sh b/plugins/catalog-backend/scripts/mock-data.sh old mode 100644 new mode 100755 diff --git a/plugins/scaffolder-backend/scripts/mock-data.sh b/plugins/scaffolder-backend/scripts/mock-data.sh old mode 100644 new mode 100755 From eed37376fc9e4cb304316b08d78efbea38d983db Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Thu, 9 Jul 2020 11:08:19 +0200 Subject: [PATCH 13/13] Initial techdocs-cli (#1558) * Initial techdocs-cli * Run backstage serve and docker container from techdocs cli * Added documentation for techdocs-cli * Fix linting * Builds and export the dev folder of a plugin * make bin/build executable * Remove 'src' from files * fix * fix: corrected bad rebase * feat(techdocs-cli): create export in techdocs plugin + clone to cli build * fix: added support for nodemon * feat(techdocs-cli): add vercel/serve-handler npm package * fix: bump eslint-config to 8.0.0 + add prettier-config * fixup * fix: add HTTPServer module for serving techdocs-preview-bundle/ * fix: addressed tsc issues (unused import, missing type definition) Co-authored-by: Bilawal Hameed --- packages/techdocs-cli/.eslintrc.js | 5 +- packages/techdocs-cli/README.md | 46 ++++++ packages/techdocs-cli/bin/build | 35 +++++ packages/techdocs-cli/package.json | 22 ++- packages/techdocs-cli/src/index.ts | 74 +++++++++- packages/techdocs-cli/src/lib/httpServer.ts | 35 +++++ packages/techdocs-cli/src/lib/paths.ts | 150 ++++++++++++++++++++ packages/techdocs-cli/src/lib/version.ts | 26 ++++ yarn.lock | 97 ++++++++++++- 9 files changed, 479 insertions(+), 11 deletions(-) create mode 100755 packages/techdocs-cli/bin/build create mode 100644 packages/techdocs-cli/src/lib/httpServer.ts create mode 100644 packages/techdocs-cli/src/lib/paths.ts create mode 100644 packages/techdocs-cli/src/lib/version.ts diff --git a/packages/techdocs-cli/.eslintrc.js b/packages/techdocs-cli/.eslintrc.js index 13573efa9c..503c048748 100644 --- a/packages/techdocs-cli/.eslintrc.js +++ b/packages/techdocs-cli/.eslintrc.js @@ -1,3 +1,6 @@ module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], + extends: [require.resolve('@backstage/cli/config/eslint.backend')], + rules: { + 'no-console': 0, + }, }; diff --git a/packages/techdocs-cli/README.md b/packages/techdocs-cli/README.md index d7c17210bd..8d86363c12 100644 --- a/packages/techdocs-cli/README.md +++ b/packages/techdocs-cli/README.md @@ -3,3 +3,49 @@ Check out the [TechDocs README](https://github.com/spotify/backstage/blob/master/plugins/techdocs/README.md) to learn more. **WIP: This cli is a work in progress. It is not ready for use yet. Follow our progress on [the Backstage Discord](https://discord.gg/MUpMjP2) under #docs-like-code or on [our GitHub Milestone](https://github.com/spotify/backstage/milestone/15).** + +## Getting Started + +You'll need Docker installed and running to use this. You will also need to build the container located at `plugins/techdocs/mkdocs/container` under the tag `mkdocs:local-dev`, as you can see in the commands from below: + +```bash +docker build plugins/techdocs/mkdocs/container -t mkdocs:local-dev +``` + +From that point, you can invoke the CLI from any project with a docs folder. Try out our example! + +```bash +cd plugins/techdocs/mkdocs/mock-docs +npx @techdocs/cli serve +``` + +## Local Development + +You'll need Docker installed and running to use this. You will also need to build the container located at `plugins/techdocs/mkdocs/container` under the tag `mkdocs:local-dev`, as you can see in the commands from below: + +```bash +docker build plugins/techdocs/mkdocs/container -t mkdocs:local-dev +``` + +Once that is built, you'll need to manually create an `alias` for running the CLI locally: + +```bash +cd packages/techdocs-cli +echo "$(pwd)/bin/techdocs" + +# Copy the value from above and add it in [HERE] below +# For more convenience, add it to your ~/.zshrc or ~/.bash_profile +# otherwise you'll lose it when you open a new Terminal +alias techdocs="[HERE]" +``` + +From that point, you can invoke `techdocs` from any project with a docs folder. Try out our example! + +```bash +cd plugins/techdocs/mkdocs/mock-docs +techdocs serve +``` + +You should have a `localhost:3000` serving TechDocs in Backstage, as well as `localhost:8000` serving Mkdocs (which won't open up and be exposed to the user). + +Happy hacking! diff --git a/packages/techdocs-cli/bin/build b/packages/techdocs-cli/bin/build new file mode 100755 index 0000000000..c0a1cacdfc --- /dev/null +++ b/packages/techdocs-cli/bin/build @@ -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. + +set -e + +ROOT_DIR=$(git rev-parse --show-toplevel) +TECHDOCS_PREVIEW_SOURCE=$ROOT_DIR/plugins/techdocs/dist +TECHDOCS_PREVIEW_DEST=$ROOT_DIR/packages/techdocs-cli/dist/techdocs-preview-bundle + +# Build the CLI +backstage-cli build --outputs cjs + +# Create export of the TechDocs plugin +yarn workspace @backstage/plugin-techdocs export + +# Copy over export to techdocs-cli dist/ folder +cp -r $TECHDOCS_PREVIEW_SOURCE $TECHDOCS_PREVIEW_DEST + +# Clean output +yarn workspace @backstage/plugin-techdocs clean + +# Write to console +echo "[techdocs-cli]: Built the dist/ folder" +echo "[techdocs-cli]: Imported @backstage/plugin-techdocs dist/ folder into techdocs-preview-bundle/" diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 9176d6012d..c8985367b0 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,8 +1,8 @@ { - "name": "@backstage/techdocs-cli", + "name": "@techdocs/cli", "description": "CLI for running TechDocs locally.", "version": "0.1.1-alpha.13", - "private": true, + "private": false, "publishConfig": { "access": "public" }, @@ -18,7 +18,7 @@ "license": "Apache-2.0", "main": "dist/index.cjs.js", "scripts": { - "build": "backstage-cli build --outputs cjs", + "build": "./bin/build", "lint": "backstage-cli lint", "test": "backstage-cli test --passWithNoTests", "clean": "backstage-cli clean", @@ -28,7 +28,11 @@ "techdocs": "bin/techdocs-cli" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.13" + "@spotify/eslint-config": "^7.0.0", + "@spotify/prettier-config": "^7.0.0", + "@types/serve-handler": "^6.1.0", + "eslint": "^7.1.0", + "eslint-plugin-import": "^2.22.0" }, "files": [ "bin", @@ -36,7 +40,15 @@ ], "nodemonConfig": { "watch": "./src", - "exec": "bin/techdocs-cli", + "exec": "bin/build && bin/techdocs-cli", "ext": "ts" + }, + "dependencies": { + "@backstage/cli": "^0.1.1-alpha.13", + "@backstage/plugin-techdocs": "^0.1.1-alpha.13", + "chalk": "^4.1.0", + "commander": "^5.1.0", + "fs-extra": "^9.0.1", + "serve-handler": "^6.1.3" } } diff --git a/packages/techdocs-cli/src/index.ts b/packages/techdocs-cli/src/index.ts index a010f4bfe5..f9ab1c45de 100644 --- a/packages/techdocs-cli/src/index.ts +++ b/packages/techdocs-cli/src/index.ts @@ -13,5 +13,77 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import program from 'commander'; +import { version } from './lib/version'; +// import chalk from 'chalk'; +import { spawn } from 'child_process'; +import path from 'path'; +// import HTTPServer from './lib/httpServer'; -export const techDocsCli = () => {}; +const run = (workingDirectory: string, name: string, args: string[] = []) => { + const child = spawn(name, args, { + cwd: workingDirectory, + stdio: ['inherit', 'inherit', 'inherit'], + shell: true, + env: { + ...process.env, + FORCE_COLOR: 'true', + }, + }); + + child.once('error', error => { + console.error(error); + }); + child.once('exit', code => { + console.log('exited!', code); + }); +}; + +const main = (argv: string[]) => { + program.name('techdocs-cli').version(version); + + program + .command('serve') + .description('Serve a documentation project locally') + .action(() => { + // const techdocsPreviewBundlePath = path.join( + // __dirname, + // '..', + // 'dist', + // 'techdocs-preview-bundle', + // ); + + // new HTTPServer(techdocsPreviewBundlePath, 3000).serve(); + + run(process.env.PWD!, 'docker', [ + 'run', + '-it', + '-w', + '/content', + '-v', + '$(pwd):/content', + '-p', + '8000:8000', + 'mkdocs:local-dev', + 'serve', + '-a', + '0.0.0.0:8000', + ]); + + const pluginPath = path.join( + require.resolve('@backstage/plugin-techdocs'), + '..', + '..', + ); + + run( + pluginPath, + path.join(require.resolve('@backstage/cli'), '../../bin/backstage-cli'), + ['plugin:serve'], + ); + }); + + program.parse(argv); +}; + +main(process.argv); diff --git a/packages/techdocs-cli/src/lib/httpServer.ts b/packages/techdocs-cli/src/lib/httpServer.ts new file mode 100644 index 0000000000..6ca02fc7e9 --- /dev/null +++ b/packages/techdocs-cli/src/lib/httpServer.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 serveHandler from 'serve-handler'; +import http from 'http'; + +export default class HTTPServer { + constructor(public dir: string, public port: number) {} + + serve() { + const server = http.createServer((request, response) => { + return serveHandler(request, response, { + public: this.dir, + trailingSlash: true, + }); + }); + + server.listen(this.port, () => { + console.log('Running at http://localhost:3000'); + }); + } +} diff --git a/packages/techdocs-cli/src/lib/paths.ts b/packages/techdocs-cli/src/lib/paths.ts new file mode 100644 index 0000000000..be3f3dcee8 --- /dev/null +++ b/packages/techdocs-cli/src/lib/paths.ts @@ -0,0 +1,150 @@ +/* + * 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 fs from 'fs-extra'; +import { dirname, resolve as resolvePath } from 'path'; + +export type ResolveFunc = (...paths: string[]) => string; + +// Common paths and resolve functions used by the cli. +// Currently assumes it is being executed within a monorepo. +export type Paths = { + // Root dir of the cli itself, containing package.json + ownDir: string; + + // Monorepo root dir of the cli itself. Only accessible when running inside Backstage repo. + ownRoot: string; + + // The location of the app that the cli is being executed in + targetDir: string; + + // The monorepo root package of the app that the cli is being executed in. + targetRoot: string; + + // Resolve a path relative to own repo + resolveOwn: ResolveFunc; + + // Resolve a path relative to own monorepo root. Only accessible when running inside Backstage repo. + resolveOwnRoot: ResolveFunc; + + // Resolve a path relative to the app + resolveTarget: ResolveFunc; + + // Resolve a path relative to the app repo root + resolveTargetRoot: ResolveFunc; +}; + +// Looks for a package.json that has name: "root" to identify the root of the monorepo +export function findRootPath(topPath: string): string { + let path = topPath; + + // Some sanity check to avoid infinite loop + for (let i = 0; i < 1000; i++) { + const packagePath = resolvePath(path, 'package.json'); + const exists = fs.pathExistsSync(packagePath); + if (exists) { + try { + const data = fs.readJsonSync(packagePath); + if (data.name === 'root' || data.name.includes('backstage-e2e')) { + return path; + } + } catch (error) { + throw new Error( + `Failed to parse package.json file while searching for root, ${error}`, + ); + } + } + + const newPath = dirname(path); + if (newPath === path) { + throw new Error( + `No package.json with name "root" found as a parent of ${topPath}`, + ); + } + path = newPath; + } + + throw new Error( + `Iteration limit reached when searching for root package.json at ${topPath}`, + ); +} + +// Finds the root of the cli package itself +export function findOwnDir() { + // Known relative locations of package in dist/dev + const pathDist = '..'; + const pathDev = '../..'; + + // Check the closest dir first + const pkgInDist = resolvePath(__dirname, pathDist, 'package.json'); + const isDist = fs.pathExistsSync(pkgInDist); + + const path = isDist ? pathDist : pathDev; + return resolvePath(__dirname, path); +} + +// Finds the root of the monorepo that the cli exists in. Only accessible when running inside Backstage repo. +export function findOwnRootPath(ownDir: string) { + const isLocal = fs.pathExistsSync(resolvePath(ownDir, 'src')); + if (!isLocal) { + throw new Error( + 'Tried to access monorepo package root dir outside of Backstage repository', + ); + } + + return resolvePath(ownDir, '../..'); +} + +export function findPaths(): Paths { + const ownDir = findOwnDir(); + const targetDir = fs.realpathSync(process.cwd()); + + // Lazy load this as it will throw an error if we're not inside the Backstage repo. + let ownRoot = ''; + const getOwnRoot = () => { + if (!ownRoot) { + ownRoot = findOwnRootPath(ownDir); + } + return ownRoot; + }; + + // We're not always running in a monorepo, so we lazy init this to only crash commands + // that require a monorepo when we're not in one. + let targetRoot = ''; + const getTargetRoot = () => { + if (!targetRoot) { + targetRoot = findRootPath(targetDir); + } + return targetRoot; + }; + + return { + ownDir, + get ownRoot() { + return getOwnRoot(); + }, + targetDir, + get targetRoot() { + return getTargetRoot(); + }, + resolveOwn: (...paths) => resolvePath(ownDir, ...paths), + resolveOwnRoot: (...paths) => resolvePath(getOwnRoot(), ...paths), + resolveTarget: (...paths) => resolvePath(targetDir, ...paths), + resolveTargetRoot: (...paths) => resolvePath(getTargetRoot(), ...paths), + }; +} + +export const paths = findPaths(); diff --git a/packages/techdocs-cli/src/lib/version.ts b/packages/techdocs-cli/src/lib/version.ts new file mode 100644 index 0000000000..24734b87cc --- /dev/null +++ b/packages/techdocs-cli/src/lib/version.ts @@ -0,0 +1,26 @@ +/* + * 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 fs from 'fs-extra'; +import { paths } from './paths'; + +export function findVersion() { + const pkgContent = fs.readFileSync(paths.resolveOwn('package.json'), 'utf8'); + return JSON.parse(pkgContent).version; +} + +export const version = findVersion(); +export const isDev = fs.pathExistsSync(paths.resolveOwn('src')); diff --git a/yarn.lock b/yarn.lock index 29fe56eb44..fbdde05008 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2559,7 +2559,7 @@ resolved "https://registry.npmjs.org/@spotify/eslint-config-typescript/-/eslint-config-typescript-7.0.0.tgz#fc5227e3344f74b41ac3a530df24a95ac13254e4" integrity sha512-28I/SAf68NKbWZ5IY0WYMa0D18PxWdC9DP9gRbOTlZufmsS8jEgqf3zBUWmP6XOf1nihpKWcqvbFUG5H7/JYXA== -"@spotify/eslint-config@^7.0.1": +"@spotify/eslint-config@^7.0.0", "@spotify/eslint-config@^7.0.1": version "7.0.1" resolved "https://registry.npmjs.org/@spotify/eslint-config/-/eslint-config-7.0.1.tgz#07a21cfd7fce89cfc2c6dd5ea5d747e741201b66" integrity sha512-8GI/TZGUhS4pr7oipT2MjrZFRgXcKzk9YImEusUdD2f5vlCniRFIBQNrvTMkyjfdQqvIVqJPLcdVPXeAgprsMw== @@ -2576,6 +2576,11 @@ eslint-plugin-react "^7.12.4" eslint-plugin-react-hooks "^4.0.0" +"@spotify/prettier-config@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-7.0.0.tgz#47750979d1282197295108b6958360660a955c16" + integrity sha512-lIMcx/2oDqTtW84iHKkRJe+8U6HK6GPwWH5sJp9UEHcDpdXomOQYvwcGXy2I2zwPQQ14gYYE6nEJuSnnYqsYRw== + "@spotify/prettier-config@^8.0.0": version "8.0.0" resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-8.0.0.tgz#8b6c2bd579ddc54887155a0721fe04e96c89f7f2" @@ -3960,6 +3965,13 @@ "@types/node" "*" rollup "^0.63.4" +"@types/serve-handler@^6.1.0": + version "6.1.0" + resolved "https://registry.npmjs.org/@types/serve-handler/-/serve-handler-6.1.0.tgz#6952aaf864e542297ce8a2480b3e0d9ea59de7f6" + integrity sha512-F9BpWotLZrQvq1CdE3oCnVmQUb6dWq20HSB/qjF4mG7WkZjPQ6fye8veznMKoBPhFJve4yz9C1NOITdTcoLngQ== + dependencies: + "@types/node" "*" + "@types/serve-static@*": version "1.13.3" resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" @@ -5938,7 +5950,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0: +chalk@^4.0.0, chalk@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== @@ -6505,6 +6517,11 @@ contains-path@^0.1.0: resolved "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= + content-disposition@0.5.3: version "0.5.3" resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" @@ -8139,6 +8156,25 @@ eslint-plugin-import@^2.20.2: resolve "^1.17.0" tsconfig-paths "^3.9.0" +eslint-plugin-import@^2.22.0: + version "2.22.0" + resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz#92f7736fe1fde3e2de77623c838dd992ff5ffb7e" + integrity sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg== + dependencies: + array-includes "^3.1.1" + array.prototype.flat "^1.2.3" + contains-path "^0.1.0" + debug "^2.6.9" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.3" + eslint-module-utils "^2.6.0" + has "^1.0.3" + minimatch "^3.0.4" + object.values "^1.1.1" + read-pkg-up "^2.0.0" + resolve "^1.17.0" + tsconfig-paths "^3.9.0" + eslint-plugin-jest@^23.6.0: version "23.8.2" resolved "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-23.8.2.tgz#6f28b41c67ef635f803ebd9e168f6b73858eb8d4" @@ -8659,6 +8695,13 @@ fast-shallow-equal@^1.0.0: resolved "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b" integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw== +fast-url-parser@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" + integrity sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0= + dependencies: + punycode "^1.3.2" + fastest-stable-stringify@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz#9122d406d4c9d98bea644a6b6853d5874b87b028" @@ -9145,6 +9188,16 @@ fs-extra@^9.0.0: jsonfile "^6.0.1" universalify "^1.0.0" +fs-extra@^9.0.1: + version "9.0.1" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" + integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^1.0.0" + fs-minipass@^1.2.5: version "1.2.7" resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" @@ -12764,6 +12817,18 @@ mime-db@1.44.0: resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== + +mime-types@2.1.18: + version "2.1.18" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== + dependencies: + mime-db "~1.33.0" + mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.26" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" @@ -14253,7 +14318,7 @@ path-is-absolute@^1.0.0: resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -path-is-inside@^1.0.2: +path-is-inside@1.0.2, path-is-inside@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= @@ -14290,6 +14355,11 @@ path-to-regexp@0.1.7: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-to-regexp@2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45" + integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== + path-type@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -15189,7 +15259,7 @@ punycode@1.3.2: resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= -punycode@^1.2.4: +punycode@^1.2.4, punycode@^1.3.2: version "1.4.1" resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= @@ -15301,6 +15371,11 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" +range-parser@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= + range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -16705,6 +16780,20 @@ serve-favicon@^2.5.0: parseurl "~1.3.2" safe-buffer "5.1.1" +serve-handler@^6.1.3: + version "6.1.3" + resolved "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.3.tgz#1bf8c5ae138712af55c758477533b9117f6435e8" + integrity sha512-FosMqFBNrLyeiIDvP1zgO6YoTzFYHxLDEIavhlmQ+knB2Z7l1t+kGLHkZIDN7UVWqQAmKI3D20A6F6jo3nDd4w== + dependencies: + bytes "3.0.0" + content-disposition "0.5.2" + fast-url-parser "1.1.3" + mime-types "2.1.18" + minimatch "3.0.4" + path-is-inside "1.0.2" + path-to-regexp "2.2.1" + range-parser "1.2.0" + serve-index@^1.9.1: version "1.9.1" resolved "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239"