Skip to content

Commit 9e6445d

Browse files
authored
[Utilization] Add new time range picker and bunch of reusable UI (#6678)
# Overview - add time range selection for utilization aggregation - add checkbox option to filter metrics - add symblink - auto update url params when the ContextProvider is updated ## Notes Potentially the UM uis created here would be reused when we setup modularized component for upcoming benchmark ux views ## Details ### Table: Fetch Data The table is independent to fetch most params from the parent, ideally each of those component has its own data management, only access necessary central control information. this makes the component modularized, with ofc a cost of multiple query, which is expected and clickhouse expense is ok now. ### A clean time range picker <img width="616" alt="image" src="https://github.com/user-attachments/assets/85296377-85ea-46e0-8d99-30daff004316" /> ### A clean checkbox option for GeneralizedMeticsTable <img width="644" alt="image" src="https://github.com/user-attachments/assets/0cc70d8b-fd15-4a4a-bf42-0cf12ff8f722" /> ## Demo https://torchci-git-utilvisualization-fbopensource.vercel.app/utilization/report --------- Signed-off-by: Yang Wang <[email protected]>
1 parent 357223a commit 9e6445d

File tree

15 files changed

+733
-160
lines changed

15 files changed

+733
-160
lines changed

torchci/clickhouse_queries/oss_ci_util/oss_ci_list_utilization_reports/query.sql

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,14 @@ SELECT
33
{granularity:String} = 'day', toDate(time),
44
{granularity:String} = 'week', toStartOfWeek(time),
55
{granularity:String} = 'month', toStartOfMonth(time),
6+
{granularity:String} = 'all', NULL,
67
toDate(time)
78
) AS time_group,
89

910
parent_group,
1011
countDistinctMerge(run_counts) AS total_runs,
1112
group_key,
1213
{group_by:String} AS group_field,
13-
14-
avgMerge(cpu_avg_state) AS cpu_avg,
15-
avgMerge(memory_avg_state) AS memory_avg,
16-
avgMerge(gpu_avg_state) AS gpu_avg,
17-
avgMerge(gpu_mem_state) AS gpu_mem_avg,
18-
1914
map(
2015
'cpu_avg', avgMerge(cpu_avg_state),
2116
'memory_avg', avgMerge(memory_avg_state),
@@ -51,10 +46,12 @@ SELECT
5146
quantilesTDigestMerge(0.1, 0.5, 0.9, 0.95, 0.98) (gpu_mem_p_state)[4],
5247
'gpu_mem_p98',
5348
quantilesTDigestMerge(0.1, 0.5, 0.9, 0.95, 0.98) (gpu_mem_p_state)[5]
54-
) AS metrics
49+
) AS metrics,
5550

56-
FROM fortesting.oss_ci_utilization_summary_report_v1
51+
min(time) AS earliest_ts,
52+
max(time) AS latest_ts
5753

