@@ -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/": "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
+5
-5
@@ -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 && (
|
||||
<>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -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
|
||||
@@ -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();
|
||||
@@ -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}"
|
||||
]
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
},
|
||||
});
|
||||
@@ -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/': '/',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<{}> = () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,6 +17,4 @@
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { plugin } from '../src/plugin';
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(plugin)
|
||||
.render();
|
||||
createDevApp().registerPlugin(plugin).render();
|
||||
|
||||
+1
-1
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user