diff --git a/.eslintrc.js b/.eslintrc.js index da18d19a95..3681612b35 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,4 @@ module.exports = { - extends: [ - require.resolve('@backstage/cli/config/eslint'), - '@spotify/eslint-config-oss', - ], + root: true, + extends: ['@spotify/eslint-config-oss'], }; diff --git a/packages/backend-common/.eslintrc.js b/packages/backend-common/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/packages/backend-common/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/packages/backend-common/README.md b/packages/backend-common/README.md new file mode 100644 index 0000000000..32bdd3d2a8 --- /dev/null +++ b/packages/backend-common/README.md @@ -0,0 +1,38 @@ +# @backstage/backend-common + +Common functionality library for Backstage backends, implementing logging, +error handling and similar. + +## Usage + +Add the library to your backend package: + +```sh +yarn add @backstage/backend-common +``` + +then make use of the handlers and logger as necessary: + +```typescript +import { + errorHandler, + getRootLogger, + notFoundHandler, + requestLoggingHandler, +} from '@backstage/backend-common'; + +const app = express(); +app.use(requestLoggingHandler()); +app.use('/home', myHomeRouter); +app.use(errorHandler()); +app.use(notFoundHandler()); + +app.listen(PORT, () => { + getRootLogger().info(`Listening on port ${PORT}`); +}); +``` + +## Documentation + +- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md) +- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json new file mode 100644 index 0000000000..eec21063ce --- /dev/null +++ b/packages/backend-common/package.json @@ -0,0 +1,47 @@ +{ + "name": "@backstage/backend-common", + "description": "Common functionality library for Backstage backends", + "version": "0.1.1-alpha.4", + "main": "dist", + "private": false, + "publishConfig": { + "access": "public" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/spotify/backstage", + "directory": "packages/backend-common" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "scripts": { + "build": "backstage-cli build-cache -- tsc", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "clean": "backstage-cli clean" + }, + "dependencies": { + "express": "^4.17.1", + "morgan": "^1.10.0", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.4", + "@types/express": "^4.17.6", + "@types/http-errors": "^1.6.3", + "@types/morgan": "^1.9.0", + "@types/supertest": "^2.0.8", + "get-port": "^5.1.1", + "http-errors": "^1.7.3", + "jest": "^25.1.0", + "jest-fetch-mock": "^3.0.3", + "supertest": "^4.0.2", + "typescript": "^3.8.3" + }, + "files": [ + "dist" + ] +} diff --git a/packages/core/src/api/widgetView/types.ts b/packages/backend-common/src/index.ts similarity index 76% rename from packages/core/src/api/widgetView/types.ts rename to packages/backend-common/src/index.ts index 9e066225ab..54b9f5c40f 100644 --- a/packages/core/src/api/widgetView/types.ts +++ b/packages/backend-common/src/index.ts @@ -14,13 +14,5 @@ * limitations under the License. */ -import { ComponentType } from 'react'; - -export type Widget = { - size: 4 | 6 | 8 | 12; - component: ComponentType; -}; - -export type WidgetViewProps = { - widgets: Widget[]; -}; +export * from './logging'; +export * from './middleware'; diff --git a/packages/core/src/components/DefaultWidgetView/index.ts b/packages/backend-common/src/logging/index.ts similarity index 92% rename from packages/core/src/components/DefaultWidgetView/index.ts rename to packages/backend-common/src/logging/index.ts index b4aaece4cb..06ce76ac54 100644 --- a/packages/core/src/components/DefaultWidgetView/index.ts +++ b/packages/backend-common/src/logging/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { default } from './DefaultWidgetView'; +export * from './rootLogger'; diff --git a/packages/backend-common/src/logging/rootLogger.test.ts b/packages/backend-common/src/logging/rootLogger.test.ts new file mode 100644 index 0000000000..cc7b174221 --- /dev/null +++ b/packages/backend-common/src/logging/rootLogger.test.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PassThrough } from 'stream'; +import winston from 'winston'; +import { getRootLogger, setRootLogger } from './rootLogger'; + +describe('rootLogger', () => { + it('can replace the default logger', () => { + const logger = winston.createLogger({ + transports: [ + new winston.transports.Stream({ stream: new PassThrough() }), + ], + }); + jest.spyOn(logger, 'info'); + + setRootLogger(logger); + getRootLogger().info('testing'); + + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('testing'), + ); + }); +}); diff --git a/packages/backend-common/src/logging/rootLogger.ts b/packages/backend-common/src/logging/rootLogger.ts new file mode 100644 index 0000000000..8058e22947 --- /dev/null +++ b/packages/backend-common/src/logging/rootLogger.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import winston, { Logger } from 'winston'; + +let rootLogger: Logger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + format: + process.env.NODE_ENV === 'production' + ? winston.format.json() + : winston.format.combine( + winston.format.colorize(), + winston.format.timestamp(), + winston.format.simple(), + ), + defaultMeta: { service: 'backstage' }, + transports: [ + new winston.transports.Console({ + silent: + process.env.JEST_WORKER_ID !== undefined && !process.env.LOG_LEVEL, + }), + ], +}); + +export function getRootLogger(): Logger { + return rootLogger; +} + +export function setRootLogger(newLogger: Logger) { + rootLogger = newLogger; +} diff --git a/packages/backend-common/src/middleware/errorHandler.test.ts b/packages/backend-common/src/middleware/errorHandler.test.ts new file mode 100644 index 0000000000..f90794b07b --- /dev/null +++ b/packages/backend-common/src/middleware/errorHandler.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import createError from 'http-errors'; +import request from 'supertest'; +import { errorHandler } from './errorHandler'; + +describe('errorHandler', () => { + it('gives default code and message', async () => { + const app = express(); + app.use('/breaks', () => { + throw new Error('some message'); + }); + app.use(errorHandler()); + + const response = await request(app).get('/breaks'); + + expect(response.status).toBe(500); + expect(response.text).toBe('some message'); + }); + + it('takes code from StatusCodeError', async () => { + const app = express(); + app.use('/breaks', () => { + throw createError(432, 'Some Message'); + }); + app.use(errorHandler()); + + const response = await request(app).get('/breaks'); + + expect(response.status).toBe(432); + expect(response.text).toContain('Some Message'); + }); +}); diff --git a/packages/backend-common/src/middleware/errorHandler.ts b/packages/backend-common/src/middleware/errorHandler.ts new file mode 100644 index 0000000000..655d93ad91 --- /dev/null +++ b/packages/backend-common/src/middleware/errorHandler.ts @@ -0,0 +1,63 @@ +/* + * 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 { ErrorRequestHandler, NextFunction, Request, Response } from 'express'; + +/** + * Express middleware to handle errors during request processing. + * + * This is commonly the second to last middleware in the chain (before the + * notFoundHandler). + * + * Its primary purpose is not to do translation of business logic exceptions, + * but rather to be a gobal catch-all for uncaught "fatal" errors that are + * expected to result in a 500 error. However, it also does handle some common + * error types (such as http-error exceptions) and returns the enclosed status + * code accordingly. + * + * @returns An Express error request handler + */ +export function errorHandler(): ErrorRequestHandler { + /* eslint-disable @typescript-eslint/no-unused-vars */ + return ( + error: Error, + _request: Request, + response: Response, + _next: NextFunction, + ) => { + const status = getStatusCode(error); + const message = error.message; + response.status(status).send(message); + }; +} + +function getStatusCode(error: Error): number { + const knownStatusCodeFields = ['statusCode', 'status']; + + for (const field of knownStatusCodeFields) { + const statusCode = (error as any)[field]; + if ( + typeof statusCode === 'number' && + (statusCode | 0) === statusCode && // is whole integer + statusCode >= 100 && + statusCode <= 599 + ) { + return statusCode; + } + } + + return 500; +} diff --git a/packages/backend-common/src/middleware/index.ts b/packages/backend-common/src/middleware/index.ts new file mode 100644 index 0000000000..083b36c3e9 --- /dev/null +++ b/packages/backend-common/src/middleware/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './errorHandler'; +export * from './notFoundHandler'; +export * from './requestLoggingHandler'; diff --git a/packages/backend-common/src/middleware/notFoundHandler.test.ts b/packages/backend-common/src/middleware/notFoundHandler.test.ts new file mode 100644 index 0000000000..65858e8cc1 --- /dev/null +++ b/packages/backend-common/src/middleware/notFoundHandler.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import request from 'supertest'; +import { notFoundHandler } from './notFoundHandler'; + +describe('notFoundHandler', () => { + it('handles only missing routes', async () => { + const app = express(); + app.use('/exists', (_, res) => res.status(200).send()); + app.use(notFoundHandler()); + + const existsResponse = await request(app).get('/exists'); + const doesNotExistResponse = await request(app).get('/doesNotExist'); + + expect(existsResponse.status).toBe(200); + expect(doesNotExistResponse.status).toBe(404); + }); +}); diff --git a/packages/backend-common/src/middleware/notFoundHandler.ts b/packages/backend-common/src/middleware/notFoundHandler.ts new file mode 100644 index 0000000000..19dd130c64 --- /dev/null +++ b/packages/backend-common/src/middleware/notFoundHandler.ts @@ -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 { NextFunction, Request, RequestHandler, Response } from 'express'; + +/** + * Express middleware to handle requests for missing routes. + * + * Should be used as the very last handler in the chain, as it unconditionally + * returns a 404 status. + * + * @returns An Express request handler + */ +export function notFoundHandler(): RequestHandler { + /* eslint-disable @typescript-eslint/no-unused-vars */ + return (_request: Request, response: Response, _next: NextFunction) => { + response.status(404).send(); + }; +} diff --git a/packages/backend-common/src/middleware/requestLoggingHandler.test.ts b/packages/backend-common/src/middleware/requestLoggingHandler.test.ts new file mode 100644 index 0000000000..191949bebe --- /dev/null +++ b/packages/backend-common/src/middleware/requestLoggingHandler.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import { PassThrough } from 'stream'; +import request from 'supertest'; +import winston from 'winston'; +import { requestLoggingHandler } from './requestLoggingHandler'; + +describe('requestLoggingHandler', () => { + it('emits logs for each request', async () => { + const logger = winston.createLogger({ + transports: [ + new winston.transports.Stream({ stream: new PassThrough() }), + ], + }); + jest.spyOn(logger, 'info'); + + const app = express(); + app.use(requestLoggingHandler(logger)); + app.use('/exists1', (_, res) => res.status(200).send()); + app.use('/exists2', (_, res) => res.status(201).send()); + + const r = request(app); + await r.get('/exists1'); + await r.get('/exists2'); + + expect(logger.info).toHaveBeenCalledTimes(2); + expect(logger.info).toHaveBeenNthCalledWith( + 1, + expect.stringContaining('200'), + ); + expect(logger.info).toHaveBeenNthCalledWith( + 2, + expect.stringContaining('201'), + ); + }); +}); diff --git a/packages/backend-common/src/middleware/requestLoggingHandler.ts b/packages/backend-common/src/middleware/requestLoggingHandler.ts new file mode 100644 index 0000000000..6604ec245c --- /dev/null +++ b/packages/backend-common/src/middleware/requestLoggingHandler.ts @@ -0,0 +1,40 @@ +/* + * 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 { RequestHandler } from 'express'; +import { Logger } from 'winston'; +import morgan from 'morgan'; +import { getRootLogger } from '../logging'; + +/** + * Logs incoming requests. + * + * @param logger An optional logger to use. If not specified, the root logger will be used. + * @returns An Express request handler + */ +export function requestLoggingHandler(logger?: Logger): RequestHandler { + const actualLogger = (logger || getRootLogger()).child({ + type: 'incomingRequest', + }); + + return morgan('combined', { + stream: { + write(message: String) { + actualLogger.info(message); + }, + }, + }); +} diff --git a/packages/backend-common/src/setupTests.ts b/packages/backend-common/src/setupTests.ts new file mode 100644 index 0000000000..3fa7cb04b4 --- /dev/null +++ b/packages/backend-common/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +require('jest-fetch-mock').enableMocks(); diff --git a/packages/backend-common/tsconfig.json b/packages/backend-common/tsconfig.json new file mode 100644 index 0000000000..b463ac102f --- /dev/null +++ b/packages/backend-common/tsconfig.json @@ -0,0 +1,15 @@ +{ + "include": ["src"], + "compilerOptions": { + "baseUrl": "src", + "outDir": "dist", + "incremental": true, + "sourceMap": true, + "declaration": true, + "strict": true, + "target": "es5", + "module": "commonjs", + "esModuleInterop": true, + "types": ["node", "jest"] + } +} diff --git a/packages/backend/.eslintrc.js b/packages/backend/.eslintrc.js index f400a039e7..16a033dbc6 100644 --- a/packages/backend/.eslintrc.js +++ b/packages/backend/.eslintrc.js @@ -1,6 +1,3 @@ module.exports = { - rules: { - 'no-console': 0, // Permitted in console programs - 'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()' - }, + extends: [require.resolve('@backstage/cli/config/eslint.backend')], }; diff --git a/packages/backend/package.json b/packages/backend/package.json index 01614f1ce2..32be276ef2 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -15,6 +15,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/backend-common": "0.1.1-alpha.4", "@backstage/plugin-inventory-backend": "0.1.1-alpha.4", "@backstage/plugin-scaffolder-backend": "0.1.1-alpha.4", "compression": "^1.7.4", @@ -29,6 +30,7 @@ "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", "@types/helmet": "^0.0.45", + "jest": "^25.1.0", "tsc-watch": "^4.2.3", "typescript": "^3.8.3" }, diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 4aae1f244b..1a2c7b70e6 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -22,12 +22,18 @@ * Happy hacking! */ -import express from 'express'; -import cors from 'cors'; -import helmet from 'helmet'; -import compression from 'compression'; -import { testRouter } from './test'; +import { + errorHandler, + getRootLogger, + notFoundHandler, + requestLoggingHandler, +} from '@backstage/backend-common'; import { router as inventoryRouter } from '@backstage/plugin-inventory-backend'; +import compression from 'compression'; +import cors from 'cors'; +import express from 'express'; +import helmet from 'helmet'; +import { testRouter } from './test'; import { createScaffolder } from '@backstage/plugin-scaffolder-backend'; const DEFAULT_PORT = 7000; @@ -41,10 +47,13 @@ app.use(helmet()); app.use(cors()); app.use(compression()); app.use(express.json()); +app.use(requestLoggingHandler()); app.use('/test', testRouter); app.use('/inventory', inventoryRouter); app.use('/scaffolder', scaffolder); +app.use(errorHandler()); +app.use(notFoundHandler()); app.listen(PORT, () => { - console.log(`Listening on port ${PORT}`); + getRootLogger().info(`Listening on port ${PORT}`); }); diff --git a/packages/cli/config/eslint.backend.js b/packages/cli/config/eslint.backend.js new file mode 100644 index 0000000000..ef6e513cf5 --- /dev/null +++ b/packages/cli/config/eslint.backend.js @@ -0,0 +1,71 @@ +/* + * 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. + */ + +module.exports = { + extends: [ + '@spotify/eslint-config-base', + '@spotify/eslint-config-typescript', + 'prettier', + 'prettier/@typescript-eslint', + 'plugin:jest/recommended', + 'plugin:monorepo/recommended', + ], + parser: '@typescript-eslint/parser', + plugins: ['import'], + env: { + jest: true, + }, + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + }, + ignorePatterns: ['**/dist/**', '**/build/**'], + rules: { + 'no-console': 0, // Permitted in console programs + 'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()' + 'import/no-duplicates': 'warn', + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: false, + optionalDependencies: true, + peerDependencies: true, + bundledDependencies: true, + }, + ], + '@typescript-eslint/no-unused-vars': [ + 'warn', + { vars: 'all', args: 'after-used', ignoreRestSiblings: true }, + ], + }, + overrides: [ + { + files: ['*.test.*', 'src/setupTests.*', 'dev/**'], + rules: { + // Tests are allowed to import dev dependencies + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: true, + optionalDependencies: true, + peerDependencies: true, + bundledDependencies: true, + }, + ], + }, + }, + ], +}; diff --git a/packages/core/package.json b/packages/core/package.json index aa3efafa95..a5f7c35c16 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -53,8 +53,7 @@ "react-helmet": "5.2.1", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", - "react-sparklines": "^1.7.0", - "recompose": "0.30.0" + "react-sparklines": "^1.7.0" }, "files": [ "dist" diff --git a/packages/core/src/api/api.ts b/packages/core/src/api/api.ts index abaa28cbbf..cafd0af126 100644 --- a/packages/core/src/api/api.ts +++ b/packages/core/src/api/api.ts @@ -16,7 +16,6 @@ import ApiRef, { ApiRefConfig } from './apis/ApiRef'; import AppBuilder from './app/AppBuilder'; -import WidgetViewBuilder from './widgetView/WidgetViewBuilder'; import BackstagePlugin, { PluginConfig } from './plugin/Plugin'; export function createApp() { @@ -27,10 +26,6 @@ export function createApiRef(config: ApiRefConfig) { return new ApiRef(config); } -export function createWidgetView() { - return new WidgetViewBuilder(); -} - export function createPlugin(config: PluginConfig): BackstagePlugin { return new BackstagePlugin(config); } diff --git a/packages/core/src/api/app/AppBuilder.tsx b/packages/core/src/api/app/AppBuilder.tsx index ccc1303ccb..0af21f5c8d 100644 --- a/packages/core/src/api/app/AppBuilder.tsx +++ b/packages/core/src/api/app/AppBuilder.tsx @@ -21,6 +21,8 @@ import { App } from './types'; import BackstagePlugin from 'api/plugin/Plugin'; import { FeatureFlagsRegistryItem } from './FeatureFlags'; import { featureFlagsApiRef } from 'api/apis/definitions/featureFlags'; +import ErrorPage from '../../layout/ErrorPage'; + import { IconComponent, SystemIcons, @@ -115,7 +117,11 @@ export default class AppBuilder { let rendered = ( {routes} - 404 Not Found} /> + ( + + )} + /> ); diff --git a/packages/core/src/api/plugin/Plugin.tsx b/packages/core/src/api/plugin/Plugin.tsx index 47f8331e10..a8b694ae8d 100644 --- a/packages/core/src/api/plugin/Plugin.tsx +++ b/packages/core/src/api/plugin/Plugin.tsx @@ -22,7 +22,6 @@ import { FeatureFlagName, } from './types'; import { validateBrowserCompat, validateFlagName } from 'api/app/FeatureFlags'; -import { Widget } from 'api/widgetView/types'; export type PluginConfig = { id: string; @@ -31,7 +30,6 @@ export type PluginConfig = { export type PluginHooks = { router: RouterHooks; - widgets: WidgetHooks; featureFlags: FeatureFlagsHooks; }; @@ -49,10 +47,6 @@ export type RouterHooks = { ): void; }; -export type WidgetHooks = { - add(widget: Widget): void; -}; - export type FeatureFlagsHooks = { register(name: FeatureFlagName): void; }; @@ -88,11 +82,6 @@ export default class Plugin { outputs.push({ type: 'redirect-route', path, target, options }); }, }, - widgets: { - add(widget: Widget) { - outputs.push({ type: 'widget', widget }); - }, - }, featureFlags: { register(name) { validateBrowserCompat(); diff --git a/packages/core/src/api/plugin/types.ts b/packages/core/src/api/plugin/types.ts index 3192dfd095..c4ac426e5c 100644 --- a/packages/core/src/api/plugin/types.ts +++ b/packages/core/src/api/plugin/types.ts @@ -15,7 +15,6 @@ */ import { ComponentType } from 'react'; -import { Widget } from 'api/widgetView/types'; export type RouteOptions = { // Whether the route path must match exactly, defaults to true. @@ -38,11 +37,6 @@ export type RedirectRouteOutput = { options?: RouteOptions; }; -export type WidgetOutput = { - type: 'widget'; - widget: Widget; -}; - export type FeatureFlagName = string; export type FeatureFlagOutput = { @@ -53,5 +47,4 @@ export type FeatureFlagOutput = { export type PluginOutput = | RouteOutput | RedirectRouteOutput - | WidgetOutput | FeatureFlagOutput; diff --git a/packages/core/src/api/widgetView/WidgetViewBuilder.tsx b/packages/core/src/api/widgetView/WidgetViewBuilder.tsx deleted file mode 100644 index 0bf873df86..0000000000 --- a/packages/core/src/api/widgetView/WidgetViewBuilder.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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, { ComponentType } from 'react'; -import { AppComponentBuilder } from 'api/app/types'; -import { Widget } from './types'; -import BackstagePlugin from 'api/plugin/Plugin'; -import DefaultWidgetView from 'components/DefaultWidgetView'; - -type WidgetViewRegistration = - | { - type: 'component'; - widget: Widget; - } - | { - type: 'plugin'; - plugin: BackstagePlugin; - }; - -export default class WidgetViewBuilder extends AppComponentBuilder { - private readonly registrations = new Array(); - private output?: ComponentType; - - add(widget: Widget): WidgetViewBuilder { - this.registrations.push({ type: 'component', widget }); - return this; - } - - register(plugin: BackstagePlugin): WidgetViewBuilder { - this.registrations.push({ type: 'plugin', plugin }); - return this; - } - - build(): ComponentType { - if (this.output) { - return this.output; - } - - const widgets = new Array(); - - for (const reg of this.registrations) { - switch (reg.type) { - case 'component': - widgets.push(reg.widget); - break; - case 'plugin': - { - let added = false; - for (const output of reg.plugin.output()) { - if (output.type === 'widget') { - widgets.push(output.widget); - added = true; - } - } - if (!added) { - throw new Error( - `Plugin ${reg.plugin} was registered as widget provider, but did not provide any widgets`, - ); - } - } - break; - default: - throw new Error(`Unknown WidgetViewBuilder registration`); - } - } - - this.output = () => ; - return this.output; - } -} diff --git a/packages/core/src/components/DefaultWidgetView/DefaultWidgetView.tsx b/packages/core/src/components/DefaultWidgetView/DefaultWidgetView.tsx deleted file mode 100644 index 990ecb0e92..0000000000 --- a/packages/core/src/components/DefaultWidgetView/DefaultWidgetView.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { FC } from 'react'; -import { Grid, Paper, makeStyles, Theme } from '@material-ui/core'; -import { WidgetViewProps } from 'api/widgetView/types'; - -const useStyles = makeStyles(theme => ({ - root: { - padding: theme.spacing(2), - }, - widgetWrapper: { - padding: theme.spacing(2), - }, -})); - -const WidgetViewComponent: FC = ({ widgets }) => { - const classes = useStyles(); - - return ( -
- - {widgets.map(({ size, component: WidgetComponent }, index) => ( - - - - - - ))} - -
- ); -}; - -export default WidgetViewComponent; diff --git a/packages/core/src/components/SortableTable/SortableTable.js b/packages/core/src/components/SortableTable/SortableTable.tsx similarity index 54% rename from packages/core/src/components/SortableTable/SortableTable.js rename to packages/core/src/components/SortableTable/SortableTable.tsx index ab4e1af2de..4db3aee9ff 100644 --- a/packages/core/src/components/SortableTable/SortableTable.js +++ b/packages/core/src/components/SortableTable/SortableTable.tsx @@ -14,9 +14,7 @@ * limitations under the License. */ -import React from 'react'; -import { pure } from 'recompose'; -import PropTypes from 'prop-types'; +import React, { FC, CSSProperties, memo, useState, useEffect } from 'react'; import { Table, TableBody, @@ -28,11 +26,45 @@ import { Tooltip, } from '@material-ui/core'; +type Column = { + id: string; + label: string; + numeric?: boolean; + disablePadding: boolean; + style: CSSProperties; + // FIXME (@Koroeskohr) + sortValue: (obj: object) => any; +}; + +// XXX (@Koroeskohr): no idea what I did but this typechecks. Need answer for CellContentProps +type Row = { [name in string]: any }; + +type SortHandler = ( + event: React.MouseEvent, + property: string, +) => void; + +type Order = 'asc' | 'desc'; + +type EnhancedTableHeadProps = { + columns: Column[]; + onRequestSort: SortHandler; + order: Order; + orderBy: string; +}; + /** * Table header which supports sorting ascending and desc */ -const EnhancedTableHead = ({ columns, onRequestSort, order, orderBy }) => { - const createSortHandler = property => event => { +const EnhancedTableHead: FC = ({ + columns, + onRequestSort, + order, + orderBy, +}) => { + const createSortHandler = (property: string) => ( + event: React.MouseEvent, + ) => { onRequestSort(event, property); }; @@ -68,25 +100,15 @@ const EnhancedTableHead = ({ columns, onRequestSort, order, orderBy }) => { ); }; -EnhancedTableHead.propTypes = { - columns: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - numeric: PropTypes.bool, - disablePadding: PropTypes.bool, - style: PropTypes.object, - }), - ).isRequired, - onRequestSort: PropTypes.func.isRequired, - order: PropTypes.string.isRequired, - orderBy: PropTypes.string.isRequired, +type CellContentProps = { + // XXX (@Koroeskohr): what am I supposed to use here + data: any | any[]; }; /** * CellContent can be an array or a string */ -const CellContent = ({ data }) => { +const CellContent: FC = ({ data }) => { if (Array.isArray(data)) { return data.map((item, index) => ( @@ -98,11 +120,12 @@ const CellContent = ({ data }) => { return data; }; -CellContent.propTypes = { - data: PropTypes.any.isRequired, +type DataTableCellProps = { + column: Column; + row: Row; }; -const DataTableCell = ({ column, row }) => { +const DataTableCell: FC = ({ column, row }) => { return ( { }; const noop = () => {}; -const DataTableRow = pure(({ row, columns, handleRowClick, style }) => { - const onClick = event => (handleRowClick || noop)(event, row.id); +type DataTableRowProps = { + row: Row; + columns: Column[]; + handleRowClick?: (event: React.MouseEvent, rowId: string) => void; + style?: React.CSSProperties; +}; +const _DataTableRow: FC = ({ + row, + columns, + handleRowClick, + style, +}) => { + const onClick: React.MouseEventHandler = event => + (handleRowClick || noop)(event, row.id); return ( {columns.map(column => ( @@ -124,17 +159,18 @@ const DataTableRow = pure(({ row, columns, handleRowClick, style }) => { ))} ); -}); +}; +const DataTableRow = memo(_DataTableRow); /** * Table with sorting capabilites automatic rendering of cells * Note that the objects in props.data needs have an id property * The columns array defines which columns from the data to show. * - * @param {Array[Object]} data A list of data entries, where object properties must + * @param data A list of data entries, where object properties must * be strictly equal to column ids. * - * @param {Array[Object]} columns A list of columns with the following shape: + * @param columns A list of columns with the following shape: * { * // The column identifier must be strictly equal the relevant data entry * // key: @@ -154,17 +190,17 @@ const DataTableRow = pure(({ row, columns, handleRowClick, style }) => { * sortValue: (Object) => Any * } * - * @param {String} orderBy The column ID initially used for sorting + * @param orderBy The column ID initially used for sorting * - * @param {String} [dataVersion] A version identifier for the data which *must* + * @param dataVersion A version identifier for the data which *must* * be updated when the contents of the data changes. This can be used for * components where the same SortableTable element will be used to display * variable sets of data. * - * @param {Array[Object]} [footerData] A list of data entries to be placed in + * @param footerData A list of data entries to be placed in * the table footer, which will not be sorted. * - * @param {(String, Event) => Void} [onRowClicked] Get notified when a user clicks + * @param onRowClicked Get notified when a user clicks * on the row. The handler will receive the row id as the first argument, and * the synthetic click event as the second argument. * @@ -188,49 +224,38 @@ const DataTableRow = pure(({ row, columns, handleRowClick, style }) => { * ev.preventDefault();}}/>) * } * + * // XXX (@koroeskohr): supposedly this is leftover from your internal doc * @deprecated use shared/components/DataGrid */ -class SortableTable extends React.Component { - static propTypes = { - // TODO: figure out how to make id of the object requried while others are dynamic - data: PropTypes.arrayOf(PropTypes.object).isRequired, - orderBy: PropTypes.string.isRequired, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - onRowClicked: PropTypes.func, - dataVersion: PropTypes.string, - }; - constructor(props) { - super(props); - this.handleRowClick = this.handleRowClick.bind(this); +type SortableTableProps = { + data: Row[]; + footerData: Row[]; + orderBy: string; + columns: Column[]; + onRowClicked?: ( + id: string, + event: React.MouseEvent, + ) => void; + dataVersion: string; +}; - this.state = { - orderBy: props.orderBy, - order: 'asc', - data: props.data, - }; - } +type TableState = { + orderBy: string; + order: Order; + data: Row[]; +}; - handleRequestSort = (event, property) => { - event.preventDefault(); - const orderBy = property; - let order = 'desc'; - if (this.state.orderBy === property && this.state.order === 'desc') { - order = 'asc'; - } - this.updateData(this.state.data, orderBy, order); - }; +const SortableTable: FC = props => { + const [state, setState] = useState({ + orderBy: props.orderBy, + order: 'asc', + data: props.data, + }); - handleRowClick = (event, id) => { - if (this.props.onRowClicked) { - this.props.onRowClicked(id, event); - } - }; - - updateData = (data, orderBy, order) => { - const sortValueFn = ( - this.props.columns.filter(col => col.id === orderBy)[0] || {} - ).sortValue; + const updateData = (data: Row[], orderBy: string, order: Order) => { + const sortValueFn = (props.columns.find(col => col.id === orderBy) || {}) + .sortValue; const sortedData = data.slice().sort((a, b) => { const valueA = sortValueFn ? sortValueFn(a) : a[orderBy]; @@ -241,56 +266,70 @@ class SortableTable extends React.Component { if (valueB === '' || valueB === null) return -inc; return valueA < valueB ? -inc : inc; }); - this.setState({ data: sortedData, order, orderBy }); + setState({ data: sortedData, order, orderBy }); }; - UNSAFE_componentWillReceiveProps(props) { - if (props.dataVersion !== this.props.dataVersion) { - this.updateData(props.data, this.state.orderBy, this.state.order); + const handleRequestSort: SortHandler = (event, property) => { + event.preventDefault(); + const orderBy = property; + let order: Order = 'desc'; + if (state.orderBy === property && state.order === 'desc') { + order = 'asc'; } - } + updateData(state.data, orderBy, order); + }; - render() { - const { data, order, orderBy } = this.state; - const { columns, dataVersion, footerData } = this.props; - - let tableFoot = null; - if (footerData && footerData.length > 0) { - tableFoot = ( - - {footerData.map(row => ( - - ))} - - ); + const handleRowClick = (event: React.MouseEvent, id: string): void => { + if (props.onRowClicked) { + props.onRowClicked(id, event); } - return ( - - - - {data.map(row => ( - - ))} - - {tableFoot} -
+ }; + + useEffect(() => { + const { data } = props; + const { orderBy, order } = state; + updateData(data, orderBy, order); + }, [props.dataVersion]); + + const { data, order, orderBy } = state; + const { columns, dataVersion, footerData } = props; + + let tableFoot = null; + if (footerData && footerData.length > 0) { + tableFoot = ( + + {footerData.map(row => ( + + ))} + ); } -} + return ( + + + + {data.map(row => ( + + ))} + + {tableFoot} +
+ ); +}; export default SortableTable; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 62a89fd337..619e672072 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -22,6 +22,7 @@ export { default as ContentHeader } from './layout/ContentHeader/ContentHeader'; export { default as Header } from './layout/Header/Header'; export { default as HeaderLabel } from './layout/HeaderLabel'; export { default as InfoCard } from './layout/InfoCard'; +export { CardTab, TabbedCard } from './layout/TabbedCard'; export { default as ErrorBoundary } from './layout/ErrorBoundary'; export * from './layout/Sidebar'; export { default as HorizontalScrollGrid } from './components/HorizontalScrollGrid'; diff --git a/packages/core/src/layout/ErrorPage/ErrorPage.test.tsx b/packages/core/src/layout/ErrorPage/ErrorPage.test.tsx new file mode 100644 index 0000000000..1309fdf474 --- /dev/null +++ b/packages/core/src/layout/ErrorPage/ErrorPage.test.tsx @@ -0,0 +1,38 @@ +/* + * 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 ErrorPage from './ErrorPage'; +import { wrapInThemedTestApp } from '@backstage/test-utils'; + +describe('', () => { + it('should render with status code, status message and go back link', () => { + const rendered = render( + wrapInThemedTestApp( + {} }} + />, + ), + ); + rendered.getByText(/page not found/i); + rendered.getByText(/404/i); + rendered.getByText(/Looks like someone dropped the mic!/i); + expect(rendered.getByTestId('go-back-link')).toBeDefined(); + }); +}); diff --git a/packages/core/src/layout/ErrorPage/ErrorPage.tsx b/packages/core/src/layout/ErrorPage/ErrorPage.tsx new file mode 100644 index 0000000000..ea593dbb49 --- /dev/null +++ b/packages/core/src/layout/ErrorPage/ErrorPage.tsx @@ -0,0 +1,68 @@ +/* + * 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 { Typography, Link, Grid } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import { BackstageTheme } from '@backstage/theme'; +import MicDrop from './MicDrop'; + +interface IErrorPageProps { + status: string; + statusMessage: string; + history: { + goBack: () => void; + }; +} + +const useStyles = makeStyles(theme => ({ + container: { + padding: theme.spacing(8), + }, + title: { + paddingBottom: theme.spacing(5), + }, + subtitle: { + color: theme.palette.textSubtle, + }, +})); + +const ErrorPage = ({ status, statusMessage, history }: IErrorPageProps) => { + const classes = useStyles(); + + return ( + + + + + ERROR {status}: {statusMessage} + + + Looks like someone dropped the mic! + + + + Go back + + ... or if you think this is a bug, please file an{' '} + issue. + + + + ); +}; + +export default ErrorPage; diff --git a/packages/core/src/layout/ErrorPage/MicDrop.js b/packages/core/src/layout/ErrorPage/MicDrop.js new file mode 100644 index 0000000000..f951830574 --- /dev/null +++ b/packages/core/src/layout/ErrorPage/MicDrop.js @@ -0,0 +1,162 @@ +/* + * 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 { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ + micDrop: { + maxWidth: '60%', + bottom: 10, + right: 10, + position: 'absolute', + }, +}); + +const MicDrop = () => { + const classes = useStyles(); + return ( + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default MicDrop; diff --git a/packages/core/src/layout/ErrorPage/index.ts b/packages/core/src/layout/ErrorPage/index.ts new file mode 100644 index 0000000000..872e5cce2d --- /dev/null +++ b/packages/core/src/layout/ErrorPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { default } from './ErrorPage'; diff --git a/packages/core/src/layout/InfoCard/InfoCard.tsx b/packages/core/src/layout/InfoCard/InfoCard.tsx index d645b25aaa..fe2bbfa1ce 100644 --- a/packages/core/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core/src/layout/InfoCard/InfoCard.tsx @@ -52,9 +52,6 @@ const VARIANT_STYLES = { display: 'flex', flexDirection: 'column', }, - widget: { - height: 430, - }, fullHeight: { height: '100%', }, @@ -79,11 +76,6 @@ const VARIANT_STYLES = { }, }, cardContent: { - widget: { - overflowY: 'auto', - height: 332, - width: '100%', - }, fullHeight: { height: 'calc(100% - 50px)', }, diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index 1b70f4132a..4d695f2d61 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -21,7 +21,8 @@ "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", - "test": "backstage-cli test" + "test": "backstage-cli test", + "clean": "backstage-cli clean" }, "dependencies": { "@testing-library/jest-dom": "^4.2.4", diff --git a/packages/theme/.eslintrc.js b/packages/theme/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/packages/theme/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/inventory-backend/.eslintrc.js b/plugins/inventory-backend/.eslintrc.js index f400a039e7..16a033dbc6 100644 --- a/plugins/inventory-backend/.eslintrc.js +++ b/plugins/inventory-backend/.eslintrc.js @@ -1,6 +1,3 @@ module.exports = { - rules: { - 'no-console': 0, // Permitted in console programs - 'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()' - }, + extends: [require.resolve('@backstage/cli/config/eslint.backend')], }; diff --git a/plugins/tech-radar/.eslintrc.js b/plugins/tech-radar/.eslintrc.js index dd47f29781..13573efa9c 100644 --- a/plugins/tech-radar/.eslintrc.js +++ b/plugins/tech-radar/.eslintrc.js @@ -1,3 +1,3 @@ module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.js')], + extends: [require.resolve('@backstage/cli/config/eslint')], }; diff --git a/yarn.lock b/yarn.lock index d2278c1b17..67fb65c829 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3992,6 +3992,11 @@ "@types/tapable" "*" "@types/webpack" "*" +"@types/http-errors@^1.6.3": + version "1.6.3" + resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.6.3.tgz#619a55768eab98299e8f76747339f3373f134e69" + integrity sha512-4KCE/agIcoQ9bIfa4sBxbZdnORzRjIw8JNQPLfqoNv7wQl/8f8mRbW68Q8wBsQFoJkPUHGlQYZ9sqi5WpfGSEQ== + "@types/http-proxy-middleware@*": version "0.19.3" resolved "https://registry.npmjs.org/@types/http-proxy-middleware/-/http-proxy-middleware-0.19.3.tgz#b2eb96fbc0f9ac7250b5d9c4c53aade049497d03" @@ -4112,6 +4117,13 @@ resolved "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== +"@types/morgan@^1.9.0": + version "1.9.0" + resolved "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.0.tgz#342119ae57fe67d36b91537143fc5aef16c2479f" + integrity sha512-warrzirh5dlTMaETytBTKR886pRXwr+SMZD87ZE13gLMR8Pzz69SiYFkvoDaii78qGP1iyBIUYz5GiXyryO//A== + dependencies: + "@types/express" "*" + "@types/node@*", "@types/node@>= 8", "@types/node@^13.7.2": version "13.13.4" resolved "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz#1581d6c16e3d4803eb079c87d4ac893ee7501c2c" @@ -5300,7 +5312,7 @@ arrify@^1.0.1: resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= -asap@^2.0.0, asap@~2.0.3, asap@~2.0.6: +asap@^2.0.0, asap@~2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= @@ -5912,6 +5924,13 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" +basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + batch-processor@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8" @@ -6538,11 +6557,6 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -change-emitter@^0.1.2: - version "0.1.6" - resolved "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515" - integrity sha1-6LL+PX8at9aaMhma/5HqaTFAlRU= - character-entities-legacy@^1.0.0: version "1.1.4" resolved "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" @@ -6917,6 +6931,14 @@ color-string@^1.5.2: color-name "^1.0.0" simple-swizzle "^0.2.2" +color@3.0.x: + version "3.0.0" + resolved "https://registry.npmjs.org/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a" + integrity sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w== + dependencies: + color-convert "^1.9.1" + color-string "^1.5.2" + color@^3.0.0, color@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" @@ -7359,11 +7381,6 @@ core-js-pure@^3.0.0, core-js-pure@^3.0.1: resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== -core-js@^1.0.0: - version "1.2.7" - resolved "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" - integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= - core-js@^2.4.0, core-js@^2.5.0: version "2.6.11" resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" @@ -8202,7 +8219,7 @@ delegates@^1.0.0: resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= -depd@2.0.0: +depd@2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -9555,19 +9572,6 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fbjs@^0.8.1: - version "0.8.17" - resolved "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" - integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90= - dependencies: - core-js "^1.0.0" - isomorphic-fetch "^2.1.1" - loose-envify "^1.0.0" - object-assign "^4.1.0" - promise "^7.1.1" - setimmediate "^1.0.5" - ua-parser-js "^0.7.18" - fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -10166,6 +10170,11 @@ get-port@^4.2.0: resolved "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119" integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw== +get-port@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== + get-stdin@7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz#8d5de98f15171a125c5e516643c7a6d0ea8a96f6" @@ -10834,11 +10843,6 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^2.3.1: - version "2.5.5" - resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" - integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== - hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -11048,17 +11052,7 @@ http-errors@1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -http-errors@~1.7.2: +http-errors@^1.7.3, http-errors@~1.7.2: version "1.7.3" resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== @@ -11069,6 +11063,16 @@ http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + "http-parser-js@>=0.4.0 <0.4.11": version "0.4.10" resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" @@ -11976,7 +11980,7 @@ is-ssh@^1.3.0: dependencies: protocols "^1.1.0" -is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: +is-stream@^1.0.0, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= @@ -12094,14 +12098,6 @@ isobject@^4.0.0: resolved "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0" integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA== -isomorphic-fetch@^2.1.1: - version "2.2.1" - resolved "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" - integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk= - dependencies: - node-fetch "^1.0.1" - whatwg-fetch ">=0.10.0" - isstream@~0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -14644,6 +14640,17 @@ moment@2.24.0: resolved "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== +morgan@^1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.0.2" + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -14832,14 +14839,6 @@ node-fetch@2.6.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0: resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== -node-fetch@^1.0.1: - version "1.7.3" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" - integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== - dependencies: - encoding "^0.1.11" - is-stream "^1.0.1" - node-forge@0.9.0: version "0.9.0" resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" @@ -17076,13 +17075,6 @@ promise.series@^0.2.0: resolved "https://registry.npmjs.org/promise.series/-/promise.series-0.2.0.tgz#2cc7ebe959fc3a6619c04ab4dbdc9e452d864bbd" integrity sha1-LMfr6Vn8OmYZwEq029yeRS2GS70= -promise@^7.1.1: - version "7.3.1" - resolved "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" - integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== - dependencies: - asap "~2.0.3" - promise@^8.0.3: version "8.1.0" resolved "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz#697c25c3dfe7435dd79fcd58c38a135888eaf05e" @@ -17247,11 +17239,16 @@ qs@6.7.0: resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== -qs@^6.5.1, qs@^6.6.0: +qs@^6.5.1: version "6.9.3" resolved "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== +qs@^6.6.0: + version "6.9.1" + resolved "https://registry.npmjs.org/qs/-/qs-6.9.1.tgz#20082c65cb78223635ab1a9eaca8875a29bf8ec9" + integrity sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA== + qs@~6.5.2: version "6.5.2" resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -17558,7 +17555,7 @@ react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-i resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4: +react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== @@ -17982,18 +17979,6 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -recompose@0.30.0: - version "0.30.0" - resolved "https://registry.npmjs.org/recompose/-/recompose-0.30.0.tgz#82773641b3927e8c7d24a0d87d65aeeba18aabd0" - integrity sha512-ZTrzzUDa9AqUIhRk4KmVFihH0rapdCSMFXjhHbNrjAWxBuUD/guYlyysMnuHjlZC/KRiOKRtB4jf96yYSkKE8w== - dependencies: - "@babel/runtime" "^7.0.0" - change-emitter "^0.1.2" - fbjs "^0.8.1" - hoist-non-react-statics "^2.3.1" - react-lifecycles-compat "^3.0.2" - symbol-observable "^1.0.4" - recursive-readdir@2.2.2, recursive-readdir@^2.2.2: version "2.2.2" resolved "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" @@ -18917,7 +18902,7 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -setimmediate@^1.0.4, setimmediate@^1.0.5: +setimmediate@^1.0.4: version "1.0.5" resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= @@ -19968,7 +19953,7 @@ svgo@^1.0.0, svgo@^1.2.2: unquote "~1.1.1" util.promisify "~1.0.0" -symbol-observable@^1.0.4, symbol-observable@^1.1.0: +symbol-observable@^1.1.0: version "1.2.0" resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== @@ -20630,11 +20615,6 @@ typescript@^3.7.4, typescript@^3.8.3: resolved "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== -ua-parser-js@^0.7.18: - version "0.7.21" - resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777" - integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ== - uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" @@ -21371,7 +21351,7 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" -whatwg-fetch@3.0.0, whatwg-fetch@>=0.10.0, whatwg-fetch@^3.0.0: +whatwg-fetch@3.0.0, whatwg-fetch@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==