feat: good plugin state

This commit is contained in:
Ivan Shmidt
2020-05-07 23:26:18 +02:00
parent b842989456
commit 563148998e
10 changed files with 112 additions and 40 deletions
+4 -6
View File
@@ -36,12 +36,10 @@ export class CircleCIApi {
}
async retry(buildNumber: number, options: CircleCIOptions) {
return postBuildActions(
options.token,
buildNumber,
BuildAction.RETRY,
options,
);
return postBuildActions(options.token, buildNumber, BuildAction.RETRY, {
circleHost: this.apiUrl,
...options.vcs,
});
}
async getBuilds(options: CircleCIOptions) {
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

@@ -1,11 +1,20 @@
import React from 'react';
import { Header, Page, pageTheme, HeaderLabel } from '@backstage/core';
// @ts-ignore
import logo from '../../assets/circle-logo-horizontal-white.png';
import logo from '../../assets/circle-logo-badge-white-15.png';
import { Box } from '@material-ui/core';
export const Layout: React.FC = ({ children }) => (
<Page theme={pageTheme.tool}>
<Header title={<img src={logo} style={{ height: '2em' }} />}>
<Header
pageTitleOverride="Circle CI Plugin"
title={
<Box display="flex" alignItems="center">
<img src={logo} style={{ height: '1em' }} />
<Box mr={1} /> Circle CI Plugin
</Box>
}
>
<HeaderLabel label="Owner" value="Team X" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
@@ -1,7 +1,7 @@
import React from 'react';
import { Link as RouterLink, useLocation } from 'react-router-dom';
import { ContentHeader, SupportButton } from '@backstage/core';
import { Button, IconButton, Box } from '@material-ui/core';
import { Button, IconButton, Box, Typography } from '@material-ui/core';
import { Settings as SettingsIcon, ArrowBack } from '@material-ui/icons';
export const PluginHeader: React.FC<{ title?: string }> = ({
title = 'Circle CI',
@@ -11,18 +11,17 @@ export const PluginHeader: React.FC<{ title?: string }> = ({
const isSettingsPage = location.pathname.match(/\/circleci\/settings\/?/);
return (
<ContentHeader
title={
(
<Box alignItems="center" display="flex">
{notRoot && (
<IconButton component={RouterLink} to="/circleci">
<ArrowBack />
</IconButton>
)}
{title}
</Box>
) as any
}
title={title}
titleComponent={() => (
<Box alignItems="center" display="flex">
{notRoot && (
<IconButton component={RouterLink} to="/circleci">
<ArrowBack />
</IconButton>
)}
<Typography variant="h4">{title}</Typography>
</Box>
)}
>
{!isSettingsPage && (
<Button
@@ -48,7 +48,11 @@ const makeReadableStatus = (status: string | undefined) => {
} as Record<string, string>)[status];
};
const transform = (buildsData: BuildSummary[]): CITableBuildInfo[] => {
const transform = (
buildsData: BuildSummary[],
dispatch: Dispatch,
api: typeof circleCIApiRef.T,
): CITableBuildInfo[] => {
return buildsData.map((buildData) => {
const tableBuildInfo: CITableBuildInfo = {
id: String(buildData.build_num),
@@ -56,7 +60,8 @@ const transform = (buildsData: BuildSummary[]): CITableBuildInfo[] => {
? buildData.subject +
(buildData.retry_of ? ` (retry of #${buildData.retry_of})` : '')
: '',
onRetryClick: () => {}, //api.retry(String(buildData.build_num)),
onRetryClick: () =>
dispatch.builds.restartBuild({ buildId: buildData.build_num, api }),
source: {
branchName: String(buildData.branch),
commit: {
@@ -90,7 +95,7 @@ export const Builds: FC<{}> = () => {
}, []);
const { builds } = useSelector((state: iRootState) => state.builds);
const { repo, owner } = useSelector((state: iRootState) => state.settings);
const transformedBuilds = transform(builds);
const transformedBuilds = transform(builds, dispatch, api);
return (
<CITable builds={transformedBuilds} projectName={`${owner}/${repo}`} />
@@ -4,9 +4,9 @@ import React, { FC } from 'react';
import {
Link,
CircularProgress,
Button,
Typography,
Box,
IconButton,
} from '@material-ui/core';
import { Replay as RetryIcon, GitHub as GithubIcon } from '@material-ui/icons';
import { Link as RouterLink } from 'react-router-dom';
@@ -142,17 +142,19 @@ const generatedColumns: TableColumn[] = [
{
title: 'Status',
render: (row: Partial<CITableBuildInfo>) => (
<>
{getStatusComponent(row.status)} {row.status}
</>
<Box display="flex" alignItems="center">
{getStatusComponent(row.status)}
<Box mr={1} />
<Typography variant="button">{row.status}</Typography>
</Box>
),
},
{
title: 'Actions',
render: (row: Partial<CITableBuildInfo>) => (
<Button onClick={row.onRetryClick}>
<IconButton onClick={row.onRetryClick}>
<RetryIcon />
</Button>
</IconButton>
),
// @ts-ignore
width: '10%',
@@ -12,11 +12,12 @@ import { useDispatch, useSelector } from 'react-redux';
const BuildName: FC<{ build: BuildWithSteps | null }> = ({ build }) => (
<>
#{build?.build_num} - {build?.branch}
#{build?.build_num} - {build?.subject}
</>
);
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
neutral: {},
failed: {
position: 'relative',
'&:after': {
@@ -30,6 +31,19 @@ const useStyles = makeStyles((theme) => ({
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,
},
@@ -48,6 +62,21 @@ const useStyles = makeStyles((theme) => ({
},
}));
const pickClassName = (
classes: ReturnType<typeof useStyles>,
build: BuildWithSteps = {} as BuildWithSteps,
) => {
switch (true) {
case build.failed:
return classes.failed;
case ['running', 'queued'].includes(build.status!):
return classes.running;
case build.status === 'success':
return classes.success;
default:
return classes.neutral;
}
};
export const DetailedViewPage: FC<{}> = () => {
let { buildId = '' } = useParams();
const classes = useStyles();
@@ -60,7 +89,8 @@ export const DetailedViewPage: FC<{}> = () => {
dispatch.buildWithSteps.stopPolling();
};
}, []);
const { build } = useSelector((state: iRootState) => state.buildWithSteps);
const { builds } = useSelector((state: iRootState) => state.buildWithSteps);
const build = builds[parseInt(buildId, 10)];
return (
<Layout>
@@ -70,7 +100,7 @@ export const DetailedViewPage: FC<{}> = () => {
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard
className={build?.failed ? classes.failed : classes.success}
className={pickClassName(classes, build)}
title={<BuildName build={build} />}
cardClassName={classes.cardContent}
>
@@ -5,13 +5,13 @@ import {
Typography,
ExpansionPanelDetails,
} 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,
@@ -29,7 +29,7 @@ export const ActionOutput: FC<{
name: string;
className?: string;
action: BuildStepAction;
}> = ({ url, name, className }) => {
}> = ({ url, name, className, action }) => {
const classes = useStyles();
const [messages, setMessages] = useState([]);
@@ -43,6 +43,12 @@ export const ActionOutput: FC<{
);
});
}, [url]);
const timeElapsed = moment
.duration(
moment(action.end_time || moment()).diff(moment(action.start_time)),
)
.humanize();
return (
<ExpansionPanel
TransitionProps={{ unmountOnExit: true }}
@@ -56,7 +62,9 @@ export const ActionOutput: FC<{
className: classes.button,
}}
>
<Typography variant="button">{name}</Typography>
<Typography variant="button">
{name} ({timeElapsed})
</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails className={classes.expansionPanelDetails}>
{messages.length === 0 ? (
@@ -3,7 +3,7 @@ import { GitType, BuildWithSteps } from 'circleci-api';
import { CircleCIApi } from 'api';
export type BuildState = {
build: BuildWithSteps | null;
builds: Record<number, BuildWithSteps>;
pollingIntervalId: number | null;
pollingState: PollingState;
};
@@ -16,7 +16,7 @@ export enum PollingState {
}
export const buildWithSteps = {
state: {
build: null,
builds: {},
pollingIntervalId: null,
pollingState: PollingState.Idle,
} as BuildState,
@@ -25,7 +25,10 @@ export const buildWithSteps = {
if (state.pollingState !== PollingState.Polling) {
return state;
}
return { ...state, build: payload };
return {
...state,
builds: { ...state.builds, [payload.build_num!]: payload },
};
},
setPollingIntervalId(state: BuildState, payload: number | null) {
return {
@@ -52,6 +52,24 @@ export const builds = {
console.log(e);
}
},
async restartBuild(
{ api, buildId }: { api: CircleCIApi; buildId: number },
state: iRootState,
) {
try {
await api.retry(buildId, {
token: state.settings.token,
vcs: {
owner: state.settings.owner,
repo: state.settings.repo,
type: GitType.GITHUB,
},
});
} catch (e) {
console.log(e);
}
},
startPolling(api: CircleCIApi, state: iRootState) {
if (state.builds.pollingIntervalId) return;