Merge branch 'master' of github.com:spotify/backstage into shmidt-i/proxy-plugin
This commit is contained in:
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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!
|
||||
|
||||
Executable
+35
@@ -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/"
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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'));
|
||||
+15
-12
@@ -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;
|
||||
`);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
};
|
||||
@@ -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]
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<RollbarProject[]>('/projects').then(projects =>
|
||||
projects.filter(p => p.name),
|
||||
);
|
||||
}
|
||||
|
||||
async getProject(projectName: string) {
|
||||
return this.getForProject<RollbarProject>(
|
||||
`/project/:projectId`,
|
||||
projectName,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async getProjectItems(projectName: string) {
|
||||
return this.getForProject<RollbarItemsResponse>(
|
||||
`/items`,
|
||||
projectName,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
async getTopActiveItems(
|
||||
projectName: string,
|
||||
options: { hours: number; environment: string } = {
|
||||
hours: 24,
|
||||
environment: 'production',
|
||||
},
|
||||
) {
|
||||
return this.getForProject<RollbarTopActiveItem[]>(
|
||||
`/reports/top_active_items?${buildQuery(options)}`,
|
||||
projectName,
|
||||
);
|
||||
}
|
||||
|
||||
async getOccuranceCounts(
|
||||
projectName: string,
|
||||
options: { environment: string; item_id?: number } = {
|
||||
environment: 'production',
|
||||
},
|
||||
) {
|
||||
return this.getForProject<RollbarItemCount[]>(
|
||||
`/reports/occurrence_counts?${buildQuery(options as any)}`,
|
||||
projectName,
|
||||
);
|
||||
}
|
||||
|
||||
async getActivatedCounts(
|
||||
projectName: string,
|
||||
options: { environment: string; item_id?: number } = {
|
||||
environment: 'production',
|
||||
},
|
||||
) {
|
||||
return this.getForProject<RollbarItemCount[]>(
|
||||
`/reports/activated_counts?${buildQuery(options as any)}`,
|
||||
projectName,
|
||||
);
|
||||
}
|
||||
|
||||
private async getProjectAccessTokens(projectId: number) {
|
||||
return this.get<RollbarProjectAccessToken[]>(
|
||||
`/project/${projectId}/access_tokens`,
|
||||
);
|
||||
}
|
||||
|
||||
private async get<T>(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<T>(response?.data?.result, { deep: true }),
|
||||
);
|
||||
}
|
||||
|
||||
private async getForProject<T>(
|
||||
url: string,
|
||||
projectName: string,
|
||||
useProjectToken = true,
|
||||
) {
|
||||
const project = await this.getProjectMetadata(projectName);
|
||||
const resolvedUrl = url.replace(':projectId', project.id.toString());
|
||||
return this.get<T>(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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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[];
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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<RollbarApi>;
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<express.Router> {
|
||||
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;
|
||||
}
|
||||
@@ -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<Server> {
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -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 {};
|
||||
@@ -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('&');
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -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]
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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<RollbarApi>({
|
||||
id: 'plugin.rollbar.service',
|
||||
description:
|
||||
'Used by the Rollbar plugin to make requests to accompanying backend',
|
||||
});
|
||||
|
||||
export interface RollbarApi {
|
||||
getAllProjects(): Promise<RollbarProject[]>;
|
||||
getTopActiveItems(
|
||||
project: string,
|
||||
hours?: number,
|
||||
): Promise<RollbarTopActiveItem[]>;
|
||||
getProjectItems(project: string): Promise<RollbarItemsResponse>;
|
||||
}
|
||||
@@ -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<RollbarProject[]> {
|
||||
const path = `/projects`;
|
||||
|
||||
return await this.get(path);
|
||||
}
|
||||
|
||||
async getTopActiveItems(
|
||||
project: string,
|
||||
hours = 24,
|
||||
environment = 'production',
|
||||
): Promise<RollbarTopActiveItem[]> {
|
||||
const path = `/projects/${project}/top_active_items?environment=${environment}&hours=${hours}`;
|
||||
|
||||
return await this.get(path);
|
||||
}
|
||||
|
||||
async getProjectItems(project: string): Promise<RollbarItemsResponse> {
|
||||
const path = `/projects/${project}/items`;
|
||||
|
||||
return await this.get(path);
|
||||
}
|
||||
|
||||
private async get(path: string): Promise<any> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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<RollbarProject[]> {
|
||||
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<RollbarTopActiveItem[]> {
|
||||
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<RollbarItemsResponse> {
|
||||
return Promise.resolve({
|
||||
items: [],
|
||||
page: 0,
|
||||
totalCount: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
};
|
||||
@@ -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 (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
title="Rollbar"
|
||||
subtitle="Real-time error tracking & debugging tools for developers"
|
||||
/>
|
||||
<Content>
|
||||
<ContentHeader title={title}>
|
||||
<SupportButton>
|
||||
Rollbar plugin allows you to preview issues and navigate to rollbar.
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
{children}
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -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<RollbarApi> = {
|
||||
getAllProjects: () => Promise.resolve(projects),
|
||||
};
|
||||
|
||||
const renderWrapped = (children: React.ReactNode) =>
|
||||
render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={ApiRegistry.from([[rollbarApiRef, rollbarApi]])}>
|
||||
{children}
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
it('should render rollbar landing page', async () => {
|
||||
const rendered = renderWrapped(<RollbarPage />);
|
||||
expect(rendered.getByText(/Rollbar/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<RollbarLayout>
|
||||
<RollbarProjectTable loading={loading} projects={value || []} />
|
||||
</RollbarLayout>
|
||||
);
|
||||
};
|
||||
@@ -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<RollbarProject>) => (
|
||||
<Link component={RouterLink} to={`/rollbar/${row.name}`}>
|
||||
{row.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
field: 'status',
|
||||
type: 'string',
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
type Props = {
|
||||
projects: RollbarProject[];
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export const RollbarProjectTable = ({ projects, loading }: Props) => {
|
||||
return (
|
||||
<Table
|
||||
isLoading={loading}
|
||||
columns={columns}
|
||||
options={{
|
||||
padding: 'dense',
|
||||
paging: true,
|
||||
search: true,
|
||||
pageSize: 10,
|
||||
showEmptyDataSourceMessage: !loading,
|
||||
}}
|
||||
title="Projects"
|
||||
data={projects}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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<RollbarApi> = {
|
||||
getTopActiveItems: () => Promise.resolve(items),
|
||||
};
|
||||
|
||||
const renderWrapped = (children: React.ReactNode) =>
|
||||
render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={ApiRegistry.from([[rollbarApiRef, rollbarApi]])}>
|
||||
{children}
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
it('should render rollbar project page', async () => {
|
||||
const rendered = renderWrapped(<RollbarProjectPage />);
|
||||
expect(rendered.getByText(/Top Active Items/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<RollbarLayout>
|
||||
<RollbarTopItemsTable loading={loading} items={value || []} />
|
||||
</RollbarLayout>
|
||||
);
|
||||
};
|
||||
@@ -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(<RollbarTopItemsTable items={[]} loading={false} />),
|
||||
);
|
||||
expect(rendered.getByText(/No records to display/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display item attributes when loading has finished', async () => {
|
||||
const rendered = render(
|
||||
wrapInTestApp(<RollbarTopItemsTable items={items} loading={false} />),
|
||||
);
|
||||
expect(rendered.getByText(/1234/)).toBeInTheDocument();
|
||||
expect(rendered.getByText(/Error in foo/)).toBeInTheDocument();
|
||||
expect(rendered.getByText(/critical/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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 => (
|
||||
<RollbarTrendGraph counts={(data as RollbarTopActiveItem).counts} />
|
||||
),
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<Table
|
||||
isLoading={loading}
|
||||
columns={columns}
|
||||
options={{
|
||||
padding: 'dense',
|
||||
paging: false,
|
||||
search: true,
|
||||
showEmptyDataSourceMessage: !loading,
|
||||
}}
|
||||
title="Top Active Items"
|
||||
data={items}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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(
|
||||
<RollbarTrendGraph counts={mockCounts} data-testid="graph" />,
|
||||
),
|
||||
);
|
||||
expect(rendered).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<Sparklines data={counts} svgHeight={48} min={0} margin={4}>
|
||||
<SparklinesBars barWidth={2} />
|
||||
</Sparklines>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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();
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user