Skip to content

Commit c540550

Browse files
timjalewisbirks
andauthored
Add timings to stage view (#814)
Co-authored-by: Lewis Birks <[email protected]> Co-authored-by: lewisbirks <[email protected]>
1 parent a862fa8 commit c540550

File tree

30 files changed

+581
-107
lines changed

30 files changed

+581
-107
lines changed

.vscode/tasks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{
55
"label": "Run Jenkins",
66
"type": "shell",
7-
"command": "mvn hpi:run -Dskip.npm -P quick-build",
7+
"command": "mvn hpi:run -Djava.awt.headless=true -Dskip.npm -P quick-build",
88
"options": {
99
"env": {
1010
// Do not wait for debugger to connect (suspend=n), unlike mvnDebug
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useId } from "react";
2+
3+
export default function Checkbox({
4+
label,
5+
value,
6+
setValue,
7+
}: {
8+
label: string;
9+
value: boolean;
10+
setValue: (e: boolean) => void;
11+
}) {
12+
const id = useId();
13+
return (
14+
<div className="jenkins-checkbox">
15+
<input
16+
type="checkbox"
17+
id={id}
18+
name={id}
19+
checked={value}
20+
onChange={(e) => setValue(e.target.checked)}
21+
/>
22+
<label htmlFor={id}>{label}</label>
23+
</div>
24+
);
25+
}

src/main/frontend/common/components/dropdown-portal.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { createPortal } from "react-dom";
33

44
interface DropdownPortalProps {
55
children: ReactNode;
6+
container: HTMLElement | null;
67
}
78

8-
export default function DropdownPortal({ children }: DropdownPortalProps) {
9-
const container = document.getElementById("console-pipeline-overflow-root");
10-
9+
export default function DropdownPortal({
10+
children,
11+
container,
12+
}: DropdownPortalProps) {
1113
if (!container) {
1214
console.error("DropdownPortal: Target container not found!");
1315
return null;

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@ import Tooltip from "./tooltip.tsx";
88
*/
99
export default function Dropdown({
1010
items,
11+
tooltip = "More actions",
1112
disabled,
1213
className,
14+
icon,
1315
}: DropdownProps) {
1416
const [visible, setVisible] = useState(false);
1517
const show = () => setVisible(true);
1618
const hide = () => setVisible(false);
1719

1820
return (
19-
<Tooltip content={"More actions"}>
21+
<Tooltip content={tooltip}>
2022
<Tippy
2123
visible={visible}
2224
onClickOutside={hide}
@@ -66,11 +68,14 @@ export default function Dropdown({
6668
disabled={disabled}
6769
onClick={visible ? hide : show}
6870
>
69-
<div className="jenkins-overflow-button__ellipsis">
70-
<span />
71-
<span />
72-
<span />
73-
</div>
71+
<span className={"jenkins-visually-hidden"}>{tooltip}</span>
72+
{icon || (
73+
<div className="jenkins-overflow-button__ellipsis">
74+
<span />
75+
<span />
76+
<span />
77+
</div>
78+
)}
7479
</button>
7580
</Tippy>
7681
</Tooltip>
@@ -90,8 +95,10 @@ export const DefaultDropdownProps: TippyProps = {
9095

9196
interface DropdownProps {
9297
items: (DropdownItem | ReactElement | "separator")[];
98+
tooltip?: string;
9399
disabled?: boolean;
94100
className?: string;
101+
icon?: ReactNode;
95102
}
96103

97104
interface DropdownItem {

src/main/frontend/pipeline-console-view/pipeline-console/main/symbols.tsx renamed to src/main/frontend/common/components/symbols.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const CONSOLE = (
4444
);
4545

4646
export const SETTINGS = (
47-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
47+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" aria-hidden>
4848
<path
4949
fill="none"
5050
stroke="currentColor"

src/main/frontend/common/i18n/i18n-provider.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export const I18NProvider: FunctionComponent<I18NProviderProps> = ({
4646
);
4747
};
4848

49-
export function useMessages() {
50-
return useContext(I18NContext);
49+
export function useMessages(): Messages {
50+
const messages = useContext(I18NContext);
51+
if (!messages) {
52+
throw new Error("useI18N must be used within an I18NProvider");
53+
}
54+
return messages;
5155
}

src/main/frontend/common/i18n/messages.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ export enum LocalizedMessageKey {
8181
start = "node.start",
8282
end = "node.end",
8383
changesSummary = "changes.summary",
84+
settings = "settings",
85+
showNames = "settings.showStageName",
86+
showDuration = "settings.showStageDuration",
8487
consoleNewTab = "console.newTab",
8588
}
8689

@@ -97,6 +100,9 @@ const DEFAULT_MESSAGES: ResourceBundle = {
97100
[LocalizedMessageKey.start]: "Start",
98101
[LocalizedMessageKey.end]: "End",
99102
[LocalizedMessageKey.changesSummary]: "{0} {0,choice,1#change|1<changes}",
103+
[LocalizedMessageKey.settings]: "Settings",
104+
[LocalizedMessageKey.showNames]: "Show stage names",
105+
[LocalizedMessageKey.showDuration]: "Show stage duration",
100106
[LocalizedMessageKey.consoleNewTab]: "View step as plain text",
101107
};
102108

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {
2+
createContext,
3+
ReactNode,
4+
useContext,
5+
useEffect,
6+
useState,
7+
} from "react";
8+
9+
interface PipelineGraphViewPreferences {
10+
showNames: boolean;
11+
setShowNames: (val: boolean) => void;
12+
showDurations: boolean;
13+
setShowDurations: (val: boolean) => void;
14+
}
15+
16+
const defaultPreferences = {
17+
showNames: false,
18+
showDurations: false,
19+
};
20+
21+
const UserPreferencesContext = createContext<
22+
PipelineGraphViewPreferences | undefined
23+
>(undefined);
24+
25+
const makeKey = (setting: string) => `pgv-graph-view.${setting}`;
26+
27+
const loadFromLocalStorage = <T,>(key: string, fallback: T): T => {
28+
if (typeof window === "undefined") {
29+
return fallback;
30+
}
31+
try {
32+
const value = window.localStorage.getItem(key);
33+
if (value !== null) {
34+
if (typeof fallback === "boolean") {
35+
return (value === "true") as typeof fallback;
36+
}
37+
return value as unknown as T;
38+
}
39+
} catch (e) {
40+
console.error(`Error loading localStorage key "${key}"`, e);
41+
}
42+
return fallback;
43+
};
44+
45+
export const UserPreferencesProvider = ({
46+
children,
47+
}: {
48+
children: ReactNode;
49+
}) => {
50+
const stageNamesKey = makeKey("stageNames");
51+
const stageDurationsKey = makeKey("stageDurations");
52+
53+
const [showNames, setShowNames] = useState<boolean>(
54+
loadFromLocalStorage(stageNamesKey, defaultPreferences.showNames),
55+
);
56+
const [showDurations, setShowDurations] = useState<boolean>(
57+
loadFromLocalStorage(stageDurationsKey, defaultPreferences.showDurations),
58+
);
59+
60+
useEffect(() => {
61+
window.localStorage.setItem(stageNamesKey, String(showNames));
62+
}, [showNames]);
63+
64+
useEffect(() => {
65+
window.localStorage.setItem(stageDurationsKey, String(showDurations));
66+
}, [showDurations]);
67+
68+
return (
69+
<UserPreferencesContext.Provider
70+
value={{
71+
showNames,
72+
setShowNames,
73+
showDurations,
74+
setShowDurations,
75+
}}
76+
>
77+
{children}
78+
</UserPreferencesContext.Provider>
79+
);
80+
};
81+
82+
export const useUserPreferences = (): PipelineGraphViewPreferences => {
83+
const context = useContext(UserPreferencesContext);
84+
if (!context) {
85+
throw new Error(
86+
"useMonitorPreferences must be used within a UserPreferencesProvider",
87+
);
88+
}
89+
return context;
90+
};

src/main/frontend/multi-pipeline-graph-view/app.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,38 @@ import {
88
LocaleProvider,
99
ResourceBundleName,
1010
} from "../common/i18n/index.ts";
11+
import { UserPermissionsProvider } from "../common/user/user-permission-provider.tsx";
12+
import { UserPreferencesProvider } from "../common/user/user-preferences-provider.tsx";
1113
import { MultiPipelineGraph } from "./multi-pipeline-graph/main/MultiPipelineGraph.tsx";
14+
import OverflowDropdown from "./multi-pipeline-graph/main/overfow-dropdown.tsx";
15+
import SettingsButton from "./multi-pipeline-graph/main/settings-button.tsx";
1216

1317
const App: FunctionComponent = () => {
1418
const locale = document.getElementById("multiple-pipeline-root")!.dataset
1519
.userLocale!;
20+
const settings = document.getElementById("pgv-settings");
21+
const overflow = document.getElementById("multiple-pipeline-overflow-root");
22+
if (!settings && !overflow) {
23+
throw new Error("Failed to find the 'settings/overflow' element");
24+
}
25+
if (settings && overflow) {
26+
throw new Error(
27+
"Only one of the 'settings/overflow' elements should be defined",
28+
);
29+
}
1630
return (
1731
<div>
1832
<LocaleProvider locale={locale}>
1933
<I18NProvider bundles={[ResourceBundleName.messages]}>
20-
<MultiPipelineGraph />
34+
<UserPreferencesProvider>
35+
{settings && <SettingsButton buttonPortal={settings} />}
36+
{overflow && (
37+
<UserPermissionsProvider>
38+
<OverflowDropdown buttonPortal={overflow} />
39+
</UserPermissionsProvider>
40+
)}
41+
<MultiPipelineGraph />
42+
</UserPreferencesProvider>
2143
</I18NProvider>
2244
</LocaleProvider>
2345
</div>

src/main/frontend/multi-pipeline-graph-view/multi-pipeline-graph/main/SingleRun.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
LocalizedMessageKey,
99
} from "../../../common/i18n/index.ts";
1010
import useRunPoller from "../../../common/tree-api.ts";
11+
import { useUserPreferences } from "../../../common/user/user-preferences-provider.tsx";
1112
import { time, Total } from "../../../common/utils/timings.tsx";
1213
import { PipelineGraph } from "../../../pipeline-graph-view/pipeline-graph/main/PipelineGraph.tsx";
1314
import {
@@ -21,11 +22,6 @@ export default function SingleRun({ run, currentJobPath }: SingleRunProps) {
2122
currentRunPath: currentJobPath + run.id + "/",
2223
});
2324

24-
const layout: LayoutInfo = {
25-
...defaultLayout,
26-
nodeSpacingH: 45,
27-
};
28-
2925
function Changes() {
3026
if (run.changesCount === 0) {
3127
return;
@@ -43,10 +39,28 @@ export default function SingleRun({ run, currentJobPath }: SingleRunProps) {
4339
);
4440
}
4541

42+
const { showNames, showDurations } = useUserPreferences();
43+
44+
function getLayout() {
45+
const layout: LayoutInfo = { ...defaultLayout };
46+
47+
if (!showNames && !showDurations) {
48+
layout.nodeSpacingH = 45;
49+
} else {
50+
layout.nodeSpacingH = 90;
51+
}
52+
53+
return layout;
54+
}
55+
56+
function getCompactLayout() {
57+
return !showNames && !showDurations ? "pgv-single-run--compact" : "";
58+
}
59+
4660
return (
47-
<div className="pgv-single-run">
61+
<div className={`pgv-single-run ${getCompactLayout()}`}>
4862
<div>
49-
<a href={currentJobPath + run.id} className="pgw-user-specified-text">
63+
<a href={currentJobPath + run.id} className="pgv-user-specified-text">
5064
<StatusIcon status={run.result} />
5165
{run.displayName}
5266
<span>
@@ -55,7 +69,11 @@ export default function SingleRun({ run, currentJobPath }: SingleRunProps) {
5569
</span>
5670
</a>
5771
</div>
58-
<PipelineGraph stages={runInfo?.stages || []} layout={layout} collapsed />
72+
<PipelineGraph
73+
stages={runInfo?.stages || []}
74+
layout={getLayout()}
75+
collapsed
76+
/>
5977
</div>
6078
);
6179
}

0 commit comments

Comments
 (0)