Merge branch 'master' of github.com:Nek/backstage

This commit is contained in:
Nikita Nek Dudnik
2020-05-18 18:52:55 +02:00
22 changed files with 531 additions and 444 deletions
@@ -14,13 +14,19 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import { serveBundle } from '../../lib/bundler';
import { Command } from 'commander';
import { paths } from '../../lib/paths';
export default async (cmd: Command) => {
const pkgPath = paths.resolveTarget('package.json');
const pkg = await fs.readJson(pkgPath);
const waitForExit = await serveBundle({
entry: 'dev/index',
checksEnabled: cmd.check,
proxy: pkg.proxy,
});
await waitForExit();
+4 -1
View File
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import yn from 'yn';
import webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
@@ -36,6 +37,8 @@ export async function serveBundle(options: ServeOptions) {
const urls = prepareUrls(protocol, host, port);
const paths = resolveBundlingPaths(options);
const pkgPath = paths.targetPackageJson;
const pkg = await fs.readJson(pkgPath);
const config = createConfig(paths, { ...options, isDev: true });
const compiler = webpack(config);
@@ -48,7 +51,7 @@ export async function serveBundle(options: ServeOptions) {
https: protocol === 'https',
host,
port,
proxy: options.proxy,
proxy: pkg.proxy,
});
await new Promise((resolve, reject) => {
+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();
+28 -14
View File
@@ -3,18 +3,31 @@
"version": "0.1.1-alpha.4",
"main": "dist/index.esm.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.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"
"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.4",
"@backstage/theme": "^0.1.1-alpha.4",
"@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",
@@ -22,24 +35,25 @@
"circleci-api": "^4.0.0",
"fast-deep-equal": "^3.1.1",
"moment": "^2.25.3",
"react": "16.13.1",
"react-dom": "16.13.1",
"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": "^13.0.0"
"react-use": "^14.2.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.4",
"@testing-library/jest-dom": "^4.2.4",
"@backstage/cli": "^0.1.1-alpha.5",
"@testing-library/jest-dom": "^5.7.0",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/jest": "^24.0.0",
"@testing-library/user-event": "^10.2.4",
"@types/jest": "^25.2.1",
"@types/node": "^12.0.0",
"@types/testing-library__jest-dom": "5.0.2",
"jest-fetch-mock": "^3.0.3"
"@types/testing-library__jest-dom": "^5.0.4",
"jest-fetch-mock": "^3.0.3",
"@backstage/dev-utils": "^0.1.1-alpha.5"
},
"files": [
"dist"
"dist/**/*.{js,d.ts}"
]
}
+8 -1
View File
@@ -48,8 +48,15 @@ export class CircleCIApi {
});
}
async getBuilds(options: CircleCIOptions) {
async getBuilds(
{ limit = 10, offset = 0 }: { limit: number; offset: number },
options: CircleCIOptions,
) {
return getBuildSummaries(options.token, {
options: {
limit,
offset,
},
vcs: {},
circleHost: this.apiUrl,
...options,
+18 -14
View File
@@ -16,20 +16,24 @@
import React from 'react';
import { Switch, Route } from 'react-router';
import { BuildsPage } from '../pages/BuildsPage';
import { SettingsPage } from '../pages/SettingsPage';
import { DetailedViewPage } from '../pages/BuildWithStepsPage';
import { AppStateProvider } from '../state';
import { Settings } from './Settings';
export const App = () => (
<AppStateProvider>
<Switch>
<Route path="/circleci" exact component={BuildsPage} />
<Route path="/circleci/settings" exact component={SettingsPage} />
<Route
path="/circleci/build/:buildId"
exact
component={DetailedViewPage}
/>
</Switch>
</AppStateProvider>
);
export const App = () => {
return (
<AppStateProvider>
<>
<Switch>
<Route path="/circleci" exact component={BuildsPage} />
<Route
path="/circleci/build/:buildId"
exact
component={DetailedViewPage}
/>
</Switch>
<Settings />
</>
</AppStateProvider>
);
};
@@ -17,20 +17,22 @@ import React from 'react';
import { Header, Page, pageTheme, HeaderLabel } from '@backstage/core';
import { Box } from '@material-ui/core';
export const Layout: React.FC = ({ children }) => (
<Page theme={pageTheme.tool}>
<Header
pageTitleOverride="Circle CI"
title={
<Box display="flex" alignItems="center">
<img src={logo} style={{ height: '1em' }} alt="Backstage Logo" />
<Box mr={1} /> Circle CI
</Box>
}
>
<HeaderLabel label="Owner" value="Team X" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
{children}
</Page>
);
export const Layout: React.FC = ({ children }) => {
return (
<Page theme={pageTheme.tool}>
<Header
pageTitleOverride="Circle CI"
title={
<Box display="flex" alignItems="center">
<img src={logo} style={{ height: '1em' }} alt="Backstage Logo" />
<Box mr={1} /> Circle CI
</Box>
}
>
<HeaderLabel label="Owner" value="Team X" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
{children}
</Page>
);
};
@@ -19,9 +19,11 @@ 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\/?/);
@@ -40,11 +42,7 @@ export const PluginHeader: FC<Props> = ({ title = 'Circle CI' }) => {
)}
>
{!isSettingsPage && (
<Button
component={RouterLink}
to="/circleci/settings"
startIcon={<SettingsIcon />}
>
<Button onClick={showSettings} startIcon={<SettingsIcon />}>
Settings
</Button>
)}
@@ -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 } 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] = React.useState(() => tokenFromStore);
const [owner, setOwner] = React.useState(() => ownerFromStore);
const [repo, setRepo] = React.useState(() => repoFromStore);
React.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;
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { default as SettingsPage } from './SettingsPage';
export { default as Settings } from './Settings';
@@ -15,7 +15,7 @@
*/
import React, { FC, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { Content, InfoCard } from '@backstage/core';
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';
@@ -27,7 +27,7 @@ import { useSettings } from '../../state/useSettings';
import { useBuildWithSteps } from '../../state/useBuildWithSteps';
const IconLink = IconButton as typeof Link;
const BuildName: FC<{ build: BuildWithSteps | null }> = ({ build }) => (
const BuildName: FC<{ build?: BuildWithSteps }> = ({ build }) => (
<Box display="flex" alignItems="center">
#{build?.build_num} - {build?.subject}
<IconLink href={build?.build_url} target="_blank">
@@ -96,7 +96,7 @@ const BuildWithStepsPage: FC<{}> = () => {
const { buildId = '' } = useParams();
const classes = useStyles();
const [settings] = useSettings();
const [build, { startPolling, stopPolling }] = useBuildWithSteps(
const [{ loading, value }, { startPolling, stopPolling }] = useBuildWithSteps(
parseInt(buildId, 10),
);
@@ -113,11 +113,11 @@ const BuildWithStepsPage: FC<{}> = () => {
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard
className={pickClassName(classes, build)}
title={<BuildName build={build} />}
className={pickClassName(classes, value)}
title={<BuildName build={value} />}
cardClassName={classes.cardContent}
>
<BuildsList build={build} />
{loading ? <Progress /> : <BuildsList build={value} />}
</InfoCard>
</Grid>
</Grid>
@@ -126,7 +126,7 @@ const BuildWithStepsPage: FC<{}> = () => {
);
};
const BuildsList: FC<{ build: BuildWithSteps | null }> = ({ build }) => (
const BuildsList: FC<{ build?: BuildWithSteps }> = ({ build }) => (
<Box>
{build &&
build.steps &&
@@ -13,74 +13,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC, useEffect } from 'react';
import { CITableBuildInfo, CITable } from '../CITable';
import { BuildSummary } from '../../../../api';
import { useBuilds } from '../../../../state/useBuilds';
import { useSettings } from '../../../../state/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];
};
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;
});
};
import React, { FC } from 'react';
import { CITable } from '../CITable';
import { useBuilds } from '../../../../state';
export const Builds: FC<{}> = () => {
const [
builds,
{ restartBuild: handleRestartBuild, startPolling, stopPolling },
{ total, loading, value, projectName, page, pageSize },
{ setPage, retry, setPageSize },
] = useBuilds();
const [{ repo, owner }] = useSettings();
useEffect(() => {
startPolling();
return () => stopPolling();
}, [repo, owner]);
const transformedBuilds = transform(builds, handleRestartBuild);
return (
<CITable builds={transformedBuilds} projectName={`${owner}/${repo}`} />
<CITable
total={total}
loading={loading}
retry={retry}
builds={value ?? []}
projectName={projectName}
page={page}
onChangePage={setPage}
pageSize={pageSize}
onChangePageSize={setPageSize}
/>
);
};
@@ -114,14 +114,46 @@ const generatedColumns: TableColumn[] = [
width: '10%',
},
];
export const CITable: FC<{
type Props = {
loading: boolean;
retry: () => void;
builds: CITableBuildInfo[];
projectName: string;
}> = React.memo(({ builds = [], projectName }) => {
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
options={{ paging: false }}
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 />
@@ -132,4 +164,4 @@ export const CITable: FC<{
columns={generatedColumns}
/>
);
});
};
@@ -1,139 +0,0 @@
/*
* 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 } from 'react';
import {
Button,
TextField,
List,
Grid,
ListItem,
Snackbar,
Box,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import { InfoCard, Content } from '@backstage/core';
import { Layout } from '../../components/Layout';
import { PluginHeader } from '../../components/PluginHeader';
import { useSettings } from '../../state/useSettings';
const SettingsPage = () => {
const [
{ repo: repoFromStore, owner: ownerFromStore, token: tokenFromStore },
{ saveSettings },
] = useSettings();
const [token, setToken] = React.useState(() => tokenFromStore);
const [owner, setOwner] = React.useState(() => ownerFromStore);
const [repo, setRepo] = React.useState(() => repoFromStore);
React.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 (
<Layout>
<Content>
<PluginHeader title="Settings" />
<Grid container spacing={3}>
<Grid item xs={12} md={3}>
<InfoCard
title={
<>
Project Credentials
{/* {authed ? <StatusOK /> : <StatusFailed />} */}
<Snackbar
autoHideDuration={1000}
open={saved}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
onClose={() => setSaved(false)}
>
<Alert severity="success">Credentials saved.</Alert>
</Snackbar>
</>
}
>
<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 });
}}
>
Save credentials
</Button>
</Box>
</ListItem>
</List>
</InfoCard>
</Grid>
</Grid>
</Content>
</Layout>
);
};
export default SettingsPage;
+11 -27
View File
@@ -15,9 +15,8 @@
*/
import React, { FC, useReducer, Dispatch, Reducer } from 'react';
import { circleCIApiRef } from '../api';
import type { SettingsState, State, Action } from './types';
export type { SettingsState };
import equal from 'fast-deep-equal';
import type { State, Action, SettingsState } from './types';
export { SettingsState };
export const AppContext = React.createContext<[State, Dispatch<Action>]>(
[] as any,
@@ -25,13 +24,10 @@ export const AppContext = React.createContext<[State, Dispatch<Action>]>(
export const STORAGE_KEY = `${circleCIApiRef.id}.settings`;
const initialState: State = {
settings: {
owner: '',
repo: '',
token: '',
},
builds: [],
buildsWithSteps: {},
owner: '',
repo: '',
token: '',
showSettings: false,
};
const reducer: Reducer<State, Action> = (state, action) => {
@@ -39,23 +35,12 @@ const reducer: Reducer<State, Action> = (state, action) => {
case 'setCredentials':
return {
...state,
settings: { ...state.settings, ...action.payload },
...action.payload,
};
case 'setBuilds':
if (equal(action.payload, state.builds)) return state;
return {
...state,
builds: action.payload,
};
case 'setBuildWithSteps': {
return {
...state,
buildsWithSteps: {
...state.buildsWithSteps,
[action.payload.build_num!]: action.payload,
},
};
}
case 'showSettings':
return { ...state, showSettings: true };
case 'hideSettings':
return { ...state, showSettings: false };
default:
return state;
}
@@ -63,7 +48,6 @@ const reducer: Reducer<State, Action> = (state, action) => {
export const AppStateProvider: FC = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AppContext.Provider value={[state, dispatch]}>
<>{children}</>
+3
View File
@@ -14,3 +14,6 @@
* limitations under the License.
*/
export * from './AppState';
export * from './useSettings';
export * from './useBuilds';
export * from './useBuildWithSteps';
+16 -33
View File
@@ -13,41 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BuildSummary, BuildWithSteps } from '../api';
export type SettingsState = {
owner: string;
repo: string;
token: string;
export type Settings = { owner: string; repo: string; token: string };
export type SettingsState = Settings & {
showSettings: boolean;
};
export type BuildsState = BuildSummary[];
export type State = SettingsState;
export type State = {
settings: SettingsState;
builds: BuildsState;
buildsWithSteps: BuildsWithStepsState;
};
type SettingsAction =
| {
type: 'setCredentials';
payload: {
repo: string;
owner: string;
token: string;
};
}
| { type: 'showSettings' }
| { type: 'hideSettings' };
type SettingsAction = {
type: 'setCredentials';
payload: {
repo: string;
owner: string;
token: string;
};
};
type BuildsAction = {
type: 'setBuilds';
payload: BuildSummary[];
};
type BuildsWithStepsAction = {
type: 'setBuildWithSteps';
payload: BuildWithSteps;
};
export type BuildsWithStepsState = Record<number, BuildWithSteps>;
export type Action = SettingsAction | BuildsAction | BuildsWithStepsAction;
export type Action = SettingsAction;
+29 -25
View File
@@ -14,50 +14,47 @@
* limitations under the License.
*/
import { errorApiRef, useApi } from '@backstage/core';
import { useContext } from 'react';
import { useCallback } from 'react';
import { useAsyncRetry } from 'react-use';
import { circleCIApiRef, GitType } from '../api/index';
import { AppContext } from '.';
import { useAsyncPolling } from './useAsyncPolling';
import { useSettings } from './useSettings';
import { useAsyncPolling } from './useAsyncPolling';
const INTERVAL_AMOUNT = 3000;
const INTERVAL_AMOUNT = 1500;
export function useBuildWithSteps(buildId: number) {
const [settings] = useSettings();
const [{ buildsWithSteps }, dispatch] = useContext(AppContext);
const [{ token, repo, owner }] = useSettings();
const api = useApi(circleCIApiRef);
const errorApi = useApi(errorApiRef);
const getBuildWithSteps = async () => {
const getBuildWithSteps = useCallback(async () => {
if (owner === '' || repo === '' || token === '') {
return Promise.reject('No credentials provided');
}
try {
const options = {
token: settings.token,
token: token,
vcs: {
owner: settings.owner,
repo: settings.repo,
owner: owner,
repo: repo,
type: GitType.GITHUB,
},
};
const build = await api.getBuild(buildId, options);
dispatch({ type: 'setBuildWithSteps', payload: build });
return Promise.resolve(build);
} catch (e) {
errorApi.post(e);
return Promise.reject(e);
}
};
const { startPolling, stopPolling } = useAsyncPolling(
getBuildWithSteps,
INTERVAL_AMOUNT,
);
}, [token, owner, repo, buildId]);
const restartBuild = async () => {
try {
await api.retry(buildId, {
token: settings.token,
token: token,
vcs: {
owner: settings.owner,
repo: settings.repo,
owner: owner,
repo: repo,
type: GitType.GITHUB,
},
});
@@ -66,15 +63,22 @@ export function useBuildWithSteps(buildId: number) {
}
};
const build = buildsWithSteps[buildId];
const { loading, value, retry } = useAsyncRetry(() => getBuildWithSteps(), [
getBuildWithSteps,
]);
const { startPolling, stopPolling } = useAsyncPolling(
getBuildWithSteps,
INTERVAL_AMOUNT,
);
return [
build,
{ loading, value, retry },
{
restartBuild,
getBuildWithSteps,
startPolling,
stopPolling,
getBuildWithSteps,
},
] 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 { 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;
}
-79
View File
@@ -1,79 +0,0 @@
/*
* 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 { GitType } from 'circleci-api';
import { useContext } from 'react';
import { circleCIApiRef } from '../api/index';
import { AppContext } from '.';
import { useAsyncPolling } from './useAsyncPolling';
const INTERVAL_AMOUNT = 3000;
export function useBuilds() {
const [{ builds, settings }, dispatch] = useContext(AppContext);
const api = useApi(circleCIApiRef);
const errorApi = useApi(errorApiRef);
const getBuilds = async () => {
if (settings.owner === '' || settings.repo === '') return;
try {
const newBuilds = await api.getBuilds({
token: settings.token,
vcs: {
owner: settings.owner,
repo: settings.repo,
type: GitType.GITHUB,
},
});
dispatch({
type: 'setBuilds',
payload: newBuilds,
});
} catch (e) {
errorApi.post(e);
}
};
const { startPolling, stopPolling } = useAsyncPolling(
getBuilds,
INTERVAL_AMOUNT,
);
const restartBuild = async (buildId: number) => {
try {
await api.retry(buildId, {
token: settings.token,
vcs: {
owner: settings.owner,
repo: settings.repo,
type: GitType.GITHUB,
},
});
} catch (e) {
errorApi.post(e);
}
};
return [
builds,
{
restartBuild,
startPolling,
stopPolling,
},
] as const;
}
+8 -5
View File
@@ -13,12 +13,13 @@
* 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, SettingsState } from '.';
import { useApi, errorApiRef } from '@backstage/core';
import { AppContext, STORAGE_KEY } from './AppState';
import { Settings } from './types';
export function useSettings() {
const [{ settings }, dispatch] = useContext(AppContext);
const [settings, dispatch] = useContext(AppContext);
const errorApi = useApi(errorApiRef);
@@ -44,20 +45,22 @@ export function useSettings() {
rehydrate();
}, []);
const persist = (state: SettingsState) => {
const persist = (state: Settings) => {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
};
return [
settings,
{
saveSettings: (state: SettingsState) => {
saveSettings: (state: Settings) => {
persist(state);
dispatch({
type: 'setCredentials',
payload: state,
});
},
showSettings: () => dispatch({ type: 'showSettings' }),
hideSettings: () => dispatch({ type: 'hideSettings' }),
},
] as const;
}
+8 -8
View File
@@ -18376,10 +18376,10 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
source-map-url "^0.4.0"
urix "^0.1.0"
source-map-support@^0.5.6, source-map-support@~0.5.12:
version "0.5.16"
resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042"
integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==
source-map-support@^0.5.17, source-map-support@^0.5.6, source-map-support@~0.5.12:
version "0.5.19"
resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
@@ -19659,14 +19659,14 @@ ts-loader@^7.0.4:
semver "^6.0.0"
ts-node@^8.6.2:
version "8.8.1"
resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.8.1.tgz#7c4d3e9ed33aa703b64b28d7f9d194768be5064d"
integrity sha512-10DE9ONho06QORKAaCBpPiFCdW+tZJuY/84tyypGtl6r+/C7Asq0dhqbRZURuUlLQtZxxDvT8eoj8cGW0ha6Bg==
version "8.10.1"
resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.10.1.tgz#77da0366ff8afbe733596361d2df9a60fc9c9bd3"
integrity sha512-bdNz1L4ekHiJul6SHtZWs1ujEKERJnHs4HxN7rjTyyVOFf3HaJ6sLqe6aPG62XTzAB/63pKRh5jTSWL0D7bsvw==
dependencies:
arg "^4.1.0"
diff "^4.0.1"
make-error "^1.1.1"
source-map-support "^0.5.6"
source-map-support "^0.5.17"
yn "3.1.1"
ts-pnp@^1.1.2: