Skip to content

Commit 0f35a5c

Browse files
authored
ui: Reimplement Timeline Chart and Adjust Server Timezone Handling (#704)
1 parent 7582f5f commit 0f35a5c

File tree

4 files changed

+1515
-1506
lines changed

4 files changed

+1515
-1506
lines changed

ui/package.json

+7-4
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,16 @@
6161
"@fortawesome/fontawesome-svg-core": "^6.1.2",
6262
"@fortawesome/free-solid-svg-icons": "^6.1.2",
6363
"@fortawesome/react-fontawesome": "^0.2.0",
64-
"@mui/icons-material": "^5.8.0",
65-
"@mui/material": "^5.8.1",
64+
"@mui/icons-material": "^6.1.0",
65+
"@mui/material": "^6.1.0",
6666
"@tanstack/react-table": "^8.5.11",
6767
"@types/lodash": "^4.17.7",
6868
"cron-parser": "^4.5.0",
6969
"fontsource-roboto": "^4.0.0",
7070
"mermaid": "^9.1.1",
7171
"moment": "^2.29.3",
7272
"moment-duration-format": "^2.3.2",
73+
"moment-timezone": "^0.5.46",
7374
"monaco-editor": "^0.41.0",
7475
"monaco-loader": "^1.0.0",
7576
"monaco-react": "^1.1.0",
@@ -80,7 +81,9 @@
8081
"react-dom": "^18.1.0",
8182
"react-monaco-editor": "^0.54.0",
8283
"react-router-dom": "^6.3.0",
83-
"recharts": "^2.1.10",
84-
"swr": "^1.3.0"
84+
"recharts": "^2.13.0",
85+
"swr": "^1.3.0",
86+
"vis-data": "^7.1.9",
87+
"vis-timeline": "^7.7.3"
8588
}
8689
}
Original file line numberDiff line numberDiff line change
@@ -1,127 +1,173 @@
1+
import React, { useEffect, useRef } from 'react';
12
import { Box } from '@mui/material';
2-
import moment from 'moment';
3-
import React from 'react';
4-
import {
5-
Bar,
6-
BarChart,
7-
Cell,
8-
LabelList,
9-
LabelProps,
10-
ResponsiveContainer,
11-
XAxis,
12-
YAxis,
13-
} from 'recharts';
3+
import moment, { MomentInput } from 'moment-timezone';
4+
import { Timeline, DataSet } from 'vis-timeline/standalone';
5+
import 'vis-timeline/styles/vis-timeline-graph2d.css';
146
import { statusColorMapping } from '../../consts';
157
import { DAGStatus } from '../../models';
16-
import { SchedulerStatus } from '../../models';
178
import { WorkflowListItem } from '../../models/api';
189

1910
type Props = { data: DAGStatus[] | WorkflowListItem[] };
2011

21-
type DataFrame = {
22-
name: string;
23-
status: SchedulerStatus;
24-
values: [number, number];
12+
type TimelineItem = {
13+
id: string;
14+
content: string;
15+
start: Date;
16+
end: Date;
17+
group: string;
18+
className: string;
2519
};
2620

2721
function DashboardTimechart({ data: input }: Props) {
28-
const [data, setData] = React.useState<DataFrame[]>([]);
29-
React.useEffect(() => {
30-
const ret: DataFrame[] = [];
22+
const timelineRef = useRef<HTMLDivElement>(null);
23+
const timelineInstance = useRef<Timeline | null>(null);
24+
25+
useEffect(() => {
26+
if (!timelineRef.current) return;
27+
28+
let timezone = getConfig().tz;
29+
if (!timezone) {
30+
timezone = moment.tz.guess();
31+
}
32+
33+
const items: TimelineItem[] = [];
3134
const now = moment();
32-
const startOfDayUnix = moment().startOf('day').unix();
35+
const startOfDay = moment().startOf('day');
36+
3337
input.forEach((wf) => {
3438
const status = wf.Status;
3539
const start = status?.StartedAt;
36-
if (start && start != '-') {
37-
const startUnix = Math.max(moment(start).unix(), startOfDayUnix);
38-
const end = status.FinishedAt;
39-
let to = now.unix();
40-
if (end && end != '-') {
41-
to = moment(end).unix();
42-
}
43-
ret.push({
44-
name: status.Name,
45-
status: status.Status,
46-
values: [startUnix, to],
40+
if (start && start !== '-') {
41+
const startMoment = moment(start);
42+
const end =
43+
status.FinishedAt && status.FinishedAt !== '-'
44+
? moment(status.FinishedAt)
45+
: now;
46+
47+
items.push({
48+
id: status.Name + `_${status.RequestId}`,
49+
content: status.Name,
50+
start: startMoment.tz(timezone).toDate(),
51+
end: end.tz(timezone).toDate(),
52+
group: 'main',
53+
className: `status-${status.Status}`,
4754
});
4855
}
4956
});
50-
const sorted = ret.sort((a, b) => {
51-
return a.values[0] < b.values[0] ? -1 : 1;
52-
});
53-
setData(sorted);
57+
58+
const dataset = new DataSet(items);
59+
60+
if (!timelineInstance.current) {
61+
timelineInstance.current = new Timeline(timelineRef.current, dataset, {
62+
moment: (date: MomentInput) => moment(date).tz(timezone),
63+
start: startOfDay.toDate(),
64+
end: now.endOf('day').toDate(),
65+
orientation: 'top',
66+
stack: true,
67+
showMajorLabels: true,
68+
showMinorLabels: true,
69+
showTooltips: true,
70+
zoomable: false,
71+
verticalScroll: true,
72+
timeAxis: { scale: 'hour', step: 1 },
73+
format: {
74+
minorLabels: {
75+
minute: 'HH:mm',
76+
hour: 'HH:mm',
77+
},
78+
majorLabels: {
79+
hour: 'HH:mm',
80+
day: 'ddd D MMMM',
81+
},
82+
},
83+
height: '100%',
84+
maxHeight: '100%',
85+
margin: { item: { vertical: 10 } },
86+
});
87+
} else {
88+
timelineInstance.current.setItems(dataset);
89+
}
90+
91+
console.log(
92+
{input, items}
93+
)
94+
95+
return () => {
96+
if (timelineInstance.current) {
97+
timelineInstance.current.destroy();
98+
timelineInstance.current = null;
99+
}
100+
};
54101
}, [input]);
55-
const now = moment();
56-
const shouldScroll = data.length >= 40;
102+
57103
return (
58-
<TimelineWrapper shouldScroll={shouldScroll}>
59-
<ResponsiveContainer
60-
width="100%"
61-
minHeight={shouldScroll ? data.length * 12 : undefined}
62-
height={shouldScroll ? undefined : '90%'}
63-
>
64-
<BarChart data={data} layout="vertical">
65-
<XAxis
66-
name="Time"
67-
tickFormatter={(unixTime) => moment.unix(unixTime).format('HH:mm')}
68-
type="number"
69-
dataKey="values"
70-
tickCount={96}
71-
domain={[now.startOf('day').unix(), now.endOf('day').unix()]}
72-
/>
73-
<YAxis dataKey="name" type="category" hide />
74-
<Bar background dataKey="values" fill="lightblue" minPointSize={2}>
75-
{data.map((_, index) => {
76-
const color = statusColorMapping[data[index].status];
77-
return <Cell key={index} fill={color.backgroundColor} />;
78-
})}
79-
<LabelList
80-
dataKey="name"
81-
position="insideLeft"
82-
content={({ x, y, width, height, value }: LabelProps) => {
83-
return (
84-
<text
85-
x={Number(x) + Number(width) + 2}
86-
y={Number(y) + (Number(height) || 0) / 2}
87-
fill="#000"
88-
fontSize={12}
89-
textAnchor="start"
90-
>
91-
{value}
92-
</text>
93-
);
94-
}}
95-
/>
96-
</Bar>
97-
</BarChart>
98-
</ResponsiveContainer>
104+
<TimelineWrapper>
105+
<div
106+
ref={timelineRef}
107+
style={{ width: '100%', height: '100%' }}
108+
/>
109+
<style>
110+
{`
111+
.vis-item .vis-item-overflow {
112+
overflow: visible;
113+
color: black;
114+
}
115+
.vis-panel.vis-top {
116+
position: sticky;
117+
top: 0;
118+
z-index: 1;
119+
background-color: white;
120+
}
121+
.vis-labelset {
122+
position: sticky;
123+
left: 0;
124+
z-index: 2;
125+
background-color: white;
126+
}
127+
.vis-item .vis-item-content {
128+
position: absolute;
129+
left: 100% !important;
130+
padding-left: 5px;
131+
transform: translateY(-50%);
132+
top: 50%;
133+
white-space: nowrap;
134+
}
135+
.vis-item {
136+
overflow: visible !important;
137+
}
138+
`}
139+
</style>
140+
<style>{`
141+
${Object.entries(statusColorMapping)
142+
.map(
143+
([status, color]) => `
144+
.status-${status.toLowerCase()} {
145+
background-color: ${color.backgroundColor};
146+
color: ${color.color};
147+
border-color: ${color.backgroundColor};
148+
}
149+
`
150+
)
151+
.join('\n')}
152+
`}</style>
99153
</TimelineWrapper>
100154
);
101155
}
102156

103-
function TimelineWrapper({
104-
children,
105-
shouldScroll,
106-
}: {
107-
children: React.ReactNode;
108-
shouldScroll: boolean;
109-
}) {
110-
if (shouldScroll) {
111-
return (
112-
<Box
113-
sx={{
114-
width: '100%',
115-
maxWidth: '100%',
116-
height: '90%',
117-
overflow: 'auto',
118-
}}
119-
>
120-
{children}
121-
</Box>
122-
);
123-
}
124-
return <React.Fragment>{children}</React.Fragment>;
157+
function TimelineWrapper({ children }: { children: React.ReactNode }) {
158+
return (
159+
<Box
160+
sx={{
161+
width: '95%',
162+
maxWidth: '95%',
163+
height: '60vh',
164+
overflow: 'auto',
165+
backgroundColor: 'lightgray',
166+
}}
167+
>
168+
{children}
169+
</Box>
170+
);
125171
}
126172

127-
export default DashboardTimechart;
173+
export default DashboardTimechart;

ui/src/pages/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ function Dashboard() {
7979
height: '100%',
8080
}}
8181
>
82-
<Title>Timeline</Title>
82+
<Title>{getConfig().tz ? `Timeline in ${getConfig().tz}` : "Timeline"}</Title>
8383
<DashboardTimechart data={data?.DAGs || []} />
8484
</Box>
8585
</Grid>

0 commit comments

Comments
 (0)