1
+ import React , { useEffect , useRef } from 'react' ;
1
2
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' ;
14
6
import { statusColorMapping } from '../../consts' ;
15
7
import { DAGStatus } from '../../models' ;
16
- import { SchedulerStatus } from '../../models' ;
17
8
import { WorkflowListItem } from '../../models/api' ;
18
9
19
10
type Props = { data : DAGStatus [ ] | WorkflowListItem [ ] } ;
20
11
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 ;
25
19
} ;
26
20
27
21
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 [ ] = [ ] ;
31
34
const now = moment ( ) ;
32
- const startOfDayUnix = moment ( ) . startOf ( 'day' ) . unix ( ) ;
35
+ const startOfDay = moment ( ) . startOf ( 'day' ) ;
36
+
33
37
input . forEach ( ( wf ) => {
34
38
const status = wf . Status ;
35
39
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 } ` ,
47
54
} ) ;
48
55
}
49
56
} ) ;
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
+ } ;
54
101
} , [ input ] ) ;
55
- const now = moment ( ) ;
56
- const shouldScroll = data . length >= 40 ;
102
+
57
103
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 >
99
153
</ TimelineWrapper >
100
154
) ;
101
155
}
102
156
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
+ ) ;
125
171
}
126
172
127
- export default DashboardTimechart ;
173
+ export default DashboardTimechart ;
0 commit comments