Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f23148b
Calculate the percentage complete based off the previous run
lewisbirks Apr 15, 2025
7a7c18d
Add very rough progress ring icon
janfaracik Apr 15, 2025
cf1654d
Merge branch 'main' into complete-percentage
janfaracik Apr 15, 2025
ed93e2e
Add animations for icons
janfaracik Apr 15, 2025
a1ec0e6
Init
janfaracik Apr 16, 2025
a0c269c
Move the timing string generation to the frontend
lewisbirks Apr 16, 2025
c53dc18
Rough skeletons
janfaracik Apr 16, 2025
0fddc41
Merge branch 'refine-graph' into pr/626
janfaracik Apr 16, 2025
5b37c66
Spooky scary skeletons
janfaracik Apr 16, 2025
e50e69c
Align makeTimeSpanString with what is in core
lewisbirks Apr 16, 2025
a826ed5
Mostly remove completePercent from the API (defaults to 0)
lewisbirks Apr 16, 2025
13cfed2
Tidy
janfaracik Apr 16, 2025
a32b099
Update status-icon.tsx
janfaracik Apr 16, 2025
5f4cd0a
Cleanup
lewisbirks Apr 16, 2025
969c83a
Restore match
janfaracik Apr 17, 2025
e9df704
Estimate stage completion percentage
lewisbirks Apr 17, 2025
c2345d1
Linting
lewisbirks Apr 17, 2025
ada21af
Pull out API polling
janfaracik Apr 17, 2025
b299320
Tidy up
janfaracik Apr 17, 2025
ba1e639
Update merger.spec.ts
janfaracik Apr 17, 2025
ec24614
Timings -> timings + tsx -> ts
lewisbirks Apr 17, 2025
dcb8fdc
Tidy up
janfaracik Apr 17, 2025
5ca5b38
Undo unneeded java changes
lewisbirks Apr 17, 2025
1d7b234
Pull URL from element
janfaracik Apr 17, 2025
78ad78f
More reversions
lewisbirks Apr 17, 2025
d370cfa
Update nodes.tsx
janfaracik Apr 17, 2025
38fff35
Rename stuff
janfaracik Apr 17, 2025
df7f2e7
Move files
janfaracik Apr 17, 2025
a0e02ca
Add test for run-estimator
lewisbirks Apr 17, 2025
af5df92
Trigger Build
janfaracik Apr 17, 2025
79e2b9d
Rename complete
janfaracik Apr 17, 2025
f794af6
Update ConsoleLogCard.spec.tsx
janfaracik Apr 17, 2025
b86137c
Populate map at initialisation
lewisbirks Apr 17, 2025
f6870db
Add click state to status symbols
janfaracik Apr 17, 2025
b7db0ee
Add guard for previous build url
lewisbirks Apr 17, 2025
4febd0b
Merge branch 'main' into complete-percentage
janfaracik Apr 18, 2025
5dc93f8
Drop skeletons if stages differ, simplify
janfaracik Apr 18, 2025
0420490
Fix animation when switching between clickable and not clickable
janfaracik Apr 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions src/main/frontend/common/RestClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {

export interface RunStatus {
stages: StageInfo[];
isComplete: boolean;
complete: boolean;
}

/**
Expand All @@ -19,9 +19,9 @@ export interface StepInfo {
id: string;
type: string;
stageId: string;
pauseDurationMillis: string;
startTimeMillis: string;
totalDurationMillis: string;
pauseDurationMillis: number;
startTimeMillis: number;
totalDurationMillis: number;
}

// Internal representation of console log.
Expand All @@ -38,6 +38,20 @@ export interface ConsoleLogData {
endByte: number;
}

export async function getRunStatusFromPath(
url: string,
): Promise<RunStatus | null> {
try {
let response = await fetch(url + "/pipeline-graph/tree");
if (!response.ok) throw response.statusText;
let json = await response.json();
return json.data;
} catch (e) {
console.error(`Caught error getting tree: '${e}'`);
return null;
}
}

export async function getRunStatus(): Promise<RunStatus | null> {
try {
let response = await fetch("tree");
Expand Down
23 changes: 23 additions & 0 deletions src/main/frontend/common/components/status-icon.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.pgv-status-icon {
* {
transition: var(--elastic-transition);
transform-origin: center;
}

.pgv-scale {
animation: pulseScale 2s both ease-in-out infinite;
opacity: 0.25;

@keyframes pulseScale {
50% {
scale: 2.25;
opacity: 1;
}
}
}
}

// TODO - can be removed when Jenkins >= 2.506
.jenkins-\!-skipped-color {
--color: var(--text-color-secondary);
}
191 changes: 191 additions & 0 deletions src/main/frontend/common/components/status-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import React from "react";
import "./status-icon.scss";
import { Result } from "../../pipeline-graph-view/pipeline-graph/main";

/**
* Visual representation of a job or build status
*/
export default function StatusIcon({
status,
percentage,
skeleton,
}: StatusIconProps) {
const viewBoxSize = 512;
const strokeWidth = status === "running" ? 50 : 0;
const radius = (viewBoxSize - strokeWidth) / 2.2;
const circumference = 2 * Math.PI * radius;
const offset = circumference - ((percentage ?? 100) / 100) * circumference;

return (
<svg
viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
className={"pgv-status-icon " + resultToColor(status, skeleton)}
opacity={skeleton ? 0.5 : 1}
>
<circle
cx={viewBoxSize / 2}
cy={viewBoxSize / 2}
r={radius}
fill="oklch(from var(--color) l c h / 0.15)"
stroke="color-mix(in oklch, var(--color) 25%, var(--card-background))"
strokeWidth={20}
style={{
transition: "var(--standard-transition)",
}}
/>
<circle
cx={viewBoxSize / 2}
cy={viewBoxSize / 2}
r={radius}
fill="none"
stroke="var(--color)"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={offset}
style={{
transform: "rotate(-90deg)",
transformOrigin: "50% 50%",
transition: "var(--standard-transition)",
}}
/>

<Group currentStatus={status} status={Result.running}>
<circle
cx="256"
cy="256"
r="40"
fill="var(--color)"
className={status === "running" ? "pgv-scale" : ""}
/>
</Group>

<Group currentStatus={status} status={Result.success}>
<path
d="M336 189L224 323L176 269.4"
fill="transparent"
stroke="var(--color)"
strokeWidth={32}
strokeLinecap="round"
strokeLinejoin="round"
/>
</Group>

<Group currentStatus={status} status={Result.failure}>
<path
fill="none"
stroke="var(--color)"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={32}
d="M320 320L192 192M192 320l128-128"
/>
</Group>

<Group currentStatus={status} status={Result.aborted}>
<path
fill="none"
stroke="var(--color)"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={32}
d="M192 320l128-128"
/>
</Group>

<Group currentStatus={status} status={Result.unstable}>
<path
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"
fill="none"
stroke="var(--color)"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={32}
/>
<ellipse cx="256" cy="350" rx="26" ry="26" fill="var(--color)" />
</Group>

<Group currentStatus={status} status={Result.skipped}>
<path
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"
fill="var(--color)"
/>
</Group>

<Group currentStatus={status} status={Result.paused}>
<path
fill="none"
stroke="var(--color)"
strokeLinecap="round"
strokeMiterlimit="10"
strokeWidth={32}
d="M208 192v128M304 192v128"
/>
</Group>

<Group currentStatus={status} status={Result.not_built}>
<circle cx="256" cy="256" r="30" fill="var(--color)" />
<circle cx="352" cy="256" r="30" fill="var(--color)" />
<circle cx="160" cy="256" r="30" fill="var(--color)" />
</Group>

<Group currentStatus={status} status={Result.unknown}>
<path
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"
fill="none"
stroke="var(--color)"
strokeLinecap="round"
strokeMiterlimit="10"
strokeWidth="28"
/>
<circle cx="250" cy="348" r="20" fill="var(--color)" />
</Group>
</svg>
);
}

function Group({
currentStatus,
status,
children,
}: {
currentStatus: Result;
status: Result;
children: React.ReactNode;
}) {
return (
<g
style={{
transform: currentStatus !== status ? "scale(0)" : "scale(1)",
opacity: currentStatus !== status ? 0 : 1,
}}
>
{children}
</g>
);
}

function resultToColor(result: Result, skeleton: boolean | undefined) {
if (skeleton) {
return "jenkins-!-skipped-color";
}

switch (result) {
case "success":
return "jenkins-!-success-color";
case "failure":
return "jenkins-!-error-color";
case "running":
return "jenkins-!-accent-color";
case "unstable":
return "jenkins-!-warning-color";
default:
return "jenkins-!-skipped-color";
}
}

interface StatusIconProps {
status: Result;
percentage?: number;
skeleton?: boolean;
}
80 changes: 80 additions & 0 deletions src/main/frontend/common/tree-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useEffect, useState } from "react";
import { getRunStatusFromPath } from "./RestClient";
import { StageInfo } from "../pipeline-graph-view/pipeline-graph/main";
import startPollingPipelineStatus from "../pipeline-graph-view/pipeline-graph/main/support/startPollingPipelineStatus";
import { mergeStageInfos } from "./utils/stage-merge";

/**
* Polls a run, stopping once the run has completed
* Optionally retrieves data from the prior run and overlays the new run on top
*/
export default function useRunPoller({
currentRunPath,
previousRunPath,
}: RunPollerProps) {
const [run, setRun] = useState<Run>();

useEffect(() => {
if (previousRunPath) {
getRunStatusFromPath(previousRunPath).then((r) => {
// This should be a Result - not 'complete'
const onPipelineDataReceived = (data: {
stages: StageInfo[];
complete: boolean;
}) => {
setRun({
stages: mergeStageInfos(r!.stages, data.stages),
});
};

const onPollingError = (err: Error) => {
console.log(
"There was an error when polling the pipeline status",
err,
);
};

const onPipelineComplete = () => undefined;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be defined before the if statement so you don't repeat it on L#57

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense for now as they are the same, eventually they might differ, but we can deal with that then


startPollingPipelineStatus(
onPipelineDataReceived,
onPollingError,
onPipelineComplete,
currentRunPath,
);
});
} else {
const onPipelineDataReceived = (data: { stages: StageInfo[] }) => {
setRun({
stages: data.stages,
});
};

const onPollingError = (err: Error) => {
console.log("There was an error when polling the pipeline status", err);
};

const onPipelineComplete = () => undefined;

startPollingPipelineStatus(
onPipelineDataReceived,
onPollingError,
onPipelineComplete,
currentRunPath,
);
}
}, []);

return {
run,
};
}

interface Run {
stages: StageInfo[];
}

interface RunPollerProps {
currentRunPath: string;
previousRunPath?: string;
}
Loading