diff --git a/.github/workflows/scaffolder.yml b/.github/workflows/scaffolder.yml new file mode 100644 index 0000000000..bbfd52009d --- /dev/null +++ b/.github/workflows/scaffolder.yml @@ -0,0 +1,33 @@ +name: Scaffolder + +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: plugins/scaffolder-backend/scripts/Cookiecutter.dockerfile + registry: docker.pkg.github.com + repository: ${{ github.repository }}/cookiecutter + username: ${{ github.actor }} + password: ${{ github.token }} + tag_with_ref: true + push: true 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 3f9765f5e2..9ab4a125a6 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 d99bc38ea9..0018a7e7b6 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -26,6 +26,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 f096159598..7c9de7c3bb 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 proxy from './plugins/proxy'; @@ -71,6 +72,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/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') 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/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/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 similarity index 100% rename from plugins/catalog-backend/scripts/mock-data rename to plugins/catalog-backend/scripts/mock-data.sh 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/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 similarity index 100% rename from plugins/scaffolder-backend/scripts/mock-data rename to plugins/scaffolder-backend/scripts/mock-data.sh 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", diff --git a/yarn.lock b/yarn.lock index a26245c3b4..35b06e3d21 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" @@ -5839,6 +5851,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" @@ -5929,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== @@ -6496,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" @@ -8130,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" @@ -8650,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" @@ -9136,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" @@ -12519,6 +12581,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" @@ -12770,6 +12837,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" @@ -14259,7 +14338,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= @@ -14296,6 +14375,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" @@ -15195,7 +15279,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= @@ -15260,6 +15344,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" @@ -15302,6 +15391,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" @@ -16706,6 +16800,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"