diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts index c44988509f..44f081878f 100644 --- a/plugins/circleci/src/api/index.ts +++ b/plugins/circleci/src/api/index.ts @@ -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) { diff --git a/plugins/circleci/src/assets/circle-logo-badge-white-15.png b/plugins/circleci/src/assets/circle-logo-badge-white-15.png new file mode 100644 index 0000000000..4d094c9c57 Binary files /dev/null and b/plugins/circleci/src/assets/circle-logo-badge-white-15.png differ diff --git a/plugins/circleci/src/components/Layout/Layout.tsx b/plugins/circleci/src/components/Layout/Layout.tsx index cea9bc4081..81f5f68d44 100644 --- a/plugins/circleci/src/components/Layout/Layout.tsx +++ b/plugins/circleci/src/components/Layout/Layout.tsx @@ -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 }) => ( -
}> +
+ + Circle CI Plugin + + } + >
diff --git a/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx b/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx index 25719c3f42..f875c54f02 100644 --- a/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx +++ b/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx @@ -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 ( - {notRoot && ( - - - - )} - {title} - - ) as any - } + title={title} + titleComponent={() => ( + + {notRoot && ( + + + + )} + {title} + + )} > {!isSettingsPage && ( + ), // @ts-ignore width: '10%', diff --git a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx index d8ee53114a..7562409536 100644 --- a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx +++ b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx @@ -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, + 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 ( @@ -70,7 +100,7 @@ export const DetailedViewPage: FC<{}> = () => { } cardClassName={classes.cardContent} > diff --git a/plugins/circleci/src/pages/DetailedViewPage/lib/ActionOutput/ActionOutput.tsx b/plugins/circleci/src/pages/DetailedViewPage/lib/ActionOutput/ActionOutput.tsx index 18293b790b..63d78412a0 100644 --- a/plugins/circleci/src/pages/DetailedViewPage/lib/ActionOutput/ActionOutput.tsx +++ b/plugins/circleci/src/pages/DetailedViewPage/lib/ActionOutput/ActionOutput.tsx @@ -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 ( - {name} + + {name} ({timeElapsed}) + {messages.length === 0 ? ( diff --git a/plugins/circleci/src/state/models/buildWithSteps.ts b/plugins/circleci/src/state/models/buildWithSteps.ts index 109ee08df0..4bde261f09 100644 --- a/plugins/circleci/src/state/models/buildWithSteps.ts +++ b/plugins/circleci/src/state/models/buildWithSteps.ts @@ -3,7 +3,7 @@ import { GitType, BuildWithSteps } from 'circleci-api'; import { CircleCIApi } from 'api'; export type BuildState = { - build: BuildWithSteps | null; + builds: Record; 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 { diff --git a/plugins/circleci/src/state/models/builds.ts b/plugins/circleci/src/state/models/builds.ts index a260bfb249..f427175bd8 100644 --- a/plugins/circleci/src/state/models/builds.ts +++ b/plugins/circleci/src/state/models/builds.ts @@ -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;