feat: good plugin state
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user