54+
FROM fortesting.oss_ci_utilization_summary_report_v1
5855
WHERE
5956
time >= toDate({start_time:String})
6057
AND time <= toDate({end_time:String})
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import {
2+
Box,
3+
Checkbox,
4+
FormControlLabel,
5+
Popover,
6+
SxProps,
7+
Typography,
8+
} from "@mui/material";
9+
import { useEffect, useState } from "react";
10+
import { UMDenseButton } from "./UMDenseComponents";
11+
12+
type DenseCheckboxPopoverProps = {
13+
options: string[];
14+
onChange: (selected: string[]) => void;
15+
sx?: SxProps;
16+
buttonLabel?: string;
17+
};
18+
19+
export function UMCheckboxPopover({
20+
options,
21+
onChange,
22+
sx,
23+
buttonLabel = "Select Items",
24+
}: DenseCheckboxPopoverProps) {
25+
const [selected, setSelected] = useState<string[]>([...options]);
26+
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
27+
const open = Boolean(anchorEl);
28+
29+
const updateSelected = (newSelected: string[]) => {
30+
setSelected(newSelected);
31+
const unselected = options.filter((opt) => !newSelected.includes(opt));
32+
onChange(unselected);
33+
};
34+
35+
useEffect(() => {
36+
setSelected([...options]);
37+
}, [options]);
38+
39+
const handleToggle = (option: string) => {
40+
const updated = selected.includes(option)
41+
? selected.filter((v) => v !== option)
42+
: [...selected, option];
43+
updateSelected(updated);
44+
};
45+
46+
const handleSelectAll = () => {
47+
setSelected([...options]);
48+
onChange([]);
49+
};
50+
51+
const handleClearAll = () => {
52+
setSelected([]);
53+
onChange([...options]);
54+
};
55+
56+
return (
57+
<>
58+
<UMDenseButton
59+
variant="outlined"
60+
size="small"
61+
onClick={(e) => setAnchorEl(e.currentTarget)}
62+
sx={{ fontSize: "0.75rem", px: 0.5 }}
63+
>
64+
{buttonLabel}
65+
</UMDenseButton>
66+
<Popover
67+
open={open}
68+
anchorEl={anchorEl}
69+
onClose={() => setAnchorEl(null)}
70+
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
71+
>
72+
<Typography fontWeight={500} fontSize="0.75rem" mb={0.5}>
73+
Options
74+
</Typography>
75+
76+
<Box display="flex" flexDirection="column" gap={0.1}>
77+
<FormControlLabel
78+
control={
79+
<Checkbox
80+
size="small"
81+
sx={{ p: 0.25 }}
82+
checked={selected.length === options.length}
83+
indeterminate={
84+
selected.length > 0 && selected.length < options.length
85+
}
86+
onChange={(e) =>
87+
e.target.checked ? handleSelectAll() : handleClearAll()
88+
}
89+
/>
90+
}
91+
label="Select All"
92+
sx={{ ".MuiFormControlLabel-label": { fontSize: "0.75rem" }, m: 0 }}
93+
/>
94+
95+
{options.map((option) => (
96+
<FormControlLabel
97+
key={option}
98+
control={
99+
<Checkbox
100+
size="small"
101+
sx={{ p: 0.25, padding: "0 10px" }}
102+
checked={selected.includes(option)}
103+
onChange={() => handleToggle(option)}
104+
/>
105+
}
106+
label={option}
107+
sx={{
108+
".MuiFormControlLabel-label": { fontSize: "0.75rem" },
109+
m: 0,
110+
}}
111+
/>
112+
))}
113+
</Box>
114+
</Popover>
115+
</>
116+
);
117+
}
118+
function setSelected(arg0: string[]) {
119+
throw new Error("Function not implemented.");
120+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import CopyLink from "components/CopyLink";
2+
import { objectToQueryString } from "components/utilization/UtilizationReportPage/hepler";
3+
import { useRouter } from "next/router";
4+
import { useEffect, useState } from "react";
5+
6+
export const UMCopyLink = ({ params }: { params: any }) => {
7+
const router = useRouter();
8+
const [cleanUrl, setCleanUrl] = useState("");
9+
10+
const paramsString = `${objectToQueryString(params)}`;
11+
useEffect(() => {
12+
if (typeof window !== "undefined") {
13+
const url = new URL(window.location.href);
14+
setCleanUrl(`${url.origin}${url.pathname}`);
15+
}
16+
}, [router.asPath]);
17+
return <CopyLink textToCopy={`${cleanUrl}?${paramsString}`} />;
18+
};
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { Box, Popover, Stack } from "@mui/material";
2+
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
3+
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
4+
import dayjs, { Dayjs } from "dayjs";
5+
import * as React from "react";
6+
import { UMDenseButton } from "./UMDenseComponents";
7+
import { UMDenseDatePicker } from "./UMDenseDatePicker";
8+
9+
const presets = [
10+
{ key: "today", label: "Today", days: 1 },
11+
{ key: "last2", label: "Last 2 Days", days: 2 },
12+
{ key: "last7", label: "Last 7 Days", days: 7 },
13+
{ key: "last14", label: "Last 14 Days", days: 14 },
14+
{ key: "last30", label: "Last 30 Days", days: 30 },
15+
];
16+
17+
interface PresetDateRangeSelectorProps {
18+
setTimeRange?: (startDate: Dayjs, endDate: Dayjs) => void;
19+
start?: string;
20+
end?: string;
21+
}
22+
23+
export function UMDateRangePicker({
24+
start = dayjs().utc().startOf("day").subtract(6, "day").format("YYYY-MM-DD"),
25+
end = dayjs().utc().startOf("day").format("YYYY-MM-DD"),
26+
setTimeRange = () => {},
27+
}: PresetDateRangeSelectorProps) {
28+
const [startDate, setStartDate] = React.useState<Dayjs>(dayjs(start).utc());
29+
const [endDate, setEndDate] = React.useState<Dayjs>(dayjs(end).utc());
30+
const [activePreset, setActivePreset] = React.useState<string | null>("");
31+
32+
const setRange = (days: number, key: string) => {
33+
const now = dayjs().utc();
34+
const start = now.startOf("day").subtract(days - 1, "day");
35+
setStartDate(start);
36+
setEndDate(now);
37+
setActivePreset(key);
38+
setTimeRange(start, now);
39+
};
40+
41+
const handleManualStart = (newValue: any) => {
42+
if (newValue) {
43+
setStartDate(newValue);
44+
setActivePreset(null);
45+
setTimeRange(newValue, dayjs().utc());
46+
}
47+
};
48+
49+
const handleManualEnd = (newValue: any) => {
50+
if (newValue) {
51+
setEndDate(newValue);
52+
setActivePreset(null);
53+
setTimeRange(startDate, newValue);
54+
}
55+
};
56+
57+
return (
58+
<LocalizationProvider dateAdapter={AdapterDayjs}>
59+
<Stack spacing={2} sx={{ margin: "10px 0px" }}>
60+
{/* Preset Buttons */}
61+
<Stack direction="row" spacing={1}>
62+
{presets.map(({ key, label, days }) => (
63+
<UMDenseButton
64+
key={key}
65+
variant={activePreset === key ? "contained" : "outlined"}
66+
onClick={() => setRange(days, key)}
67+
>
68+
{label}
69+
</UMDenseButton>
70+
))}
71+
</Stack>
72+
{/* Manual Pickers */}
73+
<Box sx={{ display: "flex", gap: 2 }}>
74+
<UMDenseDatePicker
75+
label="Start Date"
76+
value={startDate}
77+
onChange={handleManualStart}
78+
/>
79+
<UMDenseDatePicker
80+
label="End Date"
81+
value={endDate}
82+
onChange={handleManualEnd}
83+
/>
84+
</Box>
85+
</Stack>
86+
</LocalizationProvider>
87+
);
88+
}
89+
90+
export function UMDateButtonPicker({
91+
start = dayjs().utc().startOf("day").subtract(6, "day").format("YYYY-MM-DD"),
92+
end = dayjs().utc().startOf("day").format("YYYY-MM-DD"),
93+
setTimeRange = () => {},
94+
}: PresetDateRangeSelectorProps) {
95+
const [open, setOpen] = React.useState(false);
96+
const anchorRef = React.useRef(null);
97+
98+
return (
99+
<div>
100+
<Box
101+
sx={{
102+
display: "flex",
103+
flexDirection: "row",
104+
alignItems: "center",
105+
}}
106+
>
107+
<Box sx={{ margin: "0 2px 0 0", fontSize: "0.8rem" }}>Time Range:</Box>
108+
<UMDenseButton
109+
ref={anchorRef}
110+
variant="outlined"
111+
onClick={() => setOpen(true)}
112+
sx={{
113+
margin: "5px 0px",
114+
borderRadius: 0,
115+
textTransform: "none",
116+
minWidth: 160,
117+
justifyContent: "space-between",
118+
}}
119+
>
120+
{start} - {end}
121+
</UMDenseButton>
122+
</Box>
123+
<Popover
124+
open={open}
125+
anchorEl={anchorRef.current}
126+
onClose={() => setOpen(false)}
127+
anchorOrigin={{
128+
vertical: "bottom",
129+
horizontal: "left",
130+
}}
131+
sx={{
132+
padding: "10px 0px",
133+
minWidth: 260,
134+
}}
135+
>
136+
<Box p={2}>
137+
<UMDateRangePicker
138+
start={start}
139+
end={end}
140+
setTimeRange={setTimeRange}
141+
/>
142+
</Box>
143+
</Popover>
144+
</div>
145+
);
146+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Button } from "@mui/material";
2+
import styled from "@mui/system/styled";
3+
4+
export const UMDenseButton = styled(Button)(({ theme }) => ({
5+
padding: "2px 2px",
6+
minHeight: "20px",
7+
fontSize: "0.75rem",
8+
color: "grey",
9+
minWidth: "auto",
10+
borderRadius: 0,
11+
textTransform: "none", // optional: avoids uppercase
12+
}));
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { styled } from "@mui/system";
2+
import { DatePicker } from "@mui/x-date-pickers";
3+
import { Dayjs } from "dayjs";
4+
5+
const DenseDatePicker = styled(DatePicker)(({ theme }) => ({
6+
"& .MuiInputBase-root": {
7+
minWidth: 180,
8+
borderRadius: 0,
9+
fontSize: "0.875rem",
10+
},
11+
"& .MuiOutlinedInput-root": {
12+
borderRadius: 0,
13+
},
14+
"& .MuiIconButton-root": {
15+
padding: 4,
16+
minWidth: 32,
17+
},
18+
}));
19+
20+
type UMDenseDatePickerProps = {
21+
label: string;
22+
value: Dayjs | null;
23+
onChange: (newDate: Dayjs | null) => void;
24+
};
25+
26+
export function UMDenseDatePicker({
27+
label,
28+
value,
29+
onChange,
30+
}: UMDenseDatePickerProps) {
31+
return (
32+
<DenseDatePicker
33+
label={label}
34+
value={value}
35+
onChange={onChange}
36+
slotProps={{
37+
textField: {
38+
size: "small",
39+
fullWidth: false,
40+
},
41+
}}
42+
/>
43+
);
44+
}

0 commit comments

Comments
 (0)