diff --git a/README.html b/README.html new file mode 100644 index 0000000000..e9a0e9c19e --- /dev/null +++ b/README.html @@ -0,0 +1,138 @@ + + + + + + +README.html + + + + + +
+headline +
headline
+
+ +

Backstage

+ +

License + +Discord +Code style +

+ +

What is Backstage?

+ +

Backstage is an open platform for building developer portals.

+ +

The philosophy behind Backstage is simple: Don’t expose your engineers to the full complexity of your infrastructure tooling. Engineers should be shipping code β€” not figuring out a whole new toolset every time they want to implement the basics. Backstage allows you add “stuff” (tooling, services, features, etc.) by adding a plugin, instead of building a new tool. This saves you work and avoids the need of your team to learn how to use and support yet another tool.

+ +

For more information go to backstage.io or join our Discord chatroom.

+ +

What problem does Backstage solve?

+ +

As companies grow, their infrastructure systems get messier. Backstage unifies all your infrastructure tooling, services, and documentation with a single, consistent UI.

+ +

This blog post provides more examples of how Backstage is used inside Spotify:

+ +

https://labs.spotify.com/2020/03/17/what-the-heck-is-backstage-anyway/

+ +

Project roadmap

+ +

We created Backstage about 4 years ago. While our internal version of Backstage has had the benefit of time to mature and evolve, the first iteration of our open source version is still nascent. We are envisioning three phases of the project and we have already begun work on various aspects of these phases:

+ + + +

Check out our Milestones and open RFCs how they relate to the three Phases outlined above.

+ +

Our vision for Backstage is for it to become the trusted standard toolbox (read: UX layer) for the open source infrastructure landscape. Think of it like Kubernetes for developer experience. We realize this is an ambitious goal. We can’t do it alone. If this sounds interesting or you’d like to help us shape our product vision, we’d love to talk. You can email me directly: alund@spotify.com.

+ +

Overview

+ +

The Backstage platform consists of a number of different components:

+ + + +

* not yet released

+ +
+overview +
overview
+
+ +

Getting started

+ +

To run a Backstage app, you will need to have the following installed:

+ + + +

After cloning this repo, open a terminal window and start the web app using the following commands from the project root:

+ +
yarn install
+yarn start
+
+ +

The final yarn start command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal.

+ +

And thats it! You are good to go πŸ‘

+ +

Next step

+ +

Take a look at the Getting Started guide to learn more about how to extend the functionality with Plugins.

+ +

Documentation

+ + + +

Contributing

+ +

We would love your help in building Backstage! See CONTRIBUTING for more information.

+ +

Community

+ + + +

Or, if you are an open source developer and are interested in joining our team, please reach out to foss-opportunities@spotify.com

+ +

License

+ +

Copyright 2020 Spotify AB.

+ +

Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0

