Skip to content

Commit 06551e8

Browse files
Leemalin Moodleyrockleez
authored andcommitted
fix: stage should show pause icon when waiting for input #967
1 parent 203d281 commit 06551e8

File tree

5 files changed

+103
-16
lines changed

5 files changed

+103
-16
lines changed

src/main/frontend/common/components/status-icon.spec.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,23 @@ describe("StatusIcon", () => {
127127
expect(result.current).to.equal(0);
128128
unmount();
129129
});
130+
it("should continue progress when waitingForInput is true", async () => {
131+
const { result, unmount } = renderHook(() => {
132+
return useStageProgress({
133+
...mockStage,
134+
state: Result.running,
135+
waitingForInput: true,
136+
startTimeMillis: now - 2_000,
137+
previousTotalDurationMillis: 20_000,
138+
});
139+
});
140+
expect(result.current).to.equal(10);
141+
await act(() => vi.advanceTimersByTime(1_000));
142+
expect(result.current).to.equal(15);
143+
await act(() => vi.advanceTimersByTime(1_000));
144+
expect(result.current).to.equal(20);
145+
unmount();
146+
});
130147
});
131148
describe("other states", function () {
132149
for (const state in Result) {

src/main/frontend/common/components/status-icon.tsx

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ import {
1010
export function useStageProgress(stage: StageInfo) {
1111
const [percentage, setPercentage] = useState(0);
1212
useEffect(() => {
13-
if (stage.state !== Result.running) {
14-
// percentage is only needed for the running icon.
15-
setPercentage(0);
13+
if (stage.state !== Result.running || stage.waitingForInput) {
14+
// percentage is only needed for the running icon (and not when paused)
1615
return;
1716
}
1817
const update = () => {
@@ -29,6 +28,7 @@ export function useStageProgress(stage: StageInfo) {
2928
stage.startTimeMillis,
3029
stage.totalDurationMillis,
3130
stage.previousTotalDurationMillis,
31+
stage.waitingForInput,
3232
]);
3333
return percentage;
3434
}
@@ -37,6 +37,7 @@ export function StageStatusIcon({ stage }: { stage: StageInfo }) {
3737
return (
3838
<StatusIcon
3939
status={stage.state}
40+
waitingForInput={stage.waitingForInput}
4041
percentage={useStageProgress(stage)}
4142
skeleton={stage.skeleton}
4243
/>
@@ -48,11 +49,14 @@ export function StageStatusIcon({ stage }: { stage: StageInfo }) {
4849
*/
4950
export default function StatusIcon({
5051
status,
52+
waitingForInput,
5153
percentage,
5254
skeleton,
5355
}: StatusIconProps) {
5456
const viewBoxSize = 512;
55-
const strokeWidth = status === "running" ? 50 : 0;
57+
const iconStatus = waitingForInput && status === Result.running ? Result.paused : status;
58+
// Keep ring when underlying status is running (even if visually paused)
59+
const strokeWidth = status === Result.running ? 50 : 0;
5660
const radius = (viewBoxSize - strokeWidth) / 2.2;
5761
const circumference = 2 * Math.PI * radius;
5862
const offset = circumference - ((percentage ?? 100) / 100) * circumference;
@@ -63,7 +67,7 @@ export default function StatusIcon({
6367
className={"pgv-status-icon " + resultToColor(status, skeleton)}
6468
opacity={skeleton ? 0.5 : 1}
6569
role={"img"}
66-
aria-label={status}
70+
aria-label={iconStatus}
6771
>
6872
<circle
6973
cx={viewBoxSize / 2}
@@ -107,7 +111,7 @@ export default function StatusIcon({
107111
}}
108112
/>
109113

110-
<Group currentStatus={status} status={Result.running}>
114+
<Group currentStatus={iconStatus} status={Result.running}>
111115
<circle
112116
cx="256"
113117
cy="256"
@@ -117,7 +121,7 @@ export default function StatusIcon({
117121
/>
118122
</Group>
119123

120-
<Group currentStatus={status} status={Result.success}>
124+
<Group currentStatus={iconStatus} status={Result.success}>
121125
<path
122126
d="M336 189L224 323L176 269.4"
123127
fill="transparent"
@@ -128,7 +132,7 @@ export default function StatusIcon({
128132
/>
129133
</Group>
130134

131-
<Group currentStatus={status} status={Result.failure}>
135+
<Group currentStatus={iconStatus} status={Result.failure}>
132136
<path
133137
fill="none"
134138
stroke="var(--color)"
@@ -139,7 +143,7 @@ export default function StatusIcon({
139143
/>
140144
</Group>
141145

142-
<Group currentStatus={status} status={Result.aborted}>
146+
<Group currentStatus={iconStatus} status={Result.aborted}>
143147
<path
144148
fill="none"
145149
stroke="var(--color)"
@@ -150,7 +154,7 @@ export default function StatusIcon({
150154
/>
151155
</Group>
152156

153-
<Group currentStatus={status} status={Result.unstable}>
157+
<Group currentStatus={iconStatus} status={Result.unstable}>
154158
<path
155159
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"
156160
fill="none"
@@ -162,7 +166,7 @@ export default function StatusIcon({
162166
<ellipse cx="256" cy="350" rx="26" ry="26" fill="var(--color)" />
163167
</Group>
164168

165-
<Group currentStatus={status} status={Result.skipped}>
169+
<Group currentStatus={iconStatus} status={Result.skipped}>
166170
<g transform="scale(0.8)">
167171
<path
168172
fill="none"
@@ -185,7 +189,7 @@ export default function StatusIcon({
185189
</g>
186190
</Group>
187191

188-
<Group currentStatus={status} status={Result.paused}>
192+
<Group currentStatus={iconStatus} status={Result.paused}>
189193
<path
190194
fill="none"
191195
stroke="var(--color)"
@@ -196,13 +200,13 @@ export default function StatusIcon({
196200
/>
197201
</Group>
198202

199-
<Group currentStatus={status} status={Result.not_built}>
203+
<Group currentStatus={iconStatus} status={Result.not_built}>
200204
<circle cx="256" cy="256" r="30" fill="var(--color)" />
201205
<circle cx="352" cy="256" r="30" fill="var(--color)" />
202206
<circle cx="160" cy="256" r="30" fill="var(--color)" />
203207
</Group>
204208

205-
<Group currentStatus={status} status={Result.unknown}>
209+
<Group currentStatus={iconStatus} status={Result.unknown}>
206210
<path
207211
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"
208212
fill="none"
@@ -259,6 +263,8 @@ export function resultToColor(result: Result, skeleton: boolean | undefined) {
259263

260264
interface StatusIconProps {
261265
status: Result;
266+
/** True when a running stage is awaiting input; renders paused glyph but keeps running color & progress */
267+
waitingForInput?: boolean;
262268
percentage?: number;
263269
skeleton?: boolean;
264270
}

src/main/frontend/common/utils/refresh-stages-from-steps.spec.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ it("should handle skeleton stage with steps", () => {
1515
startTimeMillis: 123,
1616
totalDurationMillis: undefined,
1717
pauseLiveTotal: false,
18+
waitingForInput: false,
1819
},
1920
]);
2021
expect(stages).to.not.equal(originalStages);
@@ -36,6 +37,7 @@ it("should handle skeleton stage with started children", () => {
3637
startTimeMillis: 123,
3738
totalDurationMillis: undefined,
3839
pauseLiveTotal: false,
40+
waitingForInput: false,
3941
children: [
4042
{
4143
...mockStage,
@@ -44,6 +46,7 @@ it("should handle skeleton stage with started children", () => {
4446
startTimeMillis: 123,
4547
totalDurationMillis: undefined,
4648
pauseLiveTotal: false,
49+
waitingForInput: false,
4750
},
4851
],
4952
},
@@ -84,6 +87,7 @@ it("should handle finished not_built stage", () => {
8487
skeleton: false,
8588
totalDurationMillis: undefined,
8689
pauseLiveTotal: true,
90+
waitingForInput: false,
8791
},
8892
]);
8993
expect(stages).to.not.equal(originalStages);
@@ -111,6 +115,7 @@ it("should handle finished running stage", () => {
111115
skeleton: false,
112116
totalDurationMillis: undefined,
113117
pauseLiveTotal: true,
118+
waitingForInput: false,
114119
},
115120
]);
116121
expect(stages).to.not.equal(originalStages);
@@ -149,6 +154,7 @@ it("should handle finished stage with children", () => {
149154
totalDurationMillis: undefined,
150155
skeleton: false,
151156
pauseLiveTotal: true,
157+
waitingForInput: false,
152158
children: [
153159
{
154160
...mockStage,
@@ -158,6 +164,7 @@ it("should handle finished stage with children", () => {
158164
totalDurationMillis: undefined,
159165
skeleton: false,
160166
pauseLiveTotal: true,
167+
waitingForInput: false,
161168
},
162169
],
163170
},
@@ -168,6 +175,49 @@ it("should handle finished stage with children", () => {
168175
expect(stages[0].children[0]).to.not.equal(originalChildren[0]);
169176
});
170177

178+
it("should mark running stage waitingForInput when input step present", () => {
179+
const originalStages: StageInfo[] = [
180+
{ ...mockStage, state: Result.running, startTimeMillis: 42, skeleton: false },
181+
];
182+
const stages = refreshStagesFromSteps(originalStages, [
183+
{
184+
...mockStep,
185+
stageId: "1",
186+
state: Result.running,
187+
inputStep: { message: "msg", cancel: "Cancel", id: "x", ok: "OK", parameters: false },
188+
},
189+
]);
190+
expect(stages[0].state).to.equal(Result.running);
191+
expect(stages[0].waitingForInput).to.equal(true);
192+
});
193+
194+
it("should bubble waitingForInput if child branch paused", () => {
195+
const originalStages: StageInfo[] = [
196+
{
197+
...mockStage,
198+
state: Result.running,
199+
startTimeMillis: 42,
200+
skeleton: false,
201+
children: [
202+
{
203+
...mockStage,
204+
id: 2,
205+
state: Result.paused,
206+
startTimeMillis: 50,
207+
skeleton: false,
208+
totalDurationMillis: undefined,
209+
children: [],
210+
pauseDurationMillis: 0,
211+
url: "",
212+
},
213+
],
214+
},
215+
];
216+
const stages = refreshStagesFromSteps(originalStages, []);
217+
expect(stages[0].state).to.equal(Result.running);
218+
expect(stages[0].waitingForInput).to.equal(true);
219+
});
220+
171221
const mockStage: StageInfo = {
172222
name: "Build",
173223
state: Result.not_built,
@@ -193,4 +243,4 @@ const mockStep: StepInfo = {
193243
title: "",
194244
totalDurationMillis: 0,
195245
type: "",
196-
};
246+
};

src/main/frontend/common/utils/refresh-stages-from-steps.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ export function refreshStagesFromSteps(stages: StageInfo[], steps: StepInfo[]) {
2929
totalDurationMillis = undefined;
3030
}
3131
}
32+
// Derived front-end flag: if any step has an inputStep or any child is paused/waiting, mark waitingForInput.
33+
const waitingForInput =
34+
state === Result.running &&
35+
(stageSteps.some((s) => Boolean(s.inputStep)) ||
36+
children.some(
37+
(c) => c.waitingForInput || c.state === Result.paused,
38+
));
3239
// Best effort: Pause timer when a stage is expected to have finished to avoid having to decrement the total duration when done (as confirmed by the next run polling).
3340
pauseLiveTotal =
3441
startTimeMillis > 0 &&
@@ -40,14 +47,16 @@ export function refreshStagesFromSteps(stages: StageInfo[], steps: StepInfo[]) {
4047
Boolean(stage.pauseLiveTotal) !== pauseLiveTotal ||
4148
stage.startTimeMillis !== startTimeMillis ||
4249
stage.state !== state ||
43-
stage.totalDurationMillis !== totalDurationMillis
50+
stage.totalDurationMillis !== totalDurationMillis ||
51+
Boolean(stage.waitingForInput) !== waitingForInput
4452
) {
4553
// Update in-place to avoid frequent re-render, then trigger re-render.
4654
stage.children = children;
4755
stage.pauseLiveTotal = pauseLiveTotal;
4856
stage.startTimeMillis = startTimeMillis;
4957
stage.state = state;
5058
stage.totalDurationMillis = totalDurationMillis;
59+
stage.waitingForInput = waitingForInput;
5160
if (!changed) stages = stages.slice();
5261
changed = true;
5362
stages[idx] = { ...stage };

src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ export interface StageInfo {
5757

5858
skeleton?: boolean;
5959
pauseLiveTotal?: boolean;
60+
/**
61+
* Front-end derived flag indicating a running stage is awaiting input.
62+
* Used to override the icon to the paused glyph while retaining the running state for filters & color.
63+
*/
64+
waitingForInput?: boolean;
6065
}
6166

6267
interface BaseNodeInfo {

0 commit comments

Comments
 (0)