diff --git a/packages/app/package.json b/packages/app/package.json index cca428da76..5c4321f362 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -5,6 +5,7 @@ "dependencies": { "@backstage/cli": "^0.1.1-alpha.5", "@backstage/core": "^0.1.1-alpha.5", + "@backstage/plugin-circleci": "^0.1.1-alpha.4", "@backstage/plugin-catalog": "^0.1.1-alpha.5", "@backstage/plugin-explore": "^0.1.1-alpha.5", "@backstage/plugin-home-page": "^0.1.1-alpha.5", @@ -62,5 +63,14 @@ "last 1 safari version" ] }, - "license": "Apache-2.0" -} + "license": "Apache-2.0", + "proxy": { + "/circleci/api": { + "target": "https://circleci.com/api/v1.1", + "changeOrigin": true, + "pathRewrite": { + "^/circleci/api/": "/" + } + } + } +} \ No newline at end of file diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index 2ec865fac3..c3380215b3 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -32,6 +32,8 @@ import { import { techRadarApiRef, TechRadar } from '@backstage/plugin-tech-radar'; +import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci'; + const builder = ApiRegistry.builder(); export const alertApiForwarder = new AlertApiForwarder(); @@ -39,7 +41,7 @@ builder.add(alertApiRef, alertApiForwarder); export const errorApiForwarder = new ErrorApiForwarder(alertApiForwarder); builder.add(errorApiRef, errorApiForwarder); - +builder.add(circleCIApiRef, new CircleCIApi()); builder.add(featureFlagsApiRef, new FeatureFlags()); builder.add(lighthouseApiRef, new LighthouseRestApi('http://localhost:3003')); diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 1930acae92..021005a97a 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -20,4 +20,5 @@ export { plugin as CatalogPlugin } from '@backstage/plugin-catalog'; export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder'; export { plugin as TechRadar } from '@backstage/plugin-tech-radar'; export { plugin as Explore } from '@backstage/plugin-explore'; +export { plugin as Circleci } from '@backstage/plugin-circleci'; export { plugin as RegisterComponent } from '@backstage/plugin-register-component'; diff --git a/packages/cli/package.json b/packages/cli/package.json index c3d768eec2..56dd2febd7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -83,6 +83,7 @@ "@types/diff": "^4.0.2", "@types/fs-extra": "^8.1.0", "@types/html-webpack-plugin": "^3.2.2", + "@types/http-proxy": "^1.17.4", "@types/inquirer": "^6.5.0", "@types/mini-css-extract-plugin": "^0.9.1", "@types/node": "^13.7.2", diff --git a/packages/cli/src/commands/app/serve.ts b/packages/cli/src/commands/app/serve.ts index 182e338287..416f8f0151 100644 --- a/packages/cli/src/commands/app/serve.ts +++ b/packages/cli/src/commands/app/serve.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { serveBundle } from '../../lib/bundler'; import { Command } from 'commander'; +import { serveBundle } from '../../lib/bundler'; export default async (cmd: Command) => { const waitForExit = await serveBundle({ diff --git a/packages/cli/src/commands/plugin/diff/read.ts b/packages/cli/src/commands/plugin/diff/read.ts index 316cc46922..a05d7166e2 100644 --- a/packages/cli/src/commands/plugin/diff/read.ts +++ b/packages/cli/src/commands/plugin/diff/read.ts @@ -64,9 +64,11 @@ export async function readTemplate( templateDir: string, templateVars: any, ): Promise { - const templateFilePaths = await recursiveReadDir(templateDir).catch(error => { - throw new Error(`Failed to read template directory: ${error.message}`); - }); + const templateFilePaths = await recursiveReadDir(templateDir).catch( + (error) => { + throw new Error(`Failed to read template directory: ${error.message}`); + }, + ); const templateFiles = new Array(); for (const templateFile of templateFilePaths) { diff --git a/packages/cli/src/commands/plugin/serve.ts b/packages/cli/src/commands/plugin/serve.ts index 5700992644..8abbd92440 100644 --- a/packages/cli/src/commands/plugin/serve.ts +++ b/packages/cli/src/commands/plugin/serve.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { serveBundle } from '../../lib/bundler'; import { Command } from 'commander'; +import { serveBundle } from '../../lib/bundler'; export default async (cmd: Command) => { const waitForExit = await serveBundle({ diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index d09ad61ed5..f02bf6524f 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -157,7 +157,7 @@ function actionHandler( }; } -process.on('unhandledRejection', rejection => { +process.on('unhandledRejection', (rejection) => { if (rejection instanceof Error) { exitWithError(rejection); } else { diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index c7860c0f42..f24da5b0c8 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -48,7 +48,7 @@ export async function buildBundle(options: BuildOptions) { const previousFileSizes = await measureFileSizesBeforeBuild(paths.targetDist); await fs.emptyDir(paths.targetDist); - const { stats } = await build(compiler, isCi).catch(error => { + const { stats } = await build(compiler, isCi).catch((error) => { console.log(chalk.red('Failed to compile.\n')); throw new Error(`Failed to compile.\n${error.message || error}`); }); diff --git a/packages/core/src/components/CodeSnippet/CodeSnippet.tsx b/packages/core/src/components/CodeSnippet/CodeSnippet.tsx index c77dc5bd43..fc7c262882 100644 --- a/packages/core/src/components/CodeSnippet/CodeSnippet.tsx +++ b/packages/core/src/components/CodeSnippet/CodeSnippet.tsx @@ -31,7 +31,7 @@ const defaultProps = { showLineNumbers: false, }; -const CodeSnippet: FC = props => { +const CodeSnippet: FC = (props) => { const { text, language, showLineNumbers } = { ...defaultProps, ...props, diff --git a/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.test.jsx b/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.test.jsx index d7f1e2387a..36079854fd 100644 --- a/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.test.jsx +++ b/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.test.jsx @@ -25,7 +25,7 @@ describe('', () => { jest.spyOn(window.performance, 'now').mockReturnValue(5); jest .spyOn(window, 'requestAnimationFrame') - .mockImplementation(cb => cb(20)); + .mockImplementation((cb) => cb(20)); }); afterEach(() => { diff --git a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.test.jsx b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.test.jsx index b9aa962969..a6e27eb29e 100644 --- a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.test.jsx +++ b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.test.jsx @@ -36,7 +36,7 @@ describe('', () => { , ); const keys = Object.keys(metadata); - keys.forEach(value => { + keys.forEach((value) => { expect(getByText(startCase(value))).toBeInTheDocument(); expect(getByText(metadata[value])).toBeInTheDocument(); }); @@ -49,7 +49,7 @@ describe('', () => { ); const keys = Object.keys(metadata); - keys.forEach(value => { + keys.forEach((value) => { expect(getByText(startCase(value))).toBeInTheDocument(); expect(getByText(metadata[value].toString())).toBeInTheDocument(); }); @@ -61,10 +61,10 @@ describe('', () => { , ); const keys = Object.keys(metadata); - keys.forEach(value => { + keys.forEach((value) => { expect(getByText(startCase(value))).toBeInTheDocument(); }); - metadata.arrayField.forEach(value => { + metadata.arrayField.forEach((value) => { expect(getByText(value)).toBeInTheDocument(); }); }); @@ -85,7 +85,7 @@ describe('', () => { ); const keys = Object.keys(metadata.config); - keys.forEach(value => { + keys.forEach((value) => { expect( getByText(startCase(value), { exact: false }), ).toBeInTheDocument(); diff --git a/packages/core/src/components/Table/Table.tsx b/packages/core/src/components/Table/Table.tsx index 550353920c..44d3788443 100644 --- a/packages/core/src/components/Table/Table.tsx +++ b/packages/core/src/components/Table/Table.tsx @@ -150,6 +150,7 @@ const convertColumns = ( export interface TableColumn extends Column<{}> { highlight?: boolean; + width?: string; } export interface TableProps extends MaterialTableProps<{}> { diff --git a/packages/core/src/layout/InfoCard/InfoCard.tsx b/packages/core/src/layout/InfoCard/InfoCard.tsx index fb5a14e18e..a2585a3006 100644 --- a/packages/core/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core/src/layout/InfoCard/InfoCard.tsx @@ -126,6 +126,7 @@ type Props = { actions?: ReactNode; cardClassName?: string; actionsTopRight?: ReactNode; + className?: string; }; const InfoCard: FC = ({ @@ -142,6 +143,7 @@ const InfoCard: FC = ({ actions, cardClassName, actionsTopRight, + className, }) => { const classes = useStyles(); @@ -169,7 +171,7 @@ const InfoCard: FC = ({ } return ( - + {title && ( <> diff --git a/packages/core/src/layout/Sidebar/Intro.tsx b/packages/core/src/layout/Sidebar/Intro.tsx index d10bfad31a..595697c466 100644 --- a/packages/core/src/layout/Sidebar/Intro.tsx +++ b/packages/core/src/layout/Sidebar/Intro.tsx @@ -26,7 +26,7 @@ import { } from './config'; import { SidebarDivider } from './Items'; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles((theme) => ({ introCard: { color: '#b5b5b5', // XXX (@koroeskohr): should I be using a Mui theme variable? @@ -74,7 +74,7 @@ type IntroCardProps = { onClose: () => void; }; -export const IntroCard: FC = props => { +export const IntroCard: FC = (props) => { const classes = useStyles(); const { text, onClose } = props; const handleClose = () => onClose(); @@ -109,18 +109,15 @@ type SidebarIntroCardProps = { onDismiss: () => void; }; -const SidebarIntroCard: FC = props => { - const {text, onDismiss} = props - const [collapsing, setCollapsing] = useState(false) +const SidebarIntroCard: FC = (props) => { + const { text, onDismiss } = props; + const [collapsing, setCollapsing] = useState(false); const startDismissing = () => { - setCollapsing(true) - } + setCollapsing(true); + }; return ( - + ); }; @@ -141,10 +138,10 @@ export const SidebarIntro: FC = () => { }); const dismissStarred = () => { - setDismissedIntro(state => ({ ...state, starredItemsDismissed: true })); + setDismissedIntro((state) => ({ ...state, starredItemsDismissed: true })); }; const dismissRecentlyViewed = () => { - setDismissedIntro(state => ({ + setDismissedIntro((state) => ({ ...state, recentlyViewedItemsDismissed: true, })); @@ -158,12 +155,18 @@ export const SidebarIntro: FC = () => { <> {!starredItemsDismissed && ( <> - + )} {!recentlyViewedItemsDismissed && ( - + )} ); diff --git a/plugins/catalog-backend/src/database/Database.ts b/plugins/catalog-backend/src/database/Database.ts index 39e075efb8..19fdd38ff0 100644 --- a/plugins/catalog-backend/src/database/Database.ts +++ b/plugins/catalog-backend/src/database/Database.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { NotFoundError } from '@backstage/backend-common'; import Knex from 'knex'; import { v4 as uuidv4 } from 'uuid'; +import { NotFoundError } from '../../../../packages/backend-common/src/errors'; import { AddDatabaseComponent, AddDatabaseLocation, diff --git a/plugins/circleci/.eslintrc.js b/plugins/circleci/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/circleci/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/circleci/README.md b/plugins/circleci/README.md new file mode 100644 index 0000000000..28d6b9c1c8 --- /dev/null +++ b/plugins/circleci/README.md @@ -0,0 +1,50 @@ +# CircleCI Plugin + +Website: [https://circleci.com/](https://circleci.com/) + + + + +## Setup + +0. If you have standalone app (you didn't clone this repo), then do + +```bash +yarn add @backstage/plugin-circleci +``` + +1. Add plugin API to your Backstage instance: + +```js +// packages/app/src/api.ts +import { ApiHolder } from '@backstage/core'; +import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci'; + +const builder = ApiRegistry.builder(); +builder.add(circleCIApiRef, new CircleCIApi(/* optional custom url for your own CircleCI instance */)); + +export default builder.build() as ApiHolder; +``` + +2. Add plugin itself: + +```js +// packages/app/src/plugins.ts +export { plugin as Circleci } from '@backstage/plugin-circleci'; +``` + +3. Run app with `yarn start` and navigate to `/circleci/settings` +4. Enter project settings and **project** token, acquired according to [https://circleci.com/docs/2.0/managing-api-tokens/](https://circleci.com/docs/2.0/managing-api-tokens/) + +## Features + +- List top 50 builds for a project +- Dive into one build to see logs +- Polling +- Retry builds + +## Limitations + +- Currently working only with project token (personal credentials WIP) +- Currently only top 50 builds in table view +- CircleCI has pretty strict rate limits per token, be careful with opened tabs diff --git a/plugins/circleci/dev/index.tsx b/plugins/circleci/dev/index.tsx new file mode 100644 index 0000000000..ed7dd5de9c --- /dev/null +++ b/plugins/circleci/dev/index.tsx @@ -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 { createDevApp } from '@backstage/dev-utils'; +import { plugin } from '../src/plugin'; +import { circleCIApiRef, CircleCIApi } from '../src/api'; + +createDevApp() + .registerPlugin(plugin) + .registerApiFactory({ + deps: {}, + factory: () => new CircleCIApi(), + implements: circleCIApiRef, + }) + .render(); diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json new file mode 100644 index 0000000000..297b06fb6d --- /dev/null +++ b/plugins/circleci/package.json @@ -0,0 +1,58 @@ +{ + "name": "@backstage/plugin-circleci", + "version": "0.1.1-alpha.4", + "main": "dist/index.esm.js", + "main:src": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "proxy": { + "/circleci/api": { + "target": "https://circleci.com/api/v1.1", + "changeOrigin": true, + "pathRewrite": { + "^/circleci/api/": "/" + } + } + }, + "scripts": { + "build": "backstage-cli plugin:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "clean": "backstage-cli clean", + "diff": "backstage-cli plugin:diff", + "start": "backstage-cli plugin:serve", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack" + }, + "dependencies": { + "@backstage/core": "^0.1.1-alpha.5", + "@backstage/theme": "^0.1.1-alpha.5", + "@material-ui/core": "^4.9.1", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "@types/react-lazylog": "^4.5.0", + "circleci-api": "^4.0.0", + "moment": "^2.25.3", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-lazylog": "^4.5.2", + "react-router": "^5.1.2", + "react-router-dom": "^5.1.2", + "react-use": "^14.2.0" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.5", + "@testing-library/jest-dom": "^5.7.0", + "@testing-library/react": "^9.3.2", + "@testing-library/user-event": "^10.2.4", + "@types/jest": "^25.2.1", + "@types/node": "^12.0.0", + "@types/testing-library__jest-dom": "^5.0.4", + "jest-fetch-mock": "^3.0.3", + "@backstage/dev-utils": "^0.1.1-alpha.5" + }, + "files": [ + "dist/**/*.{js,d.ts}" + ] +} diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts new file mode 100644 index 0000000000..f683f79ff9 --- /dev/null +++ b/plugins/circleci/src/api/index.ts @@ -0,0 +1,76 @@ +/* + * 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 { + CircleCIOptions, + getMe, + getBuildSummaries, + getFullBuild, + postBuildActions, + BuildAction, + BuildWithSteps, + BuildStepAction, + BuildSummary, + GitType, +} from 'circleci-api'; +import { ApiRef } from '@backstage/core'; + +export { BuildWithSteps, BuildStepAction, BuildSummary, GitType }; + +export const circleCIApiRef = new ApiRef({ + id: 'plugin.circleci.service', + description: 'Used by the CircleCI plugin to make requests', +}); + +export class CircleCIApi { + apiUrl: string; + constructor(apiUrl: string = '/circleci/api') { + this.apiUrl = apiUrl; + } + + async retry(buildNumber: number, options: CircleCIOptions) { + return postBuildActions(options.token, buildNumber, BuildAction.RETRY, { + circleHost: this.apiUrl, + ...options.vcs, + }); + } + + async getBuilds( + { limit = 10, offset = 0 }: { limit: number; offset: number }, + options: CircleCIOptions, + ) { + return getBuildSummaries(options.token, { + options: { + limit, + offset, + }, + vcs: {}, + circleHost: this.apiUrl, + ...options, + }); + } + + async getUser(options: CircleCIOptions) { + return getMe(options.token, { circleHost: this.apiUrl, ...options }); + } + + async getBuild(buildNumber: number, options: CircleCIOptions) { + return getFullBuild(options.token, buildNumber, { + circleHost: this.apiUrl, + ...options.vcs, + }); + } +} diff --git a/plugins/circleci/src/components/App.tsx b/plugins/circleci/src/components/App.tsx new file mode 100644 index 0000000000..1649c01b70 --- /dev/null +++ b/plugins/circleci/src/components/App.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Switch, Route, MemoryRouter } from 'react-router'; +import { BuildsPage, Builds } from '../pages/BuildsPage'; +import { DetailedViewPage, BuildWithSteps } from '../pages/BuildWithStepsPage'; +import { AppStateProvider } from '../state'; +import { Settings } from './Settings'; + +export const App = () => { + return ( + + <> + + + + + + + + ); +}; + +// TODO: allow pass in settings as props +// When some shared settings workflow +// will be established +export const CircleCIWidget = () => ( + + + <> + + + + + + + + +); diff --git a/plugins/circleci/src/components/Layout/Layout.tsx b/plugins/circleci/src/components/Layout/Layout.tsx new file mode 100644 index 0000000000..a614d388f0 --- /dev/null +++ b/plugins/circleci/src/components/Layout/Layout.tsx @@ -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 React from 'react'; +import { Header, Page, pageTheme, HeaderLabel } from '@backstage/core'; +import { Box } from '@material-ui/core'; + +export const Layout: React.FC = ({ children }) => { + return ( + +
+ Circle CI + + } + > + + +
+ {children} +
+ ); +}; diff --git a/plugins/circleci/src/components/Layout/index.ts b/plugins/circleci/src/components/Layout/index.ts new file mode 100644 index 0000000000..236fc98851 --- /dev/null +++ b/plugins/circleci/src/components/Layout/index.ts @@ -0,0 +1,16 @@ +/* + * 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 './Layout'; diff --git a/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx b/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx new file mode 100644 index 0000000000..4bab4b5921 --- /dev/null +++ b/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx @@ -0,0 +1,55 @@ +/* + * 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 { Link as RouterLink, useLocation } from 'react-router-dom'; +import { ContentHeader, SupportButton } from '@backstage/core'; +import { Button, IconButton, Box, Typography } from '@material-ui/core'; +import ArrowBack from '@material-ui/icons/ArrowBack'; +import SettingsIcon from '@material-ui/icons/Settings'; +import { useSettings } from '../../state'; + +export type Props = { title?: string }; +export const PluginHeader: FC = ({ title = 'Circle CI' }) => { + const [, { showSettings }] = useSettings(); + const location = useLocation(); + const notRoot = !location.pathname.match(/\/circleci\/?$/); + const isSettingsPage = location.pathname.match(/\/circleci\/settings\/?/); + return ( + ( + + {notRoot && ( + + + + )} + {title} + + )} + > + {!isSettingsPage && ( + + )} + + This plugin allows you to view and interact with your builds within the + Circle CI environment. + + + ); +}; diff --git a/plugins/circleci/src/components/PluginHeader/index.ts b/plugins/circleci/src/components/PluginHeader/index.ts new file mode 100644 index 0000000000..4de972f6f2 --- /dev/null +++ b/plugins/circleci/src/components/PluginHeader/index.ts @@ -0,0 +1,16 @@ +/* + * 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 './PluginHeader'; diff --git a/plugins/circleci/src/components/Settings/Settings.tsx b/plugins/circleci/src/components/Settings/Settings.tsx new file mode 100644 index 0000000000..e853bb46bb --- /dev/null +++ b/plugins/circleci/src/components/Settings/Settings.tsx @@ -0,0 +1,129 @@ +/* + * 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, { useState, useEffect } from 'react'; +import { + Button, + TextField, + List, + ListItem, + Snackbar, + Box, + Dialog, + DialogTitle, +} from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import { useSettings } from '../../state'; + +const Settings = () => { + const [ + { + repo: repoFromStore, + owner: ownerFromStore, + token: tokenFromStore, + showSettings, + }, + { saveSettings, hideSettings }, + ] = useSettings(); + + const [token, setToken] = useState(() => tokenFromStore); + const [owner, setOwner] = useState(() => ownerFromStore); + const [repo, setRepo] = useState(() => repoFromStore); + + useEffect(() => { + if (tokenFromStore !== token) { + setToken(tokenFromStore); + } + if (ownerFromStore !== owner) { + setOwner(ownerFromStore); + } + if (repoFromStore !== repo) { + setRepo(repoFromStore); + } + }, [ownerFromStore, repoFromStore, tokenFromStore]); + + const [saved, setSaved] = useState(false); + + return ( + <> + setSaved(false)} + > + Credentials saved. + + + + Project Credentials + {/* {authed ? : } */} + + + + + setToken(e.target.value)} + /> + + + setOwner(e.target.value)} + /> + + + setRepo(e.target.value)} + /> + + + + + + + + + + + ); +}; + +export default Settings; diff --git a/plugins/circleci/src/components/Settings/index.ts b/plugins/circleci/src/components/Settings/index.ts new file mode 100644 index 0000000000..c04ded6dea --- /dev/null +++ b/plugins/circleci/src/components/Settings/index.ts @@ -0,0 +1,16 @@ +/* + * 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 as Settings } from './Settings'; diff --git a/plugins/circleci/src/index.ts b/plugins/circleci/src/index.ts new file mode 100644 index 0000000000..fd0360b71c --- /dev/null +++ b/plugins/circleci/src/index.ts @@ -0,0 +1,21 @@ +/* + * 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'; +export * from './proxy'; +export * from './navTargets'; +export { CircleCIWidget } from './components/App'; diff --git a/plugins/circleci/src/navTargets.tsx b/plugins/circleci/src/navTargets.tsx new file mode 100644 index 0000000000..ce4553207e --- /dev/null +++ b/plugins/circleci/src/navTargets.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, { FC } from 'react'; +import { createNavTarget } from '@backstage/core'; +import { SvgIcon, SvgIconProps } from '@material-ui/core'; + +const CircleCIIcon: FC = (props) => ( + + + +); + +export const navTargetCircleCI = createNavTarget({ + icon: CircleCIIcon, + path: '/circleci', + title: 'CircleCI', +}); diff --git a/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx new file mode 100644 index 0000000000..8493913576 --- /dev/null +++ b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -0,0 +1,166 @@ +/* + * 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, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { Content, InfoCard, Progress } from '@backstage/core'; +import { BuildWithSteps, BuildStepAction } from '../../api'; +import { Grid, Box, Link, IconButton } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import { PluginHeader } from '../../components/PluginHeader'; +import { ActionOutput } from './lib/ActionOutput/ActionOutput'; +import { Layout } from '../../components/Layout'; +import LaunchIcon from '@material-ui/icons/Launch'; +import { useSettings } from '../../state/useSettings'; +import { useBuildWithSteps } from '../../state/useBuildWithSteps'; + +const IconLink = IconButton as typeof Link; +const BuildName: FC<{ build?: BuildWithSteps }> = ({ build }) => ( + + #{build?.build_num} - {build?.subject} + + + + +); +const useStyles = makeStyles((theme) => ({ + neutral: {}, + failed: { + position: 'relative', + '&:after': { + pointerEvents: 'none', + content: '""', + position: 'absolute', + top: 0, + right: 0, + left: 0, + bottom: 0, + boxShadow: `inset 4px 0px 0px ${theme.palette.error.main}`, + }, + }, + running: { + position: 'relative', + '&:after': { + pointerEvents: 'none', + content: '""', + position: 'absolute', + top: 0, + right: 0, + left: 0, + bottom: 0, + boxShadow: `inset 4px 0px 0px ${theme.palette.info.main}`, + }, + }, + cardContent: { + backgroundColor: theme.palette.background.default, + }, + success: { + position: 'relative', + '&:after': { + pointerEvents: 'none', + content: '""', + position: 'absolute', + top: 0, + right: 0, + left: 0, + bottom: 0, + boxShadow: `inset 4px 0px 0px ${theme.palette.success.main}`, + }, + }, +})); + +const pickClassName = ( + classes: ReturnType, + build: BuildWithSteps = {} as BuildWithSteps, +) => { + if (build.failed) return classes.failed; + if (['running', 'queued'].includes(build.status!)) return classes.running; + if (build.status === 'success') return classes.success; + + return classes.neutral; +}; + +const Page = () => ( + + + + + +); + +const BuildWithStepsView: FC<{}> = () => { + const { buildId = '' } = useParams(); + const classes = useStyles(); + const [settings] = useSettings(); + const [{ loading, value }, { startPolling, stopPolling }] = useBuildWithSteps( + parseInt(buildId, 10), + ); + + useEffect(() => { + startPolling(); + return () => stopPolling(); + }, [buildId, settings]); + + return ( + <> + + + + + } + cardClassName={classes.cardContent} + > + {loading ? : } + + + + + ); +}; + +const BuildsList: FC<{ build?: BuildWithSteps }> = ({ build }) => ( + + {build && + build.steps && + build.steps.map( + ({ name, actions }: { name: string; actions: BuildStepAction[] }) => ( + + ), + )} + +); + +const ActionsList: FC<{ actions: BuildStepAction[]; name: string }> = ({ + actions, +}) => { + const classes = useStyles(); + return ( + <> + {actions.map((action: BuildStepAction) => ( + + ))} + + ); +}; + +export default Page; +export { BuildWithStepsView as BuildWithSteps }; diff --git a/plugins/circleci/src/pages/BuildWithStepsPage/index.ts b/plugins/circleci/src/pages/BuildWithStepsPage/index.ts new file mode 100644 index 0000000000..fddff7088c --- /dev/null +++ b/plugins/circleci/src/pages/BuildWithStepsPage/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 { + default as DetailedViewPage, + BuildWithSteps, +} from './BuildWithStepsPage'; diff --git a/plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx b/plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx new file mode 100644 index 0000000000..e8e1bfe958 --- /dev/null +++ b/plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx @@ -0,0 +1,98 @@ +/* + * 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, { useEffect, useState, FC, Suspense } from 'react'; +import { + ExpansionPanel, + ExpansionPanelSummary, + Typography, + ExpansionPanelDetails, + LinearProgress, +} from '@material-ui/core'; +import moment from 'moment'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { makeStyles } from '@material-ui/core/styles'; +import { BuildStepAction } from 'circleci-api'; + +const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); +moment.relativeTimeThreshold('ss', 0); +const useStyles = makeStyles({ + expansionPanelDetails: { + padding: 0, + }, + button: { + order: -1, + marginRight: 0, + marginLeft: '-20px', + }, +}); + +export const ActionOutput: FC<{ + url: string; + name: string; + className?: string; + action: BuildStepAction; +}> = ({ url, name, className, action }) => { + const classes = useStyles(); + + const [messages, setMessages] = useState([]); + useEffect(() => { + fetch(url) + .then((res) => res.json()) + .then((actionOutput) => { + if (typeof actionOutput !== 'undefined') { + setMessages( + actionOutput.map(({ message }: { message: string }) => message), + ); + } + }); + }, [url]); + + const timeElapsed = moment + .duration( + moment(action.end_time || moment()).diff(moment(action.start_time)), + ) + .humanize(); + return ( + + } + aria-controls={`panel-${name}-content`} + id={`panel-${name}-header`} + IconButtonProps={{ + className: classes.button, + }} + > + + {name} ({timeElapsed}) + + + + {messages.length === 0 ? ( + 'Nothing here...' + ) : ( + }> +
+ +
+
+ )} +
+
+ ); +}; diff --git a/plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/index.ts b/plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/index.ts new file mode 100644 index 0000000000..7cf74c73f8 --- /dev/null +++ b/plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { ActionOutput } from './ActionOutput'; diff --git a/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx new file mode 100644 index 0000000000..7b124c03c1 --- /dev/null +++ b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx @@ -0,0 +1,43 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { FC } from 'react'; +import { Content } from '@backstage/core'; +import { Grid } from '@material-ui/core'; +import { Builds as BuildsComp } from './lib/Builds'; +import { Layout } from '../../components/Layout'; +import { PluginHeader } from '../../components/PluginHeader'; + +const BuildsPage: FC<{}> = () => ( + + + + + +); + +const Builds = () => ( + <> + + + + + + + +); + +export default BuildsPage; +export { Builds }; diff --git a/plugins/circleci/src/pages/BuildsPage/index.ts b/plugins/circleci/src/pages/BuildsPage/index.ts new file mode 100644 index 0000000000..72b46d6bc9 --- /dev/null +++ b/plugins/circleci/src/pages/BuildsPage/index.ts @@ -0,0 +1,16 @@ +/* + * 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 as BuildsPage, Builds } from './BuildsPage'; diff --git a/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx b/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx new file mode 100644 index 0000000000..bed96bb24c --- /dev/null +++ b/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.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, { FC } from 'react'; +import { CITable } from '../CITable'; +import { useBuilds } from '../../../../state'; + +export const Builds: FC<{}> = () => { + const [ + { total, loading, value, projectName, page, pageSize }, + { setPage, retry, setPageSize }, + ] = useBuilds(); + return ( + + ); +}; diff --git a/plugins/circleci/src/pages/BuildsPage/lib/Builds/index.ts b/plugins/circleci/src/pages/BuildsPage/lib/Builds/index.ts new file mode 100644 index 0000000000..e91a9496b7 --- /dev/null +++ b/plugins/circleci/src/pages/BuildsPage/lib/Builds/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { Builds } from './Builds'; diff --git a/plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx b/plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx new file mode 100644 index 0000000000..b349dcfcbe --- /dev/null +++ b/plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx @@ -0,0 +1,167 @@ +/* + * 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 { Link, Typography, Box, IconButton } from '@material-ui/core'; +import RetryIcon from '@material-ui/icons/Replay'; +import GitHubIcon from '@material-ui/icons/GitHub'; +import { Link as RouterLink } from 'react-router-dom'; +import { + StatusError, + StatusWarning, + StatusOK, + StatusPending, + StatusRunning, + Table, + TableColumn, +} from '@backstage/core'; + +export type CITableBuildInfo = { + id: string; + buildName: string; + buildUrl?: string; + source: { + branchName: string; + commit: { + hash: string; + url: string; + }; + }; + status: string; + tests?: { + total: number; + passed: number; + skipped: number; + failed: number; + testUrl: string; // fixme better name + }; + onRestartClick: () => void; +}; + +// retried, canceled, infrastructure_fail, timedout, not_run, running, failed, queued, scheduled, not_running, no_tests, fixed, success +const getStatusComponent = (status: string | undefined = '') => { + switch (status.toLowerCase()) { + case 'queued': + case 'scheduled': + return ; + case 'running': + return ; + case 'failed': + return ; + case 'success': + return ; + case 'canceled': + default: + return ; + } +}; + +const generatedColumns: TableColumn[] = [ + { + title: 'ID', + field: 'id', + type: 'numeric', + width: '80px', + }, + { + title: 'Build', + field: 'buildName', + highlight: true, + render: (row: Partial) => ( + + {row.buildName} + + ), + }, + { + title: 'Source', + render: (row: Partial) => ( + <> +

{row.source?.branchName}

+

{row.source?.commit.hash}

+ + ), + }, + { + title: 'Status', + render: (row: Partial) => ( + + {getStatusComponent(row.status)} + + {row.status} + + ), + }, + { + title: 'Actions', + render: (row: Partial) => ( + + + + ), + width: '10%', + }, +]; + +type Props = { + loading: boolean; + retry: () => void; + builds: CITableBuildInfo[]; + projectName: string; + page: number; + onChangePage: (page: number) => void; + total: number; + pageSize: number; + onChangePageSize: (pageSize: number) => void; +}; +export const CITable: FC = ({ + projectName, + loading, + pageSize, + page, + retry, + builds, + onChangePage, + onChangePageSize, + total, +}) => { + return ( + , + tooltip: 'Refresh Data', + isFreeAction: true, + onClick: () => retry(), + }, + ]} + data={builds} + onChangePage={onChangePage} + onChangeRowsPerPage={onChangePageSize} + title={ + + + + {projectName} + + } + columns={generatedColumns} + /> + ); +}; diff --git a/plugins/circleci/src/pages/BuildsPage/lib/CITable/index.ts b/plugins/circleci/src/pages/BuildsPage/lib/CITable/index.ts new file mode 100644 index 0000000000..358939e69f --- /dev/null +++ b/plugins/circleci/src/pages/BuildsPage/lib/CITable/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 { CITable } from './CITable'; +export type { CITableBuildInfo } from './CITable'; diff --git a/plugins/circleci/src/plugin.test.ts b/plugins/circleci/src/plugin.test.ts new file mode 100644 index 0000000000..821a503257 --- /dev/null +++ b/plugins/circleci/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { plugin } from './plugin'; + +describe('circleci', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts new file mode 100644 index 0000000000..1eceb60dae --- /dev/null +++ b/plugins/circleci/src/plugin.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createPlugin } from '@backstage/core'; +import { App } from './components/App'; +import { navTargetCircleCI } from './navTargets'; + +export const plugin = createPlugin({ + id: 'circleci', + register({ router }) { + router.addRoute(navTargetCircleCI, App, { exact: false }); + }, +}); diff --git a/plugins/circleci/src/proxy.ts b/plugins/circleci/src/proxy.ts new file mode 100644 index 0000000000..8a5ae460ab --- /dev/null +++ b/plugins/circleci/src/proxy.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export const proxySettings = { + '/circleci/api': { + target: 'https://circleci.com/api/v1.1', + changeOrigin: true, + logLevel: 'debug', + pathRewrite: { + '^/circleci/api/': '/', + }, + }, +}; diff --git a/plugins/circleci/src/setupTests.ts b/plugins/circleci/src/setupTests.ts new file mode 100644 index 0000000000..1a907ab8e6 --- /dev/null +++ b/plugins/circleci/src/setupTests.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom/extend-expect'; +require('jest-fetch-mock').enableMocks(); diff --git a/plugins/circleci/src/state/AppState.tsx b/plugins/circleci/src/state/AppState.tsx new file mode 100644 index 0000000000..49d55b602e --- /dev/null +++ b/plugins/circleci/src/state/AppState.tsx @@ -0,0 +1,56 @@ +/* + * 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, useReducer, Dispatch, Reducer } from 'react'; +import { circleCIApiRef } from '../api'; +import type { State, Action, SettingsState } from './types'; +export type { SettingsState }; + +export const AppContext = React.createContext<[State, Dispatch]>( + [] as any, +); +export const STORAGE_KEY = `${circleCIApiRef.id}.settings`; + +const initialState: State = { + owner: '', + repo: '', + token: '', + showSettings: false, +}; + +const reducer: Reducer = (state, action) => { + switch (action.type) { + case 'setCredentials': + return { + ...state, + ...action.payload, + }; + case 'showSettings': + return { ...state, showSettings: true }; + case 'hideSettings': + return { ...state, showSettings: false }; + default: + return state; + } +}; + +export const AppStateProvider: FC = ({ children }) => { + const [state, dispatch] = useReducer(reducer, initialState); + return ( + + <>{children} + + ); +}; diff --git a/plugins/circleci/src/state/index.ts b/plugins/circleci/src/state/index.ts new file mode 100644 index 0000000000..2321103eb4 --- /dev/null +++ b/plugins/circleci/src/state/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 './AppState'; +export * from './useSettings'; +export * from './useBuilds'; +export * from './useBuildWithSteps'; diff --git a/plugins/circleci/src/state/types.ts b/plugins/circleci/src/state/types.ts new file mode 100644 index 0000000000..41b3577082 --- /dev/null +++ b/plugins/circleci/src/state/types.ts @@ -0,0 +1,36 @@ +/* + * 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 type Settings = { owner: string; repo: string; token: string }; +export type SettingsState = Settings & { + showSettings: boolean; +}; + +export type State = SettingsState; + +type SettingsAction = + | { + type: 'setCredentials'; + payload: { + repo: string; + owner: string; + token: string; + }; + } + | { type: 'showSettings' } + | { type: 'hideSettings' }; + +export type Action = SettingsAction; diff --git a/plugins/circleci/src/state/useAsyncPolling.ts b/plugins/circleci/src/state/useAsyncPolling.ts new file mode 100644 index 0000000000..2f8de0c2fe --- /dev/null +++ b/plugins/circleci/src/state/useAsyncPolling.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 { useRef } from 'react'; + +export const useAsyncPolling = ( + pollingFn: () => Promise, + interval: number, +) => { + const isPolling = useRef(false); + const startPolling = async () => { + if (isPolling.current === true) return; + isPolling.current = true; + + while (isPolling.current === true) { + await pollingFn(); + await new Promise((resolve) => setTimeout(resolve, interval)); + } + }; + + const stopPolling = () => { + isPolling.current = false; + }; + return { startPolling, stopPolling }; +}; diff --git a/plugins/circleci/src/state/useBuildWithSteps.ts b/plugins/circleci/src/state/useBuildWithSteps.ts new file mode 100644 index 0000000000..7aba770851 --- /dev/null +++ b/plugins/circleci/src/state/useBuildWithSteps.ts @@ -0,0 +1,84 @@ +/* + * 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 { errorApiRef, useApi } from '@backstage/core'; +import { useCallback } from 'react'; +import { useAsyncRetry } from 'react-use'; +import { circleCIApiRef, GitType } from '../api/index'; +import { useAsyncPolling } from './useAsyncPolling'; +import { useSettings } from './useSettings'; + +const INTERVAL_AMOUNT = 1500; +export function useBuildWithSteps(buildId: number) { + const [{ token, repo, owner }] = useSettings(); + const api = useApi(circleCIApiRef); + const errorApi = useApi(errorApiRef); + + const getBuildWithSteps = useCallback(async () => { + if (owner === '' || repo === '' || token === '') { + return Promise.reject('No credentials provided'); + } + + try { + const options = { + token: token, + vcs: { + owner: owner, + repo: repo, + type: GitType.GITHUB, + }, + }; + const build = await api.getBuild(buildId, options); + return Promise.resolve(build); + } catch (e) { + errorApi.post(e); + return Promise.reject(e); + } + }, [token, owner, repo, buildId]); + + const restartBuild = async () => { + try { + await api.retry(buildId, { + token: token, + vcs: { + owner: owner, + repo: repo, + type: GitType.GITHUB, + }, + }); + } catch (e) { + errorApi.post(e); + } + }; + + const { loading, value, retry } = useAsyncRetry(() => getBuildWithSteps(), [ + getBuildWithSteps, + ]); + + const { startPolling, stopPolling } = useAsyncPolling( + getBuildWithSteps, + INTERVAL_AMOUNT, + ); + + return [ + { loading, value, retry }, + { + restartBuild, + getBuildWithSteps, + startPolling, + stopPolling, + }, + ] as const; +} diff --git a/plugins/circleci/src/state/useBuilds.ts b/plugins/circleci/src/state/useBuilds.ts new file mode 100644 index 0000000000..f90311b56b --- /dev/null +++ b/plugins/circleci/src/state/useBuilds.ts @@ -0,0 +1,153 @@ +/* + * 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 { errorApiRef, useApi } from '@backstage/core'; +import { BuildSummary, GitType } from 'circleci-api'; +import { useCallback, useEffect, useState } from 'react'; +import { useAsyncRetry } from 'react-use'; +import { circleCIApiRef } from '../api/index'; +import type { CITableBuildInfo } from '../pages/BuildsPage/lib/CITable'; +import { useSettings } from './useSettings'; + +const makeReadableStatus = (status: string | undefined) => { + if (!status) return ''; + return ({ + retried: 'Retried', + canceled: 'Canceled', + infrastructure_fail: 'Infra fail', + timedout: 'Timedout', + not_run: 'Not run', + running: 'Running', + failed: 'Failed', + queued: 'Queued', + scheduled: 'Scheduled', + not_running: 'Not running', + no_tests: 'No tests', + fixed: 'Fixed', + success: 'Success', + } as Record)[status]; +}; + +export const transform = ( + buildsData: BuildSummary[], + restartBuild: { (buildId: number): Promise }, +): CITableBuildInfo[] => { + return buildsData.map(buildData => { + const tableBuildInfo: CITableBuildInfo = { + id: String(buildData.build_num), + buildName: buildData.subject + ? buildData.subject + + (buildData.retry_of ? ` (retry of #${buildData.retry_of})` : '') + : '', + onRestartClick: () => + typeof buildData.build_num !== 'undefined' && + restartBuild(buildData.build_num), + source: { + branchName: String(buildData.branch), + commit: { + hash: String(buildData.vcs_revision), + url: 'todo', + }, + }, + status: makeReadableStatus(buildData.status), + buildUrl: buildData.build_url, + }; + return tableBuildInfo; + }); +}; + +export function useBuilds() { + const [{ repo, owner, token }] = useSettings(); + + const api = useApi(circleCIApiRef); + const errorApi = useApi(errorApiRef); + + const [total, setTotal] = useState(0); + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(5); + + const getBuilds = useCallback( + async ({ limit, offset }: { limit: number; offset: number }) => { + if (owner === '' || repo === '' || token === '') { + return Promise.reject('No credentials provided'); + } + + try { + return await api.getBuilds( + { limit, offset }, + { + token: token, + vcs: { + owner: owner, + repo: repo, + type: GitType.GITHUB, + }, + }, + ); + } catch (e) { + errorApi.post(e); + return Promise.reject(e); + } + }, + [repo, token, owner], + ); + + const restartBuild = async (buildId: number) => { + try { + await api.retry(buildId, { + token: token, + vcs: { + owner: owner, + repo: repo, + type: GitType.GITHUB, + }, + }); + } catch (e) { + errorApi.post(e); + } + }; + + useEffect(() => { + getBuilds({ limit: 1, offset: 0 }).then(b => setTotal(b?.[0].build_num!)); + }, [repo]); + + const { loading, value, retry } = useAsyncRetry( + () => + getBuilds({ + offset: page * pageSize, + limit: pageSize, + }).then(builds => transform(builds ?? [], restartBuild)), + [page, pageSize, getBuilds], + ); + + const projectName = `${owner}/${repo}`; + return [ + { + page, + pageSize, + loading, + value, + projectName, + total, + }, + { + getBuilds, + setPage, + setPageSize, + restartBuild, + retry, + }, + ] as const; +} diff --git a/plugins/circleci/src/state/useSettings.ts b/plugins/circleci/src/state/useSettings.ts new file mode 100644 index 0000000000..5482abeb8b --- /dev/null +++ b/plugins/circleci/src/state/useSettings.ts @@ -0,0 +1,66 @@ +/* + * 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 { errorApiRef, useApi } from '@backstage/core'; +import { useContext, useEffect } from 'react'; +import { AppContext, STORAGE_KEY } from './AppState'; +import { Settings } from './types'; + +export function useSettings() { + const [settings, dispatch] = useContext(AppContext); + + const errorApi = useApi(errorApiRef); + + const rehydrate = () => { + try { + const stateFromStorage = JSON.parse(sessionStorage.getItem(STORAGE_KEY)!); + if ( + stateFromStorage && + Object.keys(stateFromStorage).some( + (k) => (settings as any)[k] !== stateFromStorage[k], + ) + ) + dispatch({ + type: 'setCredentials', + payload: stateFromStorage, + }); + } catch (error) { + errorApi.post(error); + } + }; + + useEffect(() => { + rehydrate(); + }, []); + + const persist = (state: Settings) => { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + }; + + return [ + settings, + { + saveSettings: (state: Settings) => { + persist(state); + dispatch({ + type: 'setCredentials', + payload: state, + }); + }, + showSettings: () => dispatch({ type: 'showSettings' }), + hideSettings: () => dispatch({ type: 'hideSettings' }), + }, + ] as const; +} diff --git a/plugins/explore/src/components/ExplorePluginPage.tsx b/plugins/explore/src/components/ExplorePluginPage.tsx index 1dd2f135a4..00b8222688 100644 --- a/plugins/explore/src/components/ExplorePluginPage.tsx +++ b/plugins/explore/src/components/ExplorePluginPage.tsx @@ -64,6 +64,15 @@ const toolsCards = [ 'https://camo.githubusercontent.com/517398c3fbe0687d3d4dcbe05da82970b882e75a/68747470733a2f2f64337676366c703535716a6171632e636c6f756466726f6e742e6e65742f6974656d732f33413061324e314c3346324f304c3377326e316a2f477261706869514c382e706e673f582d436c6f75644170702d56697369746f722d49643d3433363432', tags: ['graphql', 'dev'], }, + { + title: 'CircleCI', + description: + 'Provides builds overview, detailed build info and retriggering functionality for CircleCI.', + url: '/circleci', + image: + 'https://raw.githubusercontent.com/CircleCI-Public/cimg-base/master/img/circle-circleci.svg', + tags: ['circleci', 'ci', 'dev'], + }, ]; const ExplorePluginPage: FC<{}> = () => { diff --git a/plugins/graphiql/src/navTargets.tsx b/plugins/graphiql/src/navTargets.tsx index febb6be59a..2351780f2c 100644 --- a/plugins/graphiql/src/navTargets.tsx +++ b/plugins/graphiql/src/navTargets.tsx @@ -18,7 +18,7 @@ import React, { FC } from 'react'; import { createNavTarget } from '@backstage/core'; import { SvgIcon, SvgIconProps } from '@material-ui/core'; -const GraphiQLIcon: FC = props => ( +const GraphiQLIcon: FC = (props) => ( (theme => ({ +const useStyles = makeStyles((theme) => ({ form: { alignItems: 'flex-start', display: 'flex', diff --git a/yarn.lock b/yarn.lock index 0d3dc866db..ba8d3434d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2322,6 +2322,13 @@ prop-types "^15.7.2" react-is "^16.8.0" +"@mattiasbuelens/web-streams-polyfill@^0.2.0": + version "0.2.1" + resolved "https://registry.npmjs.org/@mattiasbuelens/web-streams-polyfill/-/web-streams-polyfill-0.2.1.tgz#d7c4aa94f98084ec0787be084d47167d62ea5f67" + integrity sha512-oKuFCQFa3W7Hj7zKn0+4ypI8JFm4ZKIoncwAC6wd5WwFW2sL7O1hpPoJdSWpynQ4DJ4lQ6MvFoVDmCLilonDFg== + dependencies: + "@types/whatwg-streams" "^0.0.7" + "@mdx-js/react@^1.0.0", "@mdx-js/react@^1.5.2": version "1.5.9" resolved "https://registry.npmjs.org/@mdx-js/react/-/react-1.5.9.tgz#31873ab097fbe58c61c7585fc0be64e83182b6df" @@ -3909,7 +3916,7 @@ "@types/http-proxy" "*" "@types/node" "*" -"@types/http-proxy@*": +"@types/http-proxy@*", "@types/http-proxy@^1.17.4": version "1.17.4" resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b" integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q== @@ -4107,6 +4114,14 @@ dependencies: "@types/react" "*" +"@types/react-lazylog@^4.5.0": + version "4.5.0" + resolved "https://registry.npmjs.org/@types/react-lazylog/-/react-lazylog-4.5.0.tgz#1677ac520f29ada2e95b8cd5f8ec54b897227278" + integrity sha512-yg0IhKtDRGqUUtXJwi9Y/sSmgX/gmVP0vu0cr5uAECmJnRQbAgsGoVUntQGtwH0Z7Y2iiTh+QW5LkOFsmtIisg== + dependencies: + "@types/react" "*" + immutable ">=3.8.2" + "@types/react-router-dom@^5.1.3": version "5.1.5" resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.5.tgz#7c334a2ea785dbad2b2dcdd83d2cf3d9973da090" @@ -4392,6 +4407,11 @@ "@types/webpack-sources" "*" source-map "^0.6.0" +"@types/whatwg-streams@^0.0.7": + version "0.0.7" + resolved "https://registry.npmjs.org/@types/whatwg-streams/-/whatwg-streams-0.0.7.tgz#28bfe73dc850562296367249c4b32a50db81e9d3" + integrity sha512-6sDiSEP6DWcY2ZolsJ2s39ZmsoGQ7KVwBDI3sESQsEm9P2dHTcqnDIHRZFRNtLCzWp7hCFGqYbw5GyfpQnJ01A== + "@types/yaml@^1.9.7": version "1.9.7" resolved "https://registry.npmjs.org/@types/yaml/-/yaml-1.9.7.tgz#2331f36e0aac91311a63d33eb026c21687729679" @@ -5220,6 +5240,13 @@ aws4@^1.8.0: resolved "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== +axios@^0.19.0: + version "0.19.2" + resolved "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + axobject-query@^2.0.2: version "2.1.2" resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.1.2.tgz#2bdffc0371e643e5f03ba99065d5179b9ca79799" @@ -6325,6 +6352,13 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" +circleci-api@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/circleci-api/-/circleci-api-4.0.0.tgz#d773fe68f4a59e1968881269883a23b0805b3546" + integrity sha512-D/THFyhOv6THSkYXJhrOLIOmV7fmyDqgs1+pBFMAqDR+ywXszxa2Dqx1Zw+YD3O2zD2y5LQOifCDT96VidRG7Q== + dependencies: + axios "^0.19.0" + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -6503,7 +6537,7 @@ clone@^1.0.2: resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= -clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0: +clsx@^1.0.1, clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702" integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA== @@ -7663,7 +7697,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: dependencies: ms "2.0.0" -debug@3.1.0: +debug@3.1.0, debug@=3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== @@ -8062,6 +8096,14 @@ dom-converter@^0.2: dependencies: utila "~0.4" +dom-helpers@^5.0.0: + version "5.1.4" + resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz#4609680ab5c79a45f2531441f1949b79d6587f4b" + integrity sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^2.6.7" + dom-helpers@^5.0.1: version "5.1.3" resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.3.tgz#7233248eb3a2d1f74aafca31e52c5299cc8ce821" @@ -9153,6 +9195,11 @@ fecha@^2.3.3: resolved "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz#948e74157df1a32fd1b12c3a3c3cdcb6ec9d96cd" integrity sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg== +fetch-readablestream@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/fetch-readablestream/-/fetch-readablestream-0.2.0.tgz#eaa6d1a76b12de2d4731a343393c6ccdcfe2c795" + integrity sha512-qu4mXWf4wus4idBIN/kVH+XSer8IZ9CwHP+Pd7DL7TuKNC1hP7ykon4kkBjwJF3EMX2WsFp4hH7gU7CyL7ucXw== + figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" @@ -9393,6 +9440,13 @@ focus-lock@^0.6.6: resolved "https://registry.npmjs.org/focus-lock/-/focus-lock-0.6.6.tgz#98119a755a38cfdbeda0280eaa77e307eee850c7" integrity sha512-Dx69IXGCq1qsUExWuG+5wkiMqVM/zGx/reXSJSLogECwp3x6KeNQZ+NAetgxEFpnC41rD8U3+jRCW68+LNzdtw== +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + follow-redirects@^1.0.0: version "1.10.0" resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.10.0.tgz#01f5263aee921c6a54fb91667f08f4155ce169eb" @@ -10845,6 +10899,11 @@ immer@1.10.0: resolved "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== +immutable@>=3.8.2, immutable@^3.8.2: + version "3.8.2" + resolved "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" + integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM= + import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -13230,7 +13289,7 @@ longest@^2.0.1: resolved "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz#781e183296aa94f6d4d916dc335d0d17aefa23f8" integrity sha1-eB4YMpaqlPbU2RbcM10NF676I/g= -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -13870,6 +13929,11 @@ mississippi@^3.0.0: stream-each "^1.1.0" through2 "^2.0.0" +mitt@^1.1.2: + version "1.2.0" + resolved "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz#cb24e6569c806e31bd4e3995787fe38a04fdf90d" + integrity sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw== + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -13920,6 +13984,11 @@ moment@2.24.0: resolved "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== +moment@^2.25.3: + version "2.25.3" + resolved "https://registry.npmjs.org/moment/-/moment-2.25.3.tgz#252ff41319cf41e47761a1a88cab30edfe9808c0" + integrity sha512-PuYv0PHxZvzc15Sp8ybUCoQ+xpyPWvjOuK72a5ovzp2LI32rJXOiIfyoFoYvG3s6EwwrdkMyWuRiEHSZRLJNdg== + morgan@^1.10.0: version "1.10.0" resolved "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" @@ -16577,6 +16646,21 @@ 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-lazylog@^4.5.2: + version "4.5.2" + resolved "https://registry.npmjs.org/react-lazylog/-/react-lazylog-4.5.2.tgz#9b66a0997348690f56286f6afcda97ebd0c62b7c" + integrity sha512-XvAjlzs8tzbjmqyEdj8HwW8Kg5nfqZAzIGeeG1incZuhuXQkekIs6nYb3W/GQNxDpA1CowgNUR4UfP+7/C2Ang== + dependencies: + "@mattiasbuelens/web-streams-polyfill" "^0.2.0" + fetch-readablestream "^0.2.0" + immutable "^3.8.2" + mitt "^1.1.2" + prop-types "^15.6.1" + react-string-replace "^0.4.1" + react-virtualized "^9.21.0" + text-encoding-utf-8 "^1.0.1" + whatwg-fetch "^2.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" @@ -16628,7 +16712,7 @@ react-redux@^7.0.3: prop-types "^15.7.2" react-is "^16.9.0" -react-router-dom@^5.2.0: +react-router-dom@^5.1.2, react-router-dom@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA== @@ -16641,7 +16725,7 @@ react-router-dom@^5.2.0: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@5.2.0, react-router@^5.2.0: +react-router@5.2.0, react-router@^5.1.2, react-router@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293" integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw== @@ -16681,6 +16765,13 @@ react-sparklines@^1.7.0: dependencies: prop-types "^15.5.10" +react-string-replace@^0.4.1: + version "0.4.4" + resolved "https://registry.npmjs.org/react-string-replace/-/react-string-replace-0.4.4.tgz#24006fbe0db573d5be583133df38b1a735cb4225" + integrity sha512-FAMkhxmDpCsGTwTZg7p/2v+/GTmxAp73so3fbSvlAcBBX36ujiGRNEaM/1u+jiYQrArhns+7eE92g2pi5E5FUA== + dependencies: + lodash "^4.17.4" + react-syntax-highlighter@^11.0.2: version "11.0.2" resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-11.0.2.tgz#4e3f376e752b20d2f54e4c55652fd663149e4029" @@ -16740,6 +16831,18 @@ react-use@^14.2.0: ts-easing "^0.2.0" tslib "^1.10.0" +react-virtualized@^9.21.0: + version "9.21.2" + resolved "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.21.2.tgz#02e6df65c1e020c8dbf574ec4ce971652afca84e" + integrity sha512-oX7I7KYiUM7lVXQzmhtF4Xg/4UA5duSA+/ZcAvdWlTLFCoFYq1SbauJT5gZK9cZS/wdYR6TPGpX/dqzvTqQeBA== + dependencies: + babel-runtime "^6.26.0" + clsx "^1.0.1" + dom-helpers "^5.0.0" + loose-envify "^1.3.0" + prop-types "^15.6.0" + react-lifecycles-compat "^3.0.4" + react@^16.0.0, react@^16.12.0, react@^16.13.1, react@^16.8.3: version "16.13.1" resolved "https://registry.npmjs.org/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" @@ -19119,6 +19222,11 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +text-encoding-utf-8@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" + integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== + text-extensions@^1.0.0: version "1.9.0" resolved "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" @@ -20318,6 +20426,11 @@ whatwg-fetch@3.0.0: resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== +whatwg-fetch@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" + integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== + whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"