+ + + diff --git a/packages/app/package.json b/packages/app/package.json index 39aea525c7..7b7cfebbe4 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -4,7 +4,6 @@ "private": true, "devDependencies": { "@testing-library/cypress": "^6.0.0", - "@types/jquery": "^3.3.34", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", @@ -15,12 +14,14 @@ "cross-env": "^7.0.0", "cypress": "^4.2.0", "eslint-plugin-cypress": "^2.10.3", + "http-proxy-middleware": "^1.0.3", "start-server-and-test": "^1.10.11" }, "dependencies": { "@backstage/cli": "^0.1.1-alpha.4", "@backstage/core": "^0.1.1-alpha.4", "@backstage/plugin-explore": "^0.1.1-alpha.4", + "@backstage/plugin-circleci": "^0.1.1-alpha.4", "@backstage/plugin-home-page": "^0.1.1-alpha.4", "@backstage/plugin-inventory": "^0.1.1-alpha.4", "@backstage/plugin-lighthouse": "^0.1.1-alpha.4", diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index a24a25e205..c00aee1f5c 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -36,6 +36,8 @@ import { loadSampleData, } from '@backstage/plugin-tech-radar'; +import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci'; + const builder = ApiRegistry.builder(); export const alertApiForwarder = new AlertApiForwarder(); @@ -43,7 +45,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 3669f83dba..463d5dc0d1 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -20,3 +20,4 @@ export { plugin as InventoryPlugin } from '@backstage/plugin-inventory'; 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'; diff --git a/packages/app/src/setupProxy.js b/packages/app/src/setupProxy.js new file mode 100644 index 0000000000..65ed973923 --- /dev/null +++ b/packages/app/src/setupProxy.js @@ -0,0 +1,7 @@ +const { proxySettings } = require('@backstage/plugin-circleci'); +const { createProxyMiddleware } = require('http-proxy-middleware'); + +module.exports = (/** @type {import('express').Application} */ app) => + Object.entries(proxySettings).forEach(([url, settings]) => + app.use(url, createProxyMiddleware(settings)), + ); 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..46d48e9411 --- /dev/null +++ b/plugins/circleci/README.md @@ -0,0 +1,6 @@ +# Title +Welcome to the circleci plugin! + +## Sub-section 1 + +## Sub-section 2 diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json new file mode 100644 index 0000000000..cf5c5f93c4 --- /dev/null +++ b/plugins/circleci/package.json @@ -0,0 +1,41 @@ +{ + "name": "@backstage/plugin-circleci", + "version": "0.1.1-alpha.4", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts", + "license": "Apache-2.0", + "private": true, + "scripts": { + "build": "backstage-cli plugin:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "clean": "backstage-cli clean" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.4", + "@testing-library/jest-dom": "^4.2.4", + "@testing-library/react": "^9.3.2", + "@testing-library/user-event": "^7.1.2", + "@types/jest": "^24.0.0", + "@types/node": "^12.0.0", + "@types/testing-library__jest-dom": "5.0.2", + "jest-fetch-mock": "^3.0.3" + }, + "dependencies": { + "@backstage/core": "^0.1.1-alpha.4", + "@backstage/theme": "^0.1.1-alpha.4", + "@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", + "react": "16.13.1", + "react-dom": "16.13.1", + "react-lazylog": "^4.5.2", + "react-router": "^5.1.2", + "react-use": "^13.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts new file mode 100644 index 0000000000..d1eabbe2d6 --- /dev/null +++ b/plugins/circleci/src/api/index.ts @@ -0,0 +1,112 @@ +/* + * 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 { CircleCI, GitType, CircleCIOptions } from 'circleci-api'; +import { ApiRef } from '@backstage/core'; + +const defaultOptions: Partial = { + circleHost: '/circleci/api', + vcs: { + type: GitType.GITHUB, + owner: 'CircleCITest3', + repo: 'circleci-test', + }, +}; +export const circleCIApiRef = new ApiRef({ + id: 'plugin.circleci.service', + description: 'Used by the CircleCI plugin to make requests', +}); + +export class CircleCIApi { + + private token: string = ''; + options: Partial; + + authed: boolean = false; + constructor(options?: Partial) { + this.options = Object.assign(Object.create(null), defaultOptions, options); + } + + setToken(token: string) { + this.token = token; + this.persistToken(); + } + + setVCSOptions(vcs: CircleCIOptions['vcs']) { + this.options.vcs = vcs; + this.persistVCSOptions(); + } + + async persistVCSOptions() { + const key = circleCIApiRef.id; + sessionStorage.setItem(key + '_options', JSON.stringify(this.options.vcs)); + } + + async restorePersistedSettings() { + if (this.authed) return Promise.resolve(); + const key = circleCIApiRef.id; + const persistedToken = sessionStorage.getItem(key); + let persistedVCSOptions: {} | undefined; + try { + persistedVCSOptions = JSON.parse( + sessionStorage.getItem(key + '_options') as string, + ); + } catch (e) {} + if (persistedToken && persistedVCSOptions) { + this.token = persistedToken; + this.options.vcs = persistedVCSOptions; + return Promise.resolve(); + } + return Promise.reject(); + } + + async persistToken() { + if (this.authed) return; + const key = circleCIApiRef.id; + sessionStorage.setItem(key, this.token); + } + + async validateToken() { + if (!this.token || this.token === '') { + return Promise.reject('Wrong token'); + } + + // TODO: switch towards using personal token + await this.api.builds(); + this.authed = true; + return Promise.resolve(); + } + + private get api() { + return new CircleCI({ ...this.options, token: this.token }); + } + + async retry(buildId: string) { + return this.api.retry(Number(buildId)); + } + + async getBuilds() { + return this.api.builds(); + } + + async getUser() { + return this.api.me(); + } + + async getBuild(buildId: string) { + return this.api.build(parseInt(buildId, 10)); + } +} diff --git a/plugins/circleci/src/components/ActionOutput/ActionOutput.tsx b/plugins/circleci/src/components/ActionOutput/ActionOutput.tsx new file mode 100644 index 0000000000..7cc92098a1 --- /dev/null +++ b/plugins/circleci/src/components/ActionOutput/ActionOutput.tsx @@ -0,0 +1,52 @@ +import React, { useEffect, useState, FC, Suspense } from 'react'; +import { + ExpansionPanel, + ExpansionPanelSummary, + Typography, + ExpansionPanelDetails, +} from '@material-ui/core'; + +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { BuildStepAction } from 'circleci-api'; + +const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); + +export const ActionOutput: FC<{ + url: string; + name: string; + action: BuildStepAction; +}> = ({ url, name }) => { + const [messages, setMessages] = useState([]); + useEffect(() => { + fetch(url) + .then(res => res.json()) + .then(actionOutput => { + actionOutput && + setMessages( + actionOutput.map(({ message }: { message: string }) => message), + ); + }); + }, [url]); + return ( + + } + aria-controls={`panel-${name}-content`} + id={`panel-${name}-header`} + > + {name} + + + {messages.length === 0 ? ( + 'Nothing here...' + ) : ( + +
+ +
+
+ )} +
+
+ ); +}; diff --git a/plugins/circleci/src/components/ActionOutput/index.ts b/plugins/circleci/src/components/ActionOutput/index.ts new file mode 100644 index 0000000000..1e4fd5dfac --- /dev/null +++ b/plugins/circleci/src/components/ActionOutput/index.ts @@ -0,0 +1 @@ +export { ActionOutput } from './ActionOutput'; diff --git a/plugins/circleci/src/components/CITable/CITable.tsx b/plugins/circleci/src/components/CITable/CITable.tsx new file mode 100644 index 0000000000..bd8be803a3 --- /dev/null +++ b/plugins/circleci/src/components/CITable/CITable.tsx @@ -0,0 +1,133 @@ +// Idea for this component to be somehow reusable representation of CI table view +import React, { FC } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { + Button, + Table, + TableBody, + TableCell, + TableHead, + TableContainer, + TableRow, + CircularProgress, +} from '@material-ui/core'; +import { Replay as RetryIcon } from '@material-ui/icons'; +import { Link } from 'react-router-dom'; +import { + StatusFailed, + StatusOK, + StatusPending, + StatusNA, +} from '@backstage/core'; + +const useStyles = makeStyles({ + table: { + minWidth: 650, + }, + avatar: { + height: 32, + width: 32, + borderRadius: '50%', + }, +}); + +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 + }; + onRetryClick: () => void; +}; + +// :retried, :canceled, :infrastructure_fail, :timedout, :not_run, :running, :failed, :queued, :scheduled, :not_running, :no_tests, :fixed, :success +const getStatusComponent = (status: string) => { + switch (status.toLowerCase()) { + case 'queued': + case 'scheduled': + return ; + case 'running': + return ; + case 'failed': + return ; + case 'success': + return ; + case 'canceled': + default: + return ; + } +}; + +export const CITableBuildRow: FC<{ build: CITableBuildInfo }> = ({ build }) => ( + + {build.id} + + {build.buildName} + + + {build.source.branchName} +
+ {build.source.commit.hash} +
+ {getStatusComponent(build.status)} + {build.tests && ( + + { + <> + {build.tests.passed}/{build.tests.total} ( + {build.tests.failed ? build.tests.failed + ', ' : ''} + {build.tests.skipped ? build.tests.skipped : ''}) + + } + + )} + + + +
+); + +export const CITableBuildHeadRow:FC<{isTestDataAvailable: boolean}> = ({isTestDataAvailable}) => ( + + ID + Build + Source + Status + {isTestDataAvailable && Tests} + Actions + +); + +export const CITable: FC<{ + builds: CITableBuildInfo[]; +}> = ({ builds }) => { + const classes = useStyles(); + const isTestDataAvailable = builds.some(build => build.tests); + return ( + + + + + {builds.map(build => ( + + ))} + +
+
+ ); +}; diff --git a/plugins/circleci/src/components/CITable/index.ts b/plugins/circleci/src/components/CITable/index.ts new file mode 100644 index 0000000000..279634bce6 --- /dev/null +++ b/plugins/circleci/src/components/CITable/index.ts @@ -0,0 +1 @@ +export { CITable, CITableBuildInfo } from './CITable'; diff --git a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx new file mode 100644 index 0000000000..92d4ca5ce2 --- /dev/null +++ b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx @@ -0,0 +1,115 @@ +/* + * 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 wr iting, 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 Alert from '@material-ui/lab/Alert'; +// import { Progress } from '@backstage/core'; + +import { BuildSummary } from 'circleci-api'; + +import { CITable, CITableBuildInfo } from '../CITable'; +import { circleCIApiRef } from 'api'; +import { useApi } from '@backstage/core'; + +// "lifecycle" : "finished", // :queued, :scheduled, :not_run, :not_running, :running or :finished +// "outcome" : "failed", // :canceled, :infrastructure_fail, :timedout, :failed, :no_tests or :success + +const makeReadableStatus = (status: string | undefined) => { + if (typeof status === 'undefined') 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]; +}; + +const transform = ( + buildsData: BuildSummary[], + api: typeof circleCIApiRef.T, +): 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})` : '') + : '', + onRetryClick: () => api.retry(String(buildData.build_num)), + source: { + branchName: String(buildData.branch), + commit: { + hash: String(buildData.vcs_revision), + url: 'todo', + }, + }, + status: makeReadableStatus(buildData.status), + buildUrl: buildData.build_url, + // tests: { + // failed: 0, + // passed: 10, + // skipped: 3, + // testUrl: 'nourlnow', + // total: 13, + // }, + }; + return tableBuildInfo; + }); +}; + +export const CircleCIFetch: FC<{}> = () => { + const [authed, setAuthed] = React.useState(false); + const [builds, setBuilds] = React.useState([]); + const api = useApi(circleCIApiRef); + + React.useEffect(() => { + const intervalId = setInterval(async () => { + if (!authed) { + await api.restorePersistedSettings(); + await api + .validateToken() + .then(() => { + setAuthed(true); + }) + .catch(() => setAuthed(false)); + } + api.getBuilds().then(setBuilds); + }, 1500); + return () => clearInterval(intervalId); + }, [authed]); + + if (!authed) return
Not authenticated
; + const transformedBuilds = transform(builds || [], api); + return ( + <> + {!api.authed ? ( +
Not authenticated
+ ) : ( + + )} + + ); +}; diff --git a/plugins/circleci/src/components/CircleCIFetch/index.ts b/plugins/circleci/src/components/CircleCIFetch/index.ts new file mode 100644 index 0000000000..e204a791cd --- /dev/null +++ b/plugins/circleci/src/components/CircleCIFetch/index.ts @@ -0,0 +1 @@ +export { CircleCIFetch } from './CirleCIFetch'; diff --git a/plugins/circleci/src/components/Layout/Layout.tsx b/plugins/circleci/src/components/Layout/Layout.tsx new file mode 100644 index 0000000000..fe82bf90e8 --- /dev/null +++ b/plugins/circleci/src/components/Layout/Layout.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Header, Page, pageTheme, HeaderLabel } from '@backstage/core'; + +export const Layout: React.FC = ({ children }) => ( + +
+ + +
+ {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..9877e7f4ae --- /dev/null +++ b/plugins/circleci/src/components/Layout/index.ts @@ -0,0 +1 @@ +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..f725a979a9 --- /dev/null +++ b/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { ContentHeader, SupportButton } from '@backstage/core'; +import { Button } from '@material-ui/core'; +import { Settings as SettingsIcon } from '@material-ui/icons'; + +export const PluginHeader = () => ( + + + A description of your plugin goes here. + +); diff --git a/plugins/circleci/src/components/PluginHeader/index.ts b/plugins/circleci/src/components/PluginHeader/index.ts new file mode 100644 index 0000000000..e9231f1318 --- /dev/null +++ b/plugins/circleci/src/components/PluginHeader/index.ts @@ -0,0 +1 @@ +export * from './PluginHeader'; diff --git a/plugins/circleci/src/components/ProjectInput/ProjectInput.tsx b/plugins/circleci/src/components/ProjectInput/ProjectInput.tsx new file mode 100644 index 0000000000..046cd171b9 --- /dev/null +++ b/plugins/circleci/src/components/ProjectInput/ProjectInput.tsx @@ -0,0 +1,48 @@ +import { useState, FC, useEffect } from 'react'; +import { List, ListItem, TextField, Button } from '@material-ui/core'; +import React from 'react'; + +export const ProjectInput: FC<{ + setGitInfo: (info: { owner: string; repo: string }) => void; + apiGitInfo?: { owner?: string; repo?: string }; +}> = ({ setGitInfo, apiGitInfo = {} }) => { + const [owner, setOwner] = useState(''); + const [repo, setRepo] = useState(''); + + useEffect(() => { + if (apiGitInfo.owner !== owner && apiGitInfo.owner) + setOwner(apiGitInfo.owner); + if (apiGitInfo.repo !== repo && apiGitInfo.repo) setRepo(apiGitInfo.repo); + }, [apiGitInfo]); + + return ( + + + setOwner(e.target.value)} + /> + + + setRepo(e.target.value)} + /> + + + + + + ); +}; diff --git a/plugins/circleci/src/index.ts b/plugins/circleci/src/index.ts new file mode 100644 index 0000000000..33b8fe0067 --- /dev/null +++ b/plugins/circleci/src/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 { plugin } from './plugin'; +export * from './api'; +export * from './proxy'; diff --git a/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx new file mode 100644 index 0000000000..98e634d4c0 --- /dev/null +++ b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx @@ -0,0 +1,36 @@ +import React, { FC } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { + Content, + ContentHeader, + SupportButton, + InfoCard, +} from '@backstage/core'; +import { Button, Grid } from '@material-ui/core'; +import { CircleCIFetch } from 'components/CircleCIFetch'; +import { Settings as SettingsIcon } from '@material-ui/icons'; +import { Layout } from 'components/Layout'; + +export const BuildsPage: FC<{}> = () => ( + + + + + A description of your plugin goes here. + + + + + + + + + + +); diff --git a/plugins/circleci/src/pages/BuildsPage/index.ts b/plugins/circleci/src/pages/BuildsPage/index.ts new file mode 100644 index 0000000000..94086cd569 --- /dev/null +++ b/plugins/circleci/src/pages/BuildsPage/index.ts @@ -0,0 +1 @@ +export * from './BuildsPage'; diff --git a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx new file mode 100644 index 0000000000..37ff1e0655 --- /dev/null +++ b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx @@ -0,0 +1,77 @@ +import React, { FC } from 'react'; +import { Content, InfoCard, useApi } from '@backstage/core'; +import { Grid, Box } from '@material-ui/core'; +import { PluginHeader } from 'components/PluginHeader'; +import { BuildWithSteps, BuildStepAction } from 'circleci-api'; +import { circleCIApiRef } from 'api'; +import { useParams } from 'react-router-dom'; +import { ActionOutput } from '../../components/ActionOutput/ActionOutput'; +import { Layout } from 'components/Layout'; + +export const DetailedViewPage: FC<{}> = () => { + let { buildId = '' } = useParams(); + + const [authed, setAuthed] = React.useState(false); + const [build, setBuild] = React.useState(null); + const api = useApi(circleCIApiRef); + + React.useEffect(() => { + const getBuildAsync = async () => { + if (!authed) { + await api.restorePersistedSettings(); + await api + .validateToken() + .then(() => { + setAuthed(true); + }) + .catch(() => setAuthed(false)); + } + api.getBuild(buildId).then(setBuild); + }; + getBuildAsync(); + }, [authed, buildId]); + return ( + + + + {!api.authed ? ( +
Not authenticated
+ ) : ( + + + + + + + + )} +
+
+ ); +}; + +const BuildsList: FC<{ build: BuildWithSteps | null }> = ({ build }) => ( + + {build && + build.steps && + build.steps.map( + ({ name, actions }: { name: string; actions: BuildStepAction[] }) => ( + + ), + )} + +); + +const ActionsList: FC<{ actions: BuildStepAction[]; name: string }> = ({ + actions, +}) => ( + <> + {actions.map((action: BuildStepAction) => ( + + ))} + +); diff --git a/plugins/circleci/src/pages/DetailedViewPage/index.ts b/plugins/circleci/src/pages/DetailedViewPage/index.ts new file mode 100644 index 0000000000..578eec4e64 --- /dev/null +++ b/plugins/circleci/src/pages/DetailedViewPage/index.ts @@ -0,0 +1 @@ +export * from './DetailedViewPage'; diff --git a/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx new file mode 100644 index 0000000000..af380509d1 --- /dev/null +++ b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { Button, TextField, List, Grid, ListItem } from '@material-ui/core'; +import { circleCIApiRef } from 'api'; +import { + InfoCard, + useApi, + Content, + ContentHeader, + SupportButton, +} from '@backstage/core'; +import { ProjectInput } from 'components/ProjectInput/ProjectInput'; +import { Link as RouterLink } from 'react-router-dom'; +import { Layout } from 'components/Layout'; + +export const SettingsPage = () => { + const api = useApi(circleCIApiRef); + const [authed, setAuthed] = React.useState(api.authed); + const [token, setToken] = React.useState(''); + + React.useEffect(() => { + api + .restorePersistedSettings() + .then(() => api.validateToken()) + .then(() => setAuthed(true)) + .catch(() => setAuthed(false)); + }, []); + + return ( + + + + + A description of your plugin goes here. + + + + + + {authed ? ( + <>Already authed + ) : ( + <> + + setToken(e.target.value)} + /> + + + + + + + )} + + + + + + api.setVCSOptions(info)} + /> + + + + + + ); +}; diff --git a/plugins/circleci/src/pages/SettingsPage/index.ts b/plugins/circleci/src/pages/SettingsPage/index.ts new file mode 100644 index 0000000000..f533f5abe0 --- /dev/null +++ b/plugins/circleci/src/pages/SettingsPage/index.ts @@ -0,0 +1 @@ +export * from './SettingsPage'; 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..11d215f30f --- /dev/null +++ b/plugins/circleci/src/plugin.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createPlugin } from '@backstage/core'; +import { BuildsPage } from './pages/BuildsPage'; +import { SettingsPage } from './pages/SettingsPage'; +import { DetailedViewPage } from './pages/DetailedViewPage'; + +export const plugin = createPlugin({ + id: 'circleci', + register({ router }) { + router.registerRoute('/circleci', BuildsPage); + router.registerRoute('/circleci/build/:buildId', DetailedViewPage); + router.registerRoute('/circleci/settings', SettingsPage); + }, +}); diff --git a/plugins/circleci/src/proxy.ts b/plugins/circleci/src/proxy.ts new file mode 100644 index 0000000000..397b9b3c56 --- /dev/null +++ b/plugins/circleci/src/proxy.ts @@ -0,0 +1,12 @@ +import type {Options} from 'http-proxy-middleware'; + +export const proxySettings: Record = { + '/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/tsconfig.json b/plugins/circleci/tsconfig.json new file mode 100644 index 0000000000..91968a6e49 --- /dev/null +++ b/plugins/circleci/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "module": "esnext", + "baseUrl": "src" + } +} diff --git a/yarn.lock b/yarn.lock index ab56118174..f88dd930ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2545,6 +2545,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" @@ -4072,7 +4079,7 @@ "@types/http-proxy" "*" "@types/node" "*" -"@types/http-proxy@*": +"@types/http-proxy@*", "@types/http-proxy@^1.17.3": version "1.17.4" resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b" integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q== @@ -4141,13 +4148,6 @@ dependencies: "@types/sizzle" "*" -"@types/jquery@^3.3.34": - version "3.3.34" - resolved "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.34.tgz#0d3b94057063d3854adaeb579652048fec07ba6c" - integrity sha512-lW9vsVL53Xu/Nj4gi2hNmHGc4u3KKghjqTkAlO0kF5GIOPxbqqnQpgqJBzmn3yXLrPqHb6cmNJ6URnS23Vtvbg== - dependencies: - "@types/sizzle" "*" - "@types/js-cookie@2.2.5": version "2.2.5" resolved "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.5.tgz#38dfaacae8623b37cc0b0d27398e574e3fc28b1e" @@ -4282,6 +4282,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.3" resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.3.tgz#b5d28e7850bd274d944c0fbbe5d57e6b30d71196" @@ -4548,6 +4556,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/yargs-parser@*": version "15.0.0" resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" @@ -5388,6 +5401,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" @@ -6577,6 +6597,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" @@ -6748,7 +6775,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== @@ -7924,7 +7951,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== @@ -8311,6 +8338,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" @@ -9480,6 +9515,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" @@ -9705,6 +9745,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" @@ -10963,7 +11010,18 @@ http-proxy-middleware@0.19.1: lodash "^4.17.11" micromatch "^3.1.10" -http-proxy@^1.17.0: +http-proxy-middleware@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.0.3.tgz#f73daad8dac622d51fe1769960c914b9b1f75a72" + integrity sha512-GHvPeBD+A357zS5tHjzj6ISrVOjjCiy0I92bdyTJz0pNmIjFxO0NX/bX+xkGgnclKQE/5hHAB9JEQ7u9Pw4olg== + dependencies: + "@types/http-proxy" "^1.17.3" + http-proxy "^1.18.0" + is-glob "^4.0.1" + lodash "^4.17.15" + micromatch "^4.0.2" + +http-proxy@^1.17.0, http-proxy@^1.18.0: version "1.18.0" resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a" integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ== @@ -11113,6 +11171,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, import-cwd@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -13833,7 +13896,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== @@ -14442,6 +14505,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" @@ -17398,7 +17466,22 @@ 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.4: +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.2, 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== @@ -17562,6 +17645,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" @@ -17629,6 +17719,18 @@ react-use@^13.24.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.13.1, 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" @@ -20025,6 +20127,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" @@ -21172,6 +21279,11 @@ whatwg-fetch@3.0.0, 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"