Merge branch 'circleci-plugin' into master

This commit is contained in:
Ivan Shmidt
2020-05-06 15:30:38 +02:00
committed by GitHub
33 changed files with 1133 additions and 15 deletions
+138
View File
@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>README.html</title>
</head>
<body>
<figure>
<img src="docs/headline.png" alt="headline" />
<figcaption>headline</figcaption>
</figure>
<h1 id="backstagehttps:backstage.io"><a href="https://backstage.io">Backstage</a></h1>
<p><a href="https://opensource.org/licenses/Apache-2.0"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License" /></a>
<img src="https://github.com/spotify/backstage/workflows/Frontend%20CI/badge.svg" alt="" />
<a href="https://discord.gg/EBHEGzX"><img src="https://img.shields.io/discord/687207715902193673" alt="Discord" /></a>
<img src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg" alt="Code style" />
<a href="https://github.com/spotify/backstage/releases"><img src="https://img.shields.io/npm/v/@backstage/core?label=Version" alt="" /></a></p>
<h2 id="whatisbackstage">What is Backstage?</h2>
<p>Backstage is an open platform for building developer portals.</p>
<p>The philosophy behind Backstage is simple: Don&#8217;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 &#8220;stuff&#8221; (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.</p>
<p>For more information go to <a href="https://backstage.io">backstage.io</a> or join our <a href="https://discord.gg/EBHEGzX">Discord chatroom</a>.</p>
<h2 id="whatproblemdoesbackstagesolve">What problem does Backstage solve?</h2>
<p>As companies grow, their infrastructure systems get messier. Backstage unifies all your infrastructure tooling, services, and documentation with a single, consistent UI.</p>
<p>This blog post provides more examples of how Backstage is used inside Spotify:</p>
<p>https://labs.spotify.com/2020/03/17/what-the-heck-is-backstage-anyway/</p>
<h2 id="projectroadmap">Project roadmap</h2>
<p>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:</p>
<ul>
<li><p>🐣 <strong>Phase 1:</strong> Extensible frontend platform (now) - You will be able to easily create a single consistent UI layer for your internal infrastructure and tools. A set of reusable UX patterns and components help ensure a consistent experience between tools.</p></li>
<li><p>🐢 <strong>Phase 2:</strong> Manage your stuff (next 2&#8211;3 months) - Manage anything from microservices to software components to infrastructure and your service catalog. Regardless of whether you want to create a new library, view service deployment status in Kubernetes, or check the test coverage for a website &#8211; Backstage will provide all of those tools - and many more - in a single developer portal.</p></li>
<li><p>🐇 <strong>Phase 3:</strong> Ecosystem (later) - Everyone&#8217;s infrastructure stack is different. By fostering a vibrant community of contributors we hope to provide an ecosystem of Open Source plugins/integrations that allows you to pick the tools that match your stack.</p></li>
</ul>
<p>Check out our <a href="https://github.com/spotify/backstage/milestones">Milestones</a> and open <a href="https://github.com/spotify/backstage/labels/rfc">RFCs</a> how they relate to the three Phases outlined above.</p>
<p>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 cant do it alone. If this sounds interesting or you&#8217;d like to help us shape our product vision, we&#8217;d love to talk. You can email me directly: <a href="mailto:alund@spotify.com">alund@spotify.com</a>.</p>
<h2 id="overview">Overview</h2>
<p>The Backstage platform consists of a number of different components:</p>
<ul>
<li><strong>app</strong> - Main web application that users interact with. It&#8217;s built up by a number of different <em>Plugins</em>. This repo contains an example implementation of an app (located in <code>packages/example-app</code>) and you can easily get started with your own app by <a href="docs/create-an-app.md">creating one</a>.</li>
<li><a href="https://github.com/spotify/backstage/tree/master/plugins"><strong>plugins</strong></a> - Each plugin is treated as a self-contained web app and can include almost any type of content. Plugins all use a common set of platform API&#8217;s and reusable UI components. Plugins can fetch data either from the <em>backend</em> or through any RESTful API exposed through the <em>proxy</em>.</li>
<li><a href="https://github.com/spotify/backstage/tree/master/packages/backend"><strong>backend</strong></a> - GraphQL aggregation service that holds the model of your software ecosystem, including organisational information and what team owns what software. The backend also has a Plugin model for extending its graph.</li>
<li><strong>proxy</strong> * - Terminates HTTPS and exposes any RESTful API to Plugins.</li>
<li><strong>identity</strong> * - A backend service that holds your organisation&#8217;s metadata.</li>
</ul>
<p><em>* not yet released</em></p>
<figure>
<img src="backstage_overview.png" alt="overview" />
<figcaption>overview</figcaption>
</figure>
<h2 id="gettingstarted">Getting started</h2>
<p>To run a Backstage app, you will need to have the following installed:</p>
<ul>
<li><a href="https://git-scm.com/book/en/v2/Getting-Started-Installing-Git">git</a></li>
<li><a href="https://nodejs.org/en/download/">NodeJS</a> - Active LTS Release, currently v12</li>
<li><a href="https://classic.yarnpkg.com/en/docs/install">yarn</a></li>
</ul>
<p>After cloning this repo, open a terminal window and start the web app using the following commands from the project root:</p>
<pre><code class="bash">yarn install
yarn start
</code></pre>
<p>The final <code>yarn start</code> command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal.</p>
<p>And thats it! You are good to go 👍</p>
<h3 id="nextstep">Next step</h3>
<p>Take a look at the <a href="docs/getting-started/README.md">Getting Started</a> guide to learn more about how to extend the functionality with Plugins.</p>
<h2 id="documentation">Documentation</h2>
<ul>
<li><a href="docs/getting-started/README.md">Getting Started</a></li>
<li><a href="docs/create-an-app.md">Create a Backstage App</a></li>
<li><a href="docs/architecture-terminology.md">Architecture</a></li>
<li><a href="docs/reference/README.md">API references</a></li>
<li><a href="docs/design.md">Designing for Backstage</a></li>
<li><a href="http://storybook.backstage.io">Storybook - UI components</a> (<a href="https://github.com/spotify/backstage/milestone/9">WIP</a>)</li>
<li><a href="docs/getting-started/contributing-to-storybook.md">Contributing to Storybook</a></li>
<li>Using Backstage components (TODO)</li>
</ul>
<h2 id="contributing">Contributing</h2>
<p>We would love your help in building Backstage! See <a href="CONTRIBUTING.md">CONTRIBUTING</a> for more information.</p>
<h2 id="community">Community</h2>
<ul>
<li><a href="https://discord.gg/MUpMjP2">Discord chatroom</a> - Get support or discuss the project</li>
<li><a href="https://github.com/spotify/backstage/labels/good%20first%20issue">Good First Issues</a> - Start here if you want to contribute</li>
<li><a href="https://github.com/spotify/backstage/labels/rfc">RFCs</a> - Help shape the technical direction</li>
<li><a href="docs/FAQ.md">FAQ</a> - Frequently Asked Questions</li>
<li><a href="CODE_OF_CONDUCT.md">Code of Conduct</a> - This is how we roll</li>
<li><a href="https://backstage.io/blog/">Blog</a> - Announcements and updates</li>
<li>Give us a star ⭐️ - If you are using Backstage or think it is an interesting project, we would love a star ❤️</li>
</ul>
<p>Or, if you are an open source developer and are interested in joining our team, please reach out to <a href="mailto:foss-opportunities@spotify.com">foss-opportunities@spotify.com </a></p>
<h2 id="license">License</h2>
<p>Copyright 2020 Spotify AB.</p>
<p>Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0</p>
</body>
</html>
+2 -1
View File
@@ -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",
+3 -1
View File
@@ -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'));
+1
View File
@@ -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';
+7
View File
@@ -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)),
);
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+6
View File
@@ -0,0 +1,6 @@
# Title
Welcome to the circleci plugin!
## Sub-section 1
## Sub-section 2
+41
View File
@@ -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"
]
}
+112
View File
@@ -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<CircleCIOptions> = {
circleHost: '/circleci/api',
vcs: {
type: GitType.GITHUB,
owner: 'CircleCITest3',
repo: 'circleci-test',
},
};
export const circleCIApiRef = new ApiRef<CircleCIApi>({
id: 'plugin.circleci.service',
description: 'Used by the CircleCI plugin to make requests',
});
export class CircleCIApi {
private token: string = '';
options: Partial<CircleCIOptions>;
authed: boolean = false;
constructor(options?: Partial<CircleCIOptions>) {
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));
}
}
@@ -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 (
<ExpansionPanel TransitionProps={{ unmountOnExit: true }}>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`panel-${name}-content`}
id={`panel-${name}-header`}
>
<Typography>{name}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
{messages.length === 0 ? (
'Nothing here...'
) : (
<Suspense fallback="...">
<div style={{ height: '200px', width: '100%' }}>
<LazyLog text={messages.join('\n')} extraLines={1} enableSearch />
</div>
</Suspense>
)}
</ExpansionPanelDetails>
</ExpansionPanel>
);
};
@@ -0,0 +1 @@
export { ActionOutput } from './ActionOutput';
@@ -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 <StatusPending />;
case 'running':
return <CircularProgress size={12} />;
case 'failed':
return <StatusFailed />;
case 'success':
return <StatusOK />;
case 'canceled':
default:
return <StatusNA />;
}
};
export const CITableBuildRow: FC<{ build: CITableBuildInfo }> = ({ build }) => (
<TableRow key={build.id}>
<TableCell>{build.id}</TableCell>
<TableCell>
<Link to={`/circleci/build/${build.id}`}>{build.buildName}</Link>
</TableCell>
<TableCell>
{build.source.branchName}
<br />
{build.source.commit.hash}
</TableCell>
<TableCell align="center">{getStatusComponent(build.status)}</TableCell>
{build.tests && (
<TableCell>
{
<>
{build.tests.passed}/{build.tests.total} (
{build.tests.failed ? build.tests.failed + ', ' : ''}
{build.tests.skipped ? build.tests.skipped : ''})
</>
}
</TableCell>
)}
<TableCell align="center">
<Button onClick={build.onRetryClick}>
<RetryIcon />
</Button>
</TableCell>
</TableRow>
);
export const CITableBuildHeadRow:FC<{isTestDataAvailable: boolean}> = ({isTestDataAvailable}) => (
<TableRow>
<TableCell>ID</TableCell>
<TableCell>Build</TableCell>
<TableCell>Source</TableCell>
<TableCell align="center">Status</TableCell>
{isTestDataAvailable && <TableCell>Tests</TableCell>}
<TableCell align="center">Actions</TableCell>
</TableRow>
);
export const CITable: FC<{
builds: CITableBuildInfo[];
}> = ({ builds }) => {
const classes = useStyles();
const isTestDataAvailable = builds.some(build => build.tests);
return (
<TableContainer>
<Table className={classes.table} size="small" aria-label="a dense table">
<TableHead><CITableBuildHeadRow isTestDataAvailable={isTestDataAvailable}/></TableHead>
<TableBody>
{builds.map(build => (
<CITableBuildRow build={build} />
))}
</TableBody>
</Table>
</TableContainer>
);
};
@@ -0,0 +1 @@
export { CITable, CITableBuildInfo } from './CITable';
@@ -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<string, string>)[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<BuildSummary[]>([]);
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 <div>Not authenticated</div>;
const transformedBuilds = transform(builds || [], api);
return (
<>
{!api.authed ? (
<div>Not authenticated</div>
) : (
<CITable builds={transformedBuilds} />
)}
</>
);
};
@@ -0,0 +1 @@
export { CircleCIFetch } from './CirleCIFetch';
@@ -0,0 +1,12 @@
import React from 'react';
import { Header, Page, pageTheme, HeaderLabel } from '@backstage/core';
export const Layout: React.FC = ({ children }) => (
<Page theme={pageTheme.tool}>
<Header title="Welcome to circleci!" subtitle="Optional subtitle">
<HeaderLabel label="Owner" value="Team X" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
{children}
</Page>
);
@@ -0,0 +1 @@
export * from './Layout';
@@ -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 = () => (
<ContentHeader title="Circle CI">
<Button
component={RouterLink}
to="/circleci/settings"
startIcon={<SettingsIcon />}
>
Settings
</Button>
<SupportButton>A description of your plugin goes here.</SupportButton>
</ContentHeader>
);
@@ -0,0 +1 @@
export * from './PluginHeader';
@@ -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 (
<List>
<ListItem>
<TextField
name="circleci-owner"
label="Owner"
value={owner}
onChange={e => setOwner(e.target.value)}
/>
</ListItem>
<ListItem>
<TextField
name="circleci-repo"
label="Repo"
value={repo}
onChange={e => setRepo(e.target.value)}
/>
</ListItem>
<ListItem>
<Button
data-testid="load-build-button"
variant="outlined"
color="primary"
onClick={() => setGitInfo({ owner, repo })}
>
Save
</Button>
</ListItem>
</List>
);
};
+19
View File
@@ -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';
@@ -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<{}> = () => (
<Layout>
<Content>
<ContentHeader title="Circle CI">
<Button
component={RouterLink}
to="/circleci/settings"
startIcon={<SettingsIcon />}
>
Settings
</Button>
<SupportButton>A description of your plugin goes here.</SupportButton>
</ContentHeader>
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard title="Pipelines">
<CircleCIFetch />
</InfoCard>
</Grid>
</Grid>
</Content>
</Layout>
);
@@ -0,0 +1 @@
export * from './BuildsPage';
@@ -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<BuildWithSteps | null>(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 (
<Layout>
<Content>
<PluginHeader />
{!api.authed ? (
<div>Not authenticated</div>
) : (
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard title="Pipelines">
<BuildsList build={build} />
</InfoCard>
</Grid>
</Grid>
)}
</Content>
</Layout>
);
};
const BuildsList: FC<{ build: BuildWithSteps | null }> = ({ build }) => (
<Box>
{build &&
build.steps &&
build.steps.map(
({ name, actions }: { name: string; actions: BuildStepAction[] }) => (
<ActionsList name={name} actions={actions} />
),
)}
</Box>
);
const ActionsList: FC<{ actions: BuildStepAction[]; name: string }> = ({
actions,
}) => (
<>
{actions.map((action: BuildStepAction) => (
<ActionOutput
action={action}
name={action.name}
url={action.output_url || ''}
/>
))}
</>
);
@@ -0,0 +1 @@
export * from './DetailedViewPage';
@@ -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 (
<Layout>
<Content>
<ContentHeader title="Settings">
<Button component={RouterLink} to="/circleci">
Back
</Button>
<SupportButton>A description of your plugin goes here.</SupportButton>
</ContentHeader>
<Grid container spacing={3}>
<Grid item xs={6}>
<InfoCard title="Authentication">
<List>
{authed ? (
<>Already authed</>
) : (
<>
<ListItem>
<TextField
name="circleci-token"
type="password"
label="Token"
value={token}
onChange={e => setToken(e.target.value)}
/>
</ListItem>
<ListItem>
<Button
data-testid="github-auth-button"
variant="outlined"
color="primary"
onClick={async () => {
api.setToken(token);
api
.validateToken()
.then(() => setAuthed(true))
.catch(() => setAuthed(false));
}}
>
Authenticate
</Button>
</ListItem>
</>
)}
</List>
</InfoCard>
</Grid>
<Grid item xs={6}>
<InfoCard title="Project configuration">
<ProjectInput
apiGitInfo={api.options.vcs}
setGitInfo={info => api.setVCSOptions(info)}
/>
</InfoCard>
</Grid>
</Grid>
</Content>
</Layout>
);
};
@@ -0,0 +1 @@
export * from './SettingsPage';
+23
View File
@@ -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();
});
});
+28
View File
@@ -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);
},
});
+12
View File
@@ -0,0 +1,12 @@
import type {Options} from 'http-proxy-middleware';
export const proxySettings: Record<string, Options> = {
'/circleci/api': {
target: 'https://circleci.com/api/v1.1',
changeOrigin: true,
logLevel: 'debug',
pathRewrite: {
'^/circleci/api/': '/',
},
},
};
+18
View File
@@ -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();
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"include": ["src"],
"compilerOptions": {
"module": "esnext",
"baseUrl": "src"
}
}
+125 -13
View File
@@ -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"