Merge pull request #748 from Nek/master

Circle CI Plugin (Fixes #667)
This commit is contained in:
Nikita Dudnik
2020-05-19 14:38:10 +02:00
committed by GitHub
56 changed files with 1928 additions and 45 deletions
+12 -2
View File
@@ -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/": "/"
}
}
}
}
+3 -1
View File
@@ -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'));
+1
View File
@@ -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';
+1
View File
@@ -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",
+1 -1
View File
@@ -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({
@@ -64,9 +64,11 @@ export async function readTemplate(
templateDir: string,
templateVars: any,
): Promise<TemplateFile[]> {
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<TemplateFile>();
for (const templateFile of templateFilePaths) {
+1 -1
View File
@@ -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({
+1 -1
View File
@@ -157,7 +157,7 @@ function actionHandler<T extends readonly any[]>(
};
}
process.on('unhandledRejection', rejection => {
process.on('unhandledRejection', (rejection) => {
if (rejection instanceof Error) {
exitWithError(rejection);
} else {
+1 -1
View File
@@ -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}`);
});
@@ -31,7 +31,7 @@ const defaultProps = {
showLineNumbers: false,
};
const CodeSnippet: FC<Props> = props => {
const CodeSnippet: FC<Props> = (props) => {
const { text, language, showLineNumbers } = {
...defaultProps,
...props,
@@ -25,7 +25,7 @@ describe('<HorizontalScrollGrid />', () => {
jest.spyOn(window.performance, 'now').mockReturnValue(5);
jest
.spyOn(window, 'requestAnimationFrame')
.mockImplementation(cb => cb(20));
.mockImplementation((cb) => cb(20));
});
afterEach(() => {
@@ -36,7 +36,7 @@ describe('<StructuredMetadataTable />', () => {
<StructuredMetadataTable metadata={metadata} />,
);
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('<StructuredMetadataTable />', () => {
);
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('<StructuredMetadataTable />', () => {
<StructuredMetadataTable metadata={metadata} />,
);
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('<StructuredMetadataTable />', () => {
);
const keys = Object.keys(metadata.config);
keys.forEach(value => {
keys.forEach((value) => {
expect(
getByText(startCase(value), { exact: false }),
).toBeInTheDocument();
@@ -150,6 +150,7 @@ const convertColumns = (
export interface TableColumn extends Column<{}> {
highlight?: boolean;
width?: string;
}
export interface TableProps extends MaterialTableProps<{}> {
@@ -126,6 +126,7 @@ type Props = {
actions?: ReactNode;
cardClassName?: string;
actionsTopRight?: ReactNode;
className?: string;
};
const InfoCard: FC<Props> = ({
@@ -142,6 +143,7 @@ const InfoCard: FC<Props> = ({
actions,
cardClassName,
actionsTopRight,
className,
}) => {
const classes = useStyles();
@@ -169,7 +171,7 @@ const InfoCard: FC<Props> = ({
}
return (
<Card style={calculatedStyle}>
<Card style={calculatedStyle} className={className}>
<ErrorBoundary slackChannel={slackChannel}>
{title && (
<>
+18 -15
View File
@@ -26,7 +26,7 @@ import {
} from './config';
import { SidebarDivider } from './Items';
const useStyles = makeStyles<BackstageTheme>(theme => ({
const useStyles = makeStyles<BackstageTheme>((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<IntroCardProps> = props => {
export const IntroCard: FC<IntroCardProps> = (props) => {
const classes = useStyles();
const { text, onClose } = props;
const handleClose = () => onClose();
@@ -109,18 +109,15 @@ type SidebarIntroCardProps = {
onDismiss: () => void;
};
const SidebarIntroCard: FC<SidebarIntroCardProps> = props => {
const {text, onDismiss} = props
const [collapsing, setCollapsing] = useState(false)
const SidebarIntroCard: FC<SidebarIntroCardProps> = (props) => {
const { text, onDismiss } = props;
const [collapsing, setCollapsing] = useState(false);
const startDismissing = () => {
setCollapsing(true)
}
setCollapsing(true);
};
return (
<Collapse in={!collapsing} onExited={onDismiss}>
<IntroCard
text={text}
onClose={startDismissing}
/>
<IntroCard text={text} onClose={startDismissing} />
</Collapse>
);
};
@@ -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 && (
<>
<SidebarIntroCard text={starredIntroText} onDismiss={dismissStarred} />
<SidebarIntroCard
text={starredIntroText}
onDismiss={dismissStarred}
/>
<SidebarDivider />
</>
)}
{!recentlyViewedItemsDismissed && (
<SidebarIntroCard text={recentlyViewedIntroText} onDismiss={dismissRecentlyViewed} />
<SidebarIntroCard
text={recentlyViewedIntroText}
onDismiss={dismissRecentlyViewed}
/>
)}
</>
);
@@ -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,
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+50
View File
@@ -0,0 +1,50 @@
# <img src="./src/assets/circle-logo-badge-black.png" width="32" /> CircleCI Plugin
Website: [https://circleci.com/](https://circleci.com/)
<img src="./src/assets/screenshot-1.png" />
<img src="./src/assets/screenshot-2.png" />
## 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
+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 { 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();
+58
View File
@@ -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}"
]
}
+76
View File
@@ -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<CircleCIApi>({
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,
});
}
}
+60
View File
@@ -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 (
<AppStateProvider>
<>
<Switch>
<Route path="/circleci" exact component={BuildsPage} />
<Route
path="/circleci/build/:buildId"
exact
component={DetailedViewPage}
/>
</Switch>
<Settings />
</>
</AppStateProvider>
);
};
// TODO: allow pass in settings as props
// When some shared settings workflow
// will be established
export const CircleCIWidget = () => (
<MemoryRouter initialEntries={['/circleci']}>
<AppStateProvider>
<>
<Switch>
<Route path="/circleci" exact component={Builds} />
<Route
path="/circleci/build/:buildId"
exact
component={BuildWithSteps}
/>
</Switch>
<Settings />
</>
</AppStateProvider>
</MemoryRouter>
);
@@ -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 (
<Page theme={pageTheme.tool}>
<Header
pageTitleOverride="Circle CI"
title={
<Box display="flex" alignItems="center">
<Box mr={1} /> Circle CI
</Box>
}
>
<HeaderLabel label="Owner" value="Team X" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
{children}
</Page>
);
};
@@ -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';
@@ -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<Props> = ({ title = 'Circle CI' }) => {
const [, { showSettings }] = useSettings();
const location = useLocation();
const notRoot = !location.pathname.match(/\/circleci\/?$/);
const isSettingsPage = location.pathname.match(/\/circleci\/settings\/?/);
return (
<ContentHeader
title={title}
titleComponent={() => (
<Box alignItems="center" display="flex">
{notRoot && (
<IconButton component={RouterLink} to="/circleci">
<ArrowBack />
</IconButton>
)}
<Typography variant="h4">{title}</Typography>
</Box>
)}
>
{!isSettingsPage && (
<Button onClick={showSettings} startIcon={<SettingsIcon />}>
Settings
</Button>
)}
<SupportButton>
This plugin allows you to view and interact with your builds within the
Circle CI environment.
</SupportButton>
</ContentHeader>
);
};
@@ -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';
@@ -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 (
<>
<Snackbar
autoHideDuration={1000}
open={saved}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
onClose={() => setSaved(false)}
>
<Alert severity="success">Credentials saved.</Alert>
</Snackbar>
<Dialog open={showSettings} onClose={hideSettings}>
<DialogTitle>
Project Credentials
{/* {authed ? <StatusOK /> : <StatusFailed />} */}
</DialogTitle>
<Box minWidth="400px">
<List>
<ListItem>
<TextField
name="circleci-token"
label="Token"
value={token}
fullWidth
variant="outlined"
onChange={(e) => setToken(e.target.value)}
/>
</ListItem>
<ListItem>
<TextField
name="circleci-owner"
fullWidth
label="Owner"
variant="outlined"
value={owner}
onChange={(e) => setOwner(e.target.value)}
/>
</ListItem>
<ListItem>
<TextField
name="circleci-repo"
label="Repo"
fullWidth
variant="outlined"
value={repo}
onChange={(e) => setRepo(e.target.value)}
/>
</ListItem>
<ListItem>
<Box mt={2} display="flex" width="100%" justifyContent="center">
<Button
data-testid="github-auth-button"
variant="outlined"
color="primary"
onClick={() => {
setSaved(true);
saveSettings({ repo, owner, token });
hideSettings();
}}
>
Save credentials
</Button>
</Box>
</ListItem>
</List>
</Box>
</Dialog>
</>
);
};
export default Settings;
@@ -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';
+21
View File
@@ -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';
+38
View File
@@ -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<SvgIconProps> = (props) => (
<SvgIcon
{...props}
enableBackground="new 0 0 200 200"
viewBox="0 0 103.8 105.2"
>
<path
d="m38.6 52.6c0-6.9 5.6-12.5 12.5-12.5s12.5 5.6 12.5 12.5-5.6 12.5-12.5 12.5c-6.9.1-12.5-5.6-12.5-12.5zm12.5-52.6c-24.6 0-45.2 16.8-51 39.6 0 .2-.1.3-.1.5 0 1.4 1.1 2.5 2.5 2.5h21.2c1 0 1.9-.6 2.3-1.5 4.4-9.5 13.9-16.1 25.1-16.1 15.2 0 27.6 12.4 27.6 27.6s-12.4 27.6-27.6 27.6c-11.1 0-20.7-6.6-25.1-16.1-.4-.9-1.3-1.5-2.3-1.5h-21.2c-1.4 0-2.5 1.1-2.5 2.5 0 .2 0 .3.1.5 5.8 22.8 26.4 39.6 51 39.6 29.1 0 52.7-23.6 52.7-52.7 0-29-23.6-52.5-52.7-52.5z"
fill="#343434"
/>
</SvgIcon>
);
export const navTargetCircleCI = createNavTarget({
icon: CircleCIIcon,
path: '/circleci',
title: 'CircleCI',
});
@@ -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 }) => (
<Box display="flex" alignItems="center">
#{build?.build_num} - {build?.subject}
<IconLink href={build?.build_url} target="_blank">
<LaunchIcon />
</IconLink>
</Box>
);
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<typeof useStyles>,
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 = () => (
<Layout>
<Content>
<BuildWithStepsView />
</Content>
</Layout>
);
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 (
<>
<PluginHeader title="Build info" />
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard
className={pickClassName(classes, value)}
title={<BuildName build={value} />}
cardClassName={classes.cardContent}
>
{loading ? <Progress /> : <BuildsList build={value} />}
</InfoCard>
</Grid>
</Grid>
</>
);
};
const BuildsList: FC<{ build?: BuildWithSteps }> = ({ 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,
}) => {
const classes = useStyles();
return (
<>
{actions.map((action: BuildStepAction) => (
<ActionOutput
className={action.failed ? classes.failed : classes.success}
action={action}
name={action.name}
url={action.output_url || ''}
/>
))}
</>
);
};
export default Page;
export { BuildWithStepsView as BuildWithSteps };
@@ -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';
@@ -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 (
<ExpansionPanel
TransitionProps={{ unmountOnExit: true }}
className={className}
>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`panel-${name}-content`}
id={`panel-${name}-header`}
IconButtonProps={{
className: classes.button,
}}
>
<Typography variant="button">
{name} ({timeElapsed})
</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails className={classes.expansionPanelDetails}>
{messages.length === 0 ? (
'Nothing here...'
) : (
<Suspense fallback={<LinearProgress />}>
<div style={{ height: '20vh', width: '100%' }}>
<LazyLog text={messages.join('\n')} extraLines={1} enableSearch />
</div>
</Suspense>
)}
</ExpansionPanelDetails>
</ExpansionPanel>
);
};
@@ -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';
@@ -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<{}> = () => (
<Layout>
<Content>
<Builds />
</Content>
</Layout>
);
const Builds = () => (
<>
<PluginHeader title="All builds" />
<Grid container spacing={3} direction="column">
<Grid item>
<BuildsComp />
</Grid>
</Grid>
</>
);
export default BuildsPage;
export { Builds };
@@ -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';
@@ -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 (
<CITable
total={total}
loading={loading}
retry={retry}
builds={value ?? []}
projectName={projectName}
page={page}
onChangePage={setPage}
pageSize={pageSize}
onChangePageSize={setPageSize}
/>
);
};
@@ -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';
@@ -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 <StatusPending />;
case 'running':
return <StatusRunning />;
case 'failed':
return <StatusError />;
case 'success':
return <StatusOK />;
case 'canceled':
default:
return <StatusWarning />;
}
};
const generatedColumns: TableColumn[] = [
{
title: 'ID',
field: 'id',
type: 'numeric',
width: '80px',
},
{
title: 'Build',
field: 'buildName',
highlight: true,
render: (row: Partial<CITableBuildInfo>) => (
<Link component={RouterLink} to={`/circleci/build/${row.id}`}>
{row.buildName}
</Link>
),
},
{
title: 'Source',
render: (row: Partial<CITableBuildInfo>) => (
<>
<p>{row.source?.branchName}</p>
<p>{row.source?.commit.hash}</p>
</>
),
},
{
title: 'Status',
render: (row: Partial<CITableBuildInfo>) => (
<Box display="flex" alignItems="center">
{getStatusComponent(row.status)}
<Box mr={1} />
<Typography variant="button">{row.status}</Typography>
</Box>
),
},
{
title: 'Actions',
render: (row: Partial<CITableBuildInfo>) => (
<IconButton onClick={row.onRestartClick}>
<RetryIcon />
</IconButton>
),
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<Props> = ({
projectName,
loading,
pageSize,
page,
retry,
builds,
onChangePage,
onChangePageSize,
total,
}) => {
return (
<Table
isLoading={loading}
options={{ paging: true, pageSize }}
totalCount={total}
page={page}
actions={[
{
icon: () => <RetryIcon />,
tooltip: 'Refresh Data',
isFreeAction: true,
onClick: () => retry(),
},
]}
data={builds}
onChangePage={onChangePage}
onChangeRowsPerPage={onChangePageSize}
title={
<Box display="flex" alignItems="center">
<GitHubIcon />
<Box mr={1} />
<Typography variant="h6">{projectName}</Typography>
</Box>
}
columns={generatedColumns}
/>
);
};
@@ -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';
+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();
});
});
+25
View File
@@ -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 });
},
});
+25
View File
@@ -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/': '/',
},
},
};
+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();
+56
View File
@@ -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<Action>]>(
[] as any,
);
export const STORAGE_KEY = `${circleCIApiRef.id}.settings`;
const initialState: State = {
owner: '',
repo: '',
token: '',
showSettings: false,
};
const reducer: Reducer<State, Action> = (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 (
<AppContext.Provider value={[state, dispatch]}>
<>{children}</>
</AppContext.Provider>
);
};
+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 * from './AppState';
export * from './useSettings';
export * from './useBuilds';
export * from './useBuildWithSteps';
+36
View File
@@ -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;
@@ -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<any>,
interval: number,
) => {
const isPolling = useRef<boolean>(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 };
};
@@ -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;
}
+153
View File
@@ -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<string, string>)[status];
};
export const transform = (
buildsData: BuildSummary[],
restartBuild: { (buildId: number): Promise<void> },
): 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;
}
+66
View File
@@ -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;
}
@@ -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<{}> = () => {
+1 -1
View File
@@ -18,7 +18,7 @@ import React, { FC } from 'react';
import { createNavTarget } from '@backstage/core';
import { SvgIcon, SvgIconProps } from '@material-ui/core';
const GraphiQLIcon: FC<SvgIconProps> = props => (
const GraphiQLIcon: FC<SvgIconProps> = (props) => (
<SvgIcon {...props}>
<g id="surface1">
<path
+1 -3
View File
@@ -17,6 +17,4 @@
import { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';
createDevApp()
.registerPlugin(plugin)
.render();
createDevApp().registerPlugin(plugin).render();
@@ -28,7 +28,7 @@ import { BackstageTheme } from '@backstage/theme';
import { Progress } from '@backstage/core';
import { ComponentIdValidators } from '../../util/validate';
const useStyles = makeStyles<BackstageTheme>(theme => ({
const useStyles = makeStyles<BackstageTheme>((theme) => ({
form: {
alignItems: 'flex-start',
display: 'flex',
+119 -6
View File
@@ -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"