Skip to content

Commit 5acf2a7

Browse files
Display information based off the previous run (jenkinsci#626)
* Calculate the percentage complete based off the previous run * Add very rough progress ring icon * Add animations for icons * Init * Move the timing string generation to the frontend * Rough skeletons * Spooky scary skeletons * Align makeTimeSpanString with what is in core * Mostly remove completePercent from the API (defaults to 0) Signed-off-by: lewisbirks <22620804+lewisbirks@users.noreply.github.com> * Tidy * Update status-icon.tsx * Cleanup Signed-off-by: lewisbirks <22620804+lewisbirks@users.noreply.github.com> * Restore match * Estimate stage completion percentage Signed-off-by: lewisbirks <22620804+lewisbirks@users.noreply.github.com> * Linting Signed-off-by: lewisbirks <22620804+lewisbirks@users.noreply.github.com> * Pull out API polling * Tidy up * Update merger.spec.ts * Timings -> timings + tsx -> ts Signed-off-by: lewisbirks <22620804+lewisbirks@users.noreply.github.com> * Tidy up * Undo unneeded java changes Signed-off-by: lewisbirks <22620804+lewisbirks@users.noreply.github.com> * Pull URL from element * More reversions Signed-off-by: lewisbirks <22620804+lewisbirks@users.noreply.github.com> * Update nodes.tsx * Rename stuff * Move files * Add test for run-estimator * Trigger Build * Rename complete * Update ConsoleLogCard.spec.tsx * Populate map at initialisation Define complete action once Use braces on throws Signed-off-by: lewisbirks <22620804+lewisbirks@users.noreply.github.com> * Add click state to status symbols * Add guard for previous build url Signed-off-by: lewisbirks <22620804+lewisbirks@users.noreply.github.com> * Drop skeletons if stages differ, simplify * Fix animation when switching between clickable and not clickable --------- Signed-off-by: lewisbirks <22620804+lewisbirks@users.noreply.github.com> Co-authored-by: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
1 parent 591d3a8 commit 5acf2a7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1223
-665
lines changed

src/main/frontend/common/RestClient.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55

66
export interface RunStatus {
77
stages: StageInfo[];
8-
isComplete: boolean;
8+
complete: boolean;
99
}
1010

1111
/**
@@ -19,9 +19,9 @@ export interface StepInfo {
1919
id: string;
2020
type: string;
2121
stageId: string;
22-
pauseDurationMillis: string;
23-
startTimeMillis: string;
24-
totalDurationMillis: string;
22+
pauseDurationMillis: number;
23+
startTimeMillis: number;
24+
totalDurationMillis: number;
2525
}
2626

2727
// Internal representation of console log.
@@ -38,6 +38,22 @@ export interface ConsoleLogData {
3838
endByte: number;
3939
}
4040

41+
export async function getRunStatusFromPath(
42+
url: string,
43+
): Promise<RunStatus | null> {
44+
try {
45+
const response = await fetch(url + "/pipeline-graph/tree");
46+
if (!response.ok) {
47+
throw response.statusText;
48+
}
49+
let json = await response.json();
50+
return json.data;
51+
} catch (e) {
52+
console.error(`Caught error getting tree: '${e}'`);
53+
return null;
54+
}
55+
}
56+
4157
export async function getRunStatus(): Promise<RunStatus | null> {
4258
try {
4359
let response = await fetch("tree");
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
.pgv-status-icon {
2+
* {
3+
transition: var(--elastic-transition);
4+
transform-origin: center;
5+
}
6+
7+
.pgv-scale {
8+
animation: pulseScale 2s both ease-in-out infinite;
9+
opacity: 0.25;
10+
11+
@keyframes pulseScale {
12+
50% {
13+
scale: 2.25;
14+
opacity: 1;
15+
}
16+
}
17+
}
18+
}
19+
20+
// TODO - can be removed when Jenkins >= 2.506
21+
.jenkins-\!-skipped-color {
22+
--color: var(--text-color-secondary);
23+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import React from "react";
2+
import "./status-icon.scss";
3+
import { Result } from "../../pipeline-graph-view/pipeline-graph/main";
4+
5+
/**
6+
* Visual representation of a job or build status
7+
*/
8+
export default function StatusIcon({
9+
status,
10+
percentage,
11+
skeleton,
12+
}: StatusIconProps) {
13+
const viewBoxSize = 512;
14+
const strokeWidth = status === "running" ? 50 : 0;
15+
const radius = (viewBoxSize - strokeWidth) / 2.2;
16+
const circumference = 2 * Math.PI * radius;
17+
const offset = circumference - ((percentage ?? 100) / 100) * circumference;
18+
19+
return (
20+
<svg
21+
viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
22+
className={"pgv-status-icon " + resultToColor(status, skeleton)}
23+
opacity={skeleton ? 0.5 : 1}
24+
>
25+
<circle
26+
cx={viewBoxSize / 2}
27+
cy={viewBoxSize / 2}
28+
r={radius}
29+
fill="oklch(from var(--color) l c h / 0.15)"
30+
style={{
31+
transition: "var(--standard-transition)",
32+
}}
33+
/>
34+
<circle
35+
cx={viewBoxSize / 2}
36+
cy={viewBoxSize / 2}
37+
r={radius - 10}
38+
fill="none"
39+
stroke="var(--color)"
40+
strokeWidth={20}
41+
strokeOpacity={0.15}
42+
/>
43+
<circle
44+
cx={viewBoxSize / 2}
45+
cy={viewBoxSize / 2}
46+
r={radius}
47+
fill="none"
48+
stroke="var(--color)"
49+
strokeWidth={strokeWidth}
50+
strokeLinecap="round"
51+
strokeDasharray={circumference}
52+
strokeDashoffset={offset}
53+
style={{
54+
transform: "rotate(-90deg)",
55+
transformOrigin: "50% 50%",
56+
transition: "var(--standard-transition)",
57+
}}
58+
/>
59+
60+
<Group currentStatus={status} status={Result.running}>
61+
<circle
62+
cx="256"
63+
cy="256"
64+
r="40"
65+
fill="var(--color)"
66+
className={status === "running" ? "pgv-scale" : ""}
67+
/>
68+
</Group>
69+
70+
<Group currentStatus={status} status={Result.success}>
71+
<path
72+
d="M336 189L224 323L176 269.4"
73+
fill="transparent"
74+
stroke="var(--color)"
75+
strokeWidth={32}
76+
strokeLinecap="round"
77+
strokeLinejoin="round"
78+
/>
79+
</Group>
80+
81+
<Group currentStatus={status} status={Result.failure}>
82+
<path
83+
fill="none"
84+
stroke="var(--color)"
85+
strokeLinecap="round"
86+
strokeLinejoin="round"
87+
strokeWidth={32}
88+
d="M320 320L192 192M192 320l128-128"
89+
/>
90+
</Group>
91+
92+
<Group currentStatus={status} status={Result.aborted}>
93+
<path
94+
fill="none"
95+
stroke="var(--color)"
96+
strokeLinecap="round"
97+
strokeLinejoin="round"
98+
strokeWidth={32}
99+
d="M192 320l128-128"
100+
/>
101+
</Group>
102+
103+
<Group currentStatus={status} status={Result.unstable}>
104+
<path
105+
d="M250.26 166.05L256 288l5.73-121.95a5.74 5.74 0 00-5.79-6h0a5.74 5.74 0 00-5.68 6z"
106+
fill="none"
107+
stroke="var(--color)"
108+
strokeLinecap="round"
109+
strokeLinejoin="round"
110+
strokeWidth={32}
111+
/>
112+
<ellipse cx="256" cy="350" rx="26" ry="26" fill="var(--color)" />
113+
</Group>
114+
115+
<Group currentStatus={status} status={Result.skipped}>
116+
<path
117+
d="M320 176a16 16 0 00-16 16v53l-111.68-67.44a10.78 10.78 0 00-16.32 9.31v138.26a10.78 10.78 0 0016.32 9.31L304 267v53a16 16 0 0032 0V192a16 16 0 00-16-16z"
118+
fill="var(--color)"
119+
/>
120+
</Group>
121+
122+
<Group currentStatus={status} status={Result.paused}>
123+
<path
124+
fill="none"
125+
stroke="var(--color)"
126+
strokeLinecap="round"
127+
strokeMiterlimit="10"
128+
strokeWidth={32}
129+
d="M208 192v128M304 192v128"
130+
/>
131+
</Group>
132+
133+
<Group currentStatus={status} status={Result.not_built}>
134+
<circle cx="256" cy="256" r="30" fill="var(--color)" />
135+
<circle cx="352" cy="256" r="30" fill="var(--color)" />
136+
<circle cx="160" cy="256" r="30" fill="var(--color)" />
137+
</Group>
138+
139+
<Group currentStatus={status} status={Result.unknown}>
140+
<path
141+
d="M200 202.29s.84-17.5 19.57-32.57C230.68 160.77 244 158.18 256 158c10.93-.14 20.69 1.67 26.53 4.45 10 4.76 29.47 16.38 29.47 41.09 0 26-17 37.81-36.37 50.8S251 281.43 251 296"
142+
fill="none"
143+
stroke="var(--color)"
144+
strokeLinecap="round"
145+
strokeMiterlimit="10"
146+
strokeWidth="28"
147+
/>
148+
<circle cx="250" cy="348" r="20" fill="var(--color)" />
149+
</Group>
150+
</svg>
151+
);
152+
}
153+
154+
function Group({
155+
currentStatus,
156+
status,
157+
children,
158+
}: {
159+
currentStatus: Result;
160+
status: Result;
161+
children: React.ReactNode;
162+
}) {
163+
return (
164+
<g
165+
style={{
166+
transform: currentStatus !== status ? "scale(0)" : "scale(1)",
167+
opacity: currentStatus !== status ? 0 : 1,
168+
}}
169+
>
170+
{children}
171+
</g>
172+
);
173+
}
174+
175+
export function resultToColor(result: Result, skeleton: boolean | undefined) {
176+
if (skeleton) {
177+
return "jenkins-!-skipped-color";
178+
}
179+
180+
switch (result) {
181+
case "success":
182+
return "jenkins-!-success-color";
183+
case "failure":
184+
return "jenkins-!-error-color";
185+
case "running":
186+
return "jenkins-!-accent-color";
187+
case "unstable":
188+
return "jenkins-!-warning-color";
189+
default:
190+
return "jenkins-!-skipped-color";
191+
}
192+
}
193+
194+
interface StatusIconProps {
195+
status: Result;
196+
percentage?: number;
197+
skeleton?: boolean;
198+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { useEffect, useState } from "react";
2+
import { getRunStatusFromPath } from "./RestClient";
3+
import { StageInfo } from "../pipeline-graph-view/pipeline-graph/main";
4+
import startPollingPipelineStatus from "../pipeline-graph-view/pipeline-graph/main/support/startPollingPipelineStatus";
5+
import { mergeStageInfos } from "./utils/stage-merge";
6+
7+
/**
8+
* Polls a run, stopping once the run has completed
9+
* Optionally retrieves data from the prior run and overlays the new run on top
10+
*/
11+
export default function useRunPoller({
12+
currentRunPath,
13+
previousRunPath,
14+
}: RunPollerProps) {
15+
const [run, setRun] = useState<Run>();
16+
17+
useEffect(() => {
18+
const onPipelineComplete = () => undefined;
19+
20+
if (previousRunPath) {
21+
getRunStatusFromPath(previousRunPath).then((r) => {
22+
// This should be a Result - not 'complete'
23+
const onPipelineDataReceived = (data: {
24+
stages: StageInfo[];
25+
complete: boolean;
26+
}) => {
27+
setRun({
28+
stages: mergeStageInfos(r!.stages, data.stages),
29+
});
30+
};
31+
32+
const onPollingError = (err: Error) => {
33+
console.log(
34+
"There was an error when polling the pipeline status",
35+
err,
36+
);
37+
};
38+
39+
startPollingPipelineStatus(
40+
onPipelineDataReceived,
41+
onPollingError,
42+
onPipelineComplete,
43+
currentRunPath,
44+
);
45+
});
46+
} else {
47+
const onPipelineDataReceived = (data: { stages: StageInfo[] }) => {
48+
setRun({
49+
stages: data.stages,
50+
});
51+
};
52+
53+
const onPollingError = (err: Error) => {
54+
console.log("There was an error when polling the pipeline status", err);
55+
};
56+
57+
startPollingPipelineStatus(
58+
onPipelineDataReceived,
59+
onPollingError,
60+
onPipelineComplete,
61+
currentRunPath,
62+
);
63+
}
64+
}, []);
65+
66+
return {
67+
run,
68+
};
69+
}
70+
71+
interface Run {
72+
stages: StageInfo[];
73+
}
74+
75+
interface RunPollerProps {
76+
currentRunPath: string;
77+
previousRunPath?: string;
78+
}

0 commit comments

Comments
 (0)