Skip to content

Commit 0e91dbe

Browse files
Add hiddenFromView step to reduce console noise (#1118)
Co-authored-by: Tim Jacomb <21194782+timja@users.noreply.github.com>
1 parent fd2b168 commit 0e91dbe

File tree

17 files changed

+360
-42
lines changed

17 files changed

+360
-42
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This plugin adds a visual representation of Jenkins pipelines, showing each stag
1818
- View logs in real time without leaving the interface
1919
- Toggle between graph and stage views; move and resize panes to suit your workflow
2020
- Quickly access details of each step and its results
21+
- Hide specific steps from view using the `hideFromView` Pipeline DSL step
2122
- Designed for better readability and faster troubleshooting
2223

2324
## Getting started
@@ -26,6 +27,8 @@ This plugin adds a visual representation of Jenkins pipelines, showing each stag
2627
2. Go to some pipeline build page (not the job page)
2728
3. Click _Pipeline Overview_
2829

30+
Hidden steps are not displayed by default in the Pipeline Overview, but can be toggled visible using the filter controls.
31+
2932
## Screenshots
3033

3134
Basic pipeline:
@@ -42,6 +45,31 @@ See a live demonstration from a Jenkins Contributor Summit:
4245

4346
[![Demo of Pipeline Graph View plugin](https://img.youtube.com/vi/MBI3MBY2eJ8/0.jpg)](https://www.youtube.com/watch?v=MBI3MBY2eJ8&t=3295 "Pipeline Graph View plugin")
4447

48+
## Pipeline DSL Extensions
49+
50+
### Hiding Steps from View
51+
52+
You can mark specific pipeline steps as hidden from the view by wrapping them with the `hideFromView` step:
53+
54+
```groovy
55+
pipeline {
56+
agent any
57+
stages {
58+
stage('Build') {
59+
steps {
60+
echo "This step is visible"
61+
62+
hideFromView {
63+
echo "This step is hidden by default"
64+
}
65+
66+
echo "This step is also visible"
67+
}
68+
}
69+
}
70+
}
71+
```
72+
4573
## REST API
4674

4775
The REST API documentation can be found [here](https://editor-next.swagger.io/?url=https://raw.githubusercontent.com/jenkinsci/pipeline-graph-view-plugin/refs/heads/main/openapi.yaml).

src/main/frontend/common/RestClient.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface StepInfo {
3838
pauseDurationMillis: number;
3939
startTimeMillis: number;
4040
totalDurationMillis: number;
41+
flags?: Record<string, unknown>;
4142
}
4243

4344
// Internal representation of console log.

src/main/frontend/common/components/filter.tsx

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@ import Tooltip from "./tooltip.tsx";
1212

1313
export default function Filter({ disabled }: FilterProps) {
1414
const [visible, setVisible] = useState(false);
15-
const { visibleStatuses, toggleStatus, resetStatuses, allVisible } =
16-
useFilter();
15+
const {
16+
visibleStatuses,
17+
toggleStatus,
18+
resetStatuses,
19+
allVisible,
20+
showHiddenSteps,
21+
setShowHiddenSteps,
22+
} = useFilter();
1723

1824
const statuses = [
1925
{
@@ -104,6 +110,57 @@ export default function Filter({ disabled }: FilterProps) {
104110
{item.text}
105111
</button>
106112
))}
113+
114+
<div className="jenkins-dropdown__separator" />
115+
116+
<button
117+
className={classNames(
118+
"jenkins-dropdown__item",
119+
"pgv-filter-button",
120+
{
121+
"pgv-filter-button--unchecked": !showHiddenSteps,
122+
},
123+
)}
124+
onClick={() => setShowHiddenSteps(!showHiddenSteps)}
125+
>
126+
<div className="jenkins-dropdown__item__icon">
127+
<svg
128+
xmlns="http://www.w3.org/2000/svg"
129+
viewBox="0 0 512 512"
130+
style={{ width: "1.375rem", height: "1.375rem" }}
131+
>
132+
<path
133+
fill="none"
134+
stroke="currentColor"
135+
strokeLinecap="round"
136+
strokeLinejoin="round"
137+
strokeWidth="32"
138+
d="M255.66 112c-77.94 0-157.89 45.11-220.83 135.33a16 16 0 00-.27 17.77C82.92 340.8 161.8 400 255.66 400c92.84 0 173.34-59.38 221.79-135.25a16.14 16.14 0 000-17.47C428.89 172.28 347.8 112 255.66 112z"
139+
/>
140+
<circle
141+
cx="256"
142+
cy="256"
143+
r="80"
144+
fill="none"
145+
stroke="currentColor"
146+
strokeMiterlimit="10"
147+
strokeWidth="32"
148+
/>
149+
{!showHiddenSteps && (
150+
<line
151+
x1="64"
152+
y1="64"
153+
x2="448"
154+
y2="448"
155+
stroke="currentColor"
156+
strokeLinecap="round"
157+
strokeWidth="32"
158+
/>
159+
)}
160+
</svg>
161+
</div>
162+
Show hidden steps
163+
</button>
107164
</div>
108165
}
109166
>

src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export default function ConsoleLogCard({
6161
}
6262

6363
return (
64-
<div className={"pgv-step-detail-group"} key={`step-card-${step.id}`}>
64+
<div className="pgv-step-detail-group" key={`step-card-${step.id}`}>
6565
<div
6666
className={classNames("pgv-step-detail-header", "jenkins-button", {
6767
"jenkins-button--tertiary": !isExpanded,

src/main/frontend/pipeline-console-view/pipeline-console/main/StageView.spec.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { TextEncoder } from "util";
55
import { vi } from "vitest";
66

77
import { Result, StageInfo, StepInfo } from "./PipelineConsoleModel.tsx";
8+
import { FilterProvider } from "./providers/filter-provider.tsx";
89
import StageView from "./StageView.tsx";
910

1011
(globalThis as any).TextEncoder = TextEncoder;
@@ -44,18 +45,20 @@ describe("StageView", () => {
4445
it("renders StageDetails and StageSteps with provided props", async () => {
4546
await act(async () => {
4647
render(
47-
<StageView
48-
tailLogs={false}
49-
scrollToTail={() => {}}
50-
stopTailingLogs={() => {}}
51-
stage={mockStage}
52-
steps={mockSteps}
53-
stepBuffers={new Map()}
54-
expandedSteps={["step-1"]}
55-
onStepToggle={vi.fn()}
56-
fetchLogText={async () => mockBuffer}
57-
fetchExceptionText={async () => mockBuffer}
58-
/>,
48+
<FilterProvider>
49+
<StageView
50+
tailLogs={false}
51+
scrollToTail={() => {}}
52+
stopTailingLogs={() => {}}
53+
stage={mockStage}
54+
steps={mockSteps}
55+
stepBuffers={new Map()}
56+
expandedSteps={["step-1"]}
57+
onStepToggle={vi.fn()}
58+
fetchLogText={async () => mockBuffer}
59+
fetchExceptionText={async () => mockBuffer}
60+
/>
61+
</FilterProvider>,
5962
);
6063
});
6164

src/main/frontend/pipeline-console-view/pipeline-console/main/providers/filter-provider.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ interface FilterContextType {
99
allVisible: boolean;
1010
search: string;
1111
setSearch: (value: string) => void;
12+
showHiddenSteps: boolean;
13+
setShowHiddenSteps: (show: boolean) => void;
1214
}
1315

1416
const FilterContext = createContext<FilterContextType | undefined>(undefined);
@@ -26,6 +28,7 @@ const defaultStatuses: Result[] = [
2628
export const FilterProvider = ({ children }: { children: ReactNode }) => {
2729
const [visibleStatuses, setVisibleStatuses] = useState<Result[]>([]);
2830
const [search, setSearch] = useState("");
31+
const [showHiddenSteps, setShowHiddenSteps] = useState(false);
2932

3033
const toggleStatus = (key: Result) => {
3134
if (visibleStatuses.includes(key as Result)) {
@@ -51,6 +54,8 @@ export const FilterProvider = ({ children }: { children: ReactNode }) => {
5154
.length === defaultStatuses.length,
5255
search,
5356
setSearch,
57+
showHiddenSteps,
58+
setShowHiddenSteps,
5459
}}
5560
>
5661
{children}

src/main/frontend/pipeline-console-view/pipeline-console/main/stage-details.spec.tsx

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Result,
88
StageInfo,
99
} from "../../../pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx";
10+
import { FilterProvider } from "./providers/filter-provider.tsx";
1011
import StageDetails from "./stage-details.tsx";
1112

1213
(globalThis as any).TextEncoder = TextEncoder;
@@ -19,7 +20,11 @@ describe("StageDetails", () => {
1920
});
2021

2122
it("renders stage name and status color class", () => {
22-
render(<StageDetails stage={mockStage} />);
23+
render(
24+
<FilterProvider>
25+
<StageDetails stage={mockStage} />
26+
</FilterProvider>,
27+
);
2328

2429
const heading = screen.getByRole("heading", { level: 2 });
2530
expect(heading).toHaveTextContent("Build");
@@ -30,7 +35,11 @@ describe("StageDetails", () => {
3035
});
3136

3237
it("shows running bar if stage is running", () => {
33-
render(<StageDetails stage={{ ...mockStage, state: Result.running }} />);
38+
render(
39+
<FilterProvider>
40+
<StageDetails stage={{ ...mockStage, state: Result.running }} />
41+
</FilterProvider>,
42+
);
3443

3544
const runningIndicator = document.querySelector(
3645
".pgv-stage-details__running",
@@ -39,19 +48,32 @@ describe("StageDetails", () => {
3948
});
4049

4150
it("does not show pause time if pauseDurationMillis is 0", () => {
42-
render(<StageDetails stage={{ ...mockStage, pauseDurationMillis: 0 }} />);
51+
render(
52+
<FilterProvider>
53+
<StageDetails stage={{ ...mockStage, pauseDurationMillis: 0 }} />
54+
</FilterProvider>,
55+
);
4356

4457
expect(screen.queryByText("Queued")).not.toBeInTheDocument();
4558
});
4659

4760
it("disables dropdown if stage is synthetic", () => {
48-
render(<StageDetails stage={{ ...mockStage, synthetic: true }} />);
61+
render(
62+
<FilterProvider>
63+
<StageDetails stage={{ ...mockStage, synthetic: true }} />
64+
</FilterProvider>,
65+
);
4966

50-
expect(screen.queryByRole("button")).toBeDisabled();
67+
const dropdownButton = screen.getByRole("button", { name: "More actions" });
68+
expect(dropdownButton).toBeDisabled();
5169
});
5270

5371
it("displays total duration", () => {
54-
render(<StageDetails stage={{ ...mockStage }} />);
72+
render(
73+
<FilterProvider>
74+
<StageDetails stage={{ ...mockStage }} />
75+
</FilterProvider>,
76+
);
5577

5678
expect(
5779
screen.queryByLabelText("Total duration")?.nextSibling,
@@ -60,9 +82,11 @@ describe("StageDetails", () => {
6082

6183
it("displays start from stage", () => {
6284
render(
63-
<StageDetails
64-
stage={{ ...mockStage, startTimeMillis: Date.now() - 60_000 }}
65-
/>,
85+
<FilterProvider>
86+
<StageDetails
87+
stage={{ ...mockStage, startTimeMillis: Date.now() - 60_000 }}
88+
/>
89+
</FilterProvider>,
6690
);
6791

6892
expect(screen.queryByText("Started 1m ago")).toBeInTheDocument();

src/main/frontend/pipeline-console-view/pipeline-console/main/stage-details.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import "./stage-details.scss";
22

33
import Dropdown from "../../../common/components/dropdown.tsx";
4+
import Filter from "../../../common/components/filter.tsx";
45
import {
56
resultToColor,
67
StageStatusIcon,
@@ -111,6 +112,9 @@ export default function StageDetails({ stage }: StageDetailsProps) {
111112
</li>
112113
)}
113114
<StageNodeLink agent={stage.agent} />
115+
<li>
116+
<Filter />
117+
</li>
114118
<li>
115119
<Dropdown
116120
className={"jenkins-button--tertiary"}

src/main/frontend/pipeline-console-view/pipeline-console/main/stage-steps.tsx

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import "./stage-steps.scss";
33
import { StepInfo, StepLogBufferInfo } from "../../../common/RestClient.tsx";
44
import ConsoleLogCard from "./ConsoleLogCard.tsx";
55
import { StageInfo } from "./PipelineConsoleModel.tsx";
6+
import { useFilter } from "./providers/filter-provider.tsx";
67

78
export default function StageSteps({
89
tailLogs,
@@ -16,6 +17,8 @@ export default function StageSteps({
1617
fetchLogText,
1718
fetchExceptionText,
1819
}: StageStepsProps) {
20+
const { showHiddenSteps } = useFilter();
21+
1922
if (steps.length === 0) {
2023
return null;
2124
}
@@ -25,22 +28,24 @@ export default function StageSteps({
2528
className={"pgv-stage-steps"}
2629
key={`stage-steps-container-${stage ? stage.id : "unk"}`}
2730
>
28-
{steps.map((stepItemData) => {
29-
return (
30-
<ConsoleLogCard
31-
tailLogs={tailLogs}
32-
scrollToTail={scrollToTail}
33-
stopTailingLogs={stopTailingLogs}
34-
step={stepItemData}
35-
stepBuffers={stepBuffers}
36-
onStepToggle={onStepToggle}
37-
isExpanded={expandedSteps.includes(stepItemData.id)}
38-
fetchLogText={fetchLogText}
39-
fetchExceptionText={fetchExceptionText}
40-
key={`step-console-card-${stepItemData.id}`}
41-
/>
42-
);
43-
})}
31+
{steps
32+
.filter((step) => showHiddenSteps || !step.flags?.hidden)
33+
.map((stepItemData) => {
34+
return (
35+
<ConsoleLogCard
36+
tailLogs={tailLogs}
37+
scrollToTail={scrollToTail}
38+
stopTailingLogs={stopTailingLogs}
39+
step={stepItemData}
40+
stepBuffers={stepBuffers}
41+
onStepToggle={onStepToggle}
42+
isExpanded={expandedSteps.includes(stepItemData.id)}
43+
fetchLogText={fetchLogText}
44+
fetchExceptionText={fetchExceptionText}
45+
key={`step-console-card-${stepItemData.id}`}
46+
/>
47+
);
48+
})}
4449
</div>
4550
);
4651
}

0 commit comments

Comments
 (0)