1
- import { Avatar , Box , SvgIcon , Typography } from "@material-ui/core"
2
- import makeStyles from "@material-ui/styles/makeStyles" ;
1
+ import { Avatar , Box , CircularProgress , SvgIcon , Typography } from "@material-ui/core"
2
+ import makeStyles from "@material-ui/styles/makeStyles"
3
3
import React , { useState } from "react"
4
- import { TerminalOutput } from "./TerminalOutput" ;
4
+ import { TerminalOutput } from "./TerminalOutput"
5
+ import StageCompleteIcon from "@material-ui/icons/Done"
6
+ import StageExpandedIcon from "@material-ui/icons/KeyboardArrowDown"
7
+ import StageErrorIcon from "@material-ui/icons/Warning"
8
+
9
+ export type BuildLogStatus = "success" | "failed" | "pending"
5
10
6
11
export interface TimelineEntry {
7
12
date : Date
8
13
title : string
9
14
description ?: string
15
+ status : BuildLogStatus
16
+ buildSummary : string
17
+ buildLogs : string [ ]
10
18
}
11
19
12
- const today = new Date ( ) ;
20
+ const today = new Date ( )
13
21
const yesterday = new Date ( )
14
22
yesterday . setHours ( - 24 )
15
23
const weekAgo = new Date ( )
@@ -36,24 +44,40 @@ Pulling image "gcr.io/coder-enterprise-nightlies/coder/envbox:1.27.0-rc.0-145-g8
36
44
Successfully pulled image "gcr.io/coder-enterprise-nightlies/coder/envbox:1.27.0-rc.0-145-g8d4ee2e9e-20220131" in 7.423772294s
37
45
` . split ( "\n" )
38
46
39
- export const mockEntries : TimelineEntry [ ] = [ {
40
- date : weekAgo ,
41
- description : "Created Workspace" ,
42
- title : "Admin" ,
43
- } , {
44
- date : yesterday ,
45
- description : "Modified Workspace" ,
46
- title : "Admin"
47
- } , {
48
- date : today ,
49
- description : "Modified Workspace" ,
50
- title : "Admin"
51
- } , {
52
- date : today ,
53
- description : "Restarted Workspace" ,
54
- title : "Admin"
55
-
56
- } ]
47
+ export const mockEntries : TimelineEntry [ ] = [
48
+ {
49
+ date : weekAgo ,
50
+ description : "Created Workspace" ,
51
+ title : "Admin" ,
52
+ status : "success" ,
53
+ buildLogs : sampleOutput ,
54
+ buildSummary : "Succeeded in 82s" ,
55
+ } ,
56
+ {
57
+ date : yesterday ,
58
+ description : "Modified Workspace" ,
59
+ title : "Admin" ,
60
+ status : "failed" ,
61
+ buildLogs : sampleOutput ,
62
+ buildSummary : "Encountered error after 49s" ,
63
+ } ,
64
+ {
65
+ date : today ,
66
+ description : "Modified Workspace" ,
67
+ title : "Admin" ,
68
+ status : "pending" ,
69
+ buildLogs : sampleOutput ,
70
+ buildSummary : "Operation in progress..." ,
71
+ } ,
72
+ {
73
+ date : today ,
74
+ description : "Restarted Workspace" ,
75
+ title : "Admin" ,
76
+ status : "success" ,
77
+ buildLogs : sampleOutput ,
78
+ buildSummary : "Succeeded in 15s" ,
79
+ } ,
80
+ ]
57
81
58
82
export interface TimelineEntryProps {
59
83
entries : TimelineEntry [ ]
@@ -69,42 +93,39 @@ const getDateWithoutTime = (date: Date) => {
69
93
}
70
94
71
95
export const groupByDate = ( entries : TimelineEntry [ ] ) : Record < string , TimelineEntry [ ] > => {
72
- const initial : Record < string , TimelineEntry [ ] > = { } ;
96
+ const initial : Record < string , TimelineEntry [ ] > = { }
73
97
return entries . reduce < Record < string , TimelineEntry [ ] > > ( ( acc , curr ) => {
74
- const dateWithoutTime = getDateWithoutTime ( curr . date ) ;
98
+ const dateWithoutTime = getDateWithoutTime ( curr . date )
75
99
const key = dateWithoutTime . getTime ( ) . toString ( )
76
- const currentEntry = acc [ key ] ;
100
+ const currentEntry = acc [ key ]
77
101
if ( currentEntry ) {
78
102
return {
79
103
...acc ,
80
- [ key ] : [ ...currentEntry , curr ]
104
+ [ key ] : [ ...currentEntry , curr ] ,
81
105
}
82
106
} else {
83
107
return {
84
108
...acc ,
85
- [ key ] : [ curr ]
109
+ [ key ] : [ curr ] ,
86
110
}
87
111
}
88
112
} , initial )
89
-
90
113
}
91
114
92
115
const formatDate = ( date : Date ) => {
93
116
let formatter = new Intl . DateTimeFormat ( "en" , {
94
- dateStyle : "long"
95
- } ) ;
117
+ dateStyle : "long" ,
118
+ } )
96
119
return formatter . format ( date )
97
120
}
98
121
99
122
const formatTime = ( date : Date ) => {
100
123
let formatter = new Intl . DateTimeFormat ( "en" , {
101
- timeStyle : "short"
102
- } ) ;
124
+ timeStyle : "short" ,
125
+ } )
103
126
return formatter . format ( date )
104
127
}
105
128
106
-
107
-
108
129
export interface EntryProps {
109
130
entry : TimelineEntry
110
131
}
@@ -117,110 +138,134 @@ export const Entry: React.FC<EntryProps> = ({ entry }) => {
117
138
setExpanded ( ( prev : boolean ) => ! prev )
118
139
}
119
140
120
- return < Box display = { "flex" } flexDirection = { "column" } onClick = { toggleExpanded } >
121
- < Box display = { "flex" } flexDirection = { "row" } justifyContent = { "flex-start" } alignItems = { "center" } >
122
- < Box display = { "flex" } flexDirection = { "column" } justifyContent = { "flex-start" } alignItems = { "center" } mb = { "auto" } >
123
- < Avatar > { "A" } </ Avatar >
124
- </ Box >
125
- < Box m = { "0em 1em" } flexDirection = { "column" } flex = { "1" } >
126
-
127
- < Box display = { "flex" } flexDirection = { "row" } alignItems = { "center" } >
128
- < Typography variant = { "h6" } > { entry . title } </ Typography >
129
- < Typography variant = { "caption" } style = { { marginLeft : "1em" } } > { formatTime ( entry . date ) } </ Typography >
141
+ return (
142
+ < Box display = { "flex" } flexDirection = { "column" } onClick = { toggleExpanded } >
143
+ < Box display = { "flex" } flexDirection = { "row" } justifyContent = { "flex-start" } alignItems = { "center" } >
144
+ < Box display = { "flex" } flexDirection = { "column" } justifyContent = { "flex-start" } alignItems = { "center" } mb = { "auto" } >
145
+ < Avatar > { "A" } </ Avatar >
130
146
</ Box >
131
- < Typography variant = { "body2" } > { entry . description } </ Typography >
132
- < Box >
133
- < BuildLog summary = { "testing" } status = { "success" } expanded = { expanded } onToggleClicked = { toggleExpanded } />
147
+ < Box m = { "0em 1em" } flexDirection = { "column" } flex = { "1" } >
148
+ < Box display = { "flex" } flexDirection = { "row" } alignItems = { "center" } >
149
+ < Typography variant = { "h6" } > { entry . title } </ Typography >
150
+ < Typography variant = { "caption" } style = { { marginLeft : "1em" } } >
151
+ { formatTime ( entry . date ) }
152
+ </ Typography >
153
+ </ Box >
154
+ < Typography variant = { "body2" } > { entry . description } </ Typography >
155
+ < Box >
156
+ < BuildLog
157
+ summary = { entry . buildSummary }
158
+ status = { entry . status }
159
+ expanded = { expanded }
160
+ onToggleClicked = { toggleExpanded }
161
+ />
162
+ </ Box >
134
163
</ Box >
135
164
</ Box >
136
165
</ Box >
137
-
138
- </ Box >
166
+ )
139
167
}
140
168
141
- export const useEntryStyles = makeStyles ( ( theme ) => ( {
142
-
143
- } ) )
144
-
145
- export type BuildLogStatus = "success" | "failure" | "pending"
169
+ export const useEntryStyles = makeStyles ( ( theme ) => ( { } ) )
146
170
147
171
export interface BuildLogProps {
148
172
summary : string
149
173
status : BuildLogStatus
150
174
expanded ?: boolean
151
175
}
152
-
176
+ const STATUS_ICON_SIZE = 18
177
+ const LOADING_SPINNER_SIZE = 14
153
178
export const BuildLog : React . FC < BuildLogProps > = ( { summary, status, expanded } ) => {
154
179
const styles = useBuildLogStyles ( status ) ( )
180
+ let icon : JSX . Element
181
+ if ( status === "failed" ) {
182
+ icon = < StageErrorIcon className = { `${ styles . statusIcon } ${ styles . statusIconError } ` } />
183
+ } else if ( status === "pending" ) {
184
+ icon = < CircularProgress size = { LOADING_SPINNER_SIZE } />
185
+ } else {
186
+ icon = < StageCompleteIcon className = { `${ styles . statusIcon } ${ styles . statusIconSuccess } ` } />
187
+ }
155
188
156
- return < div className = { styles . container } >
157
- < button className = { styles . collapseButton } >
158
- < Box m = { "0.25em 0em" } >
159
- < Typography variant = { "caption" } > { summary } </ Typography >
160
- { expanded && < TerminalOutput output = { sampleOutput } /> }
161
- </ Box >
162
- </ button >
163
- </ div >
164
-
189
+ return (
190
+ < div className = { styles . container } >
191
+ < button className = { styles . collapseButton } >
192
+ < Box m = { "0.25em 0em" } display = { "flex" } flexDirection = { "row" } alignItems = { "center" } >
193
+ < Typography variant = { "caption" } > { summary } </ Typography >
194
+ < Box m = { "0.25em" } > { icon } </ Box >
195
+ </ Box >
196
+ </ button >
197
+ { expanded && < TerminalOutput output = { sampleOutput } /> }
198
+ </ div >
199
+ )
165
200
}
166
201
167
- const useBuildLogStyles = ( status : BuildLogStatus ) => makeStyles ( ( theme ) => ( {
168
- container : {
169
- borderLeft : `2px solid ${ status === "failure" ? theme . palette . error . main : theme . palette . info . main } ` ,
170
- margin : "1em 0em" ,
171
- } ,
172
- collapseButton : {
173
- color : "inherit" ,
174
- textAlign : "left" ,
175
- width : "100%" ,
176
- background : "none" ,
177
- border : 0 ,
178
- alignItems : "center" ,
179
- borderRadius : theme . spacing ( 0.5 ) ,
180
- cursor : "pointer" ,
181
- "&:disabled" : {
202
+ const useBuildLogStyles = ( status : BuildLogStatus ) =>
203
+ makeStyles ( ( theme ) => ( {
204
+ container : {
205
+ borderLeft : `2px solid ${ theme . palette . info . main } ` ,
206
+ margin : "1em 0em" ,
207
+ } ,
208
+ collapseButton : {
182
209
color : "inherit" ,
183
- cursor : "initial" ,
210
+ textAlign : "left" ,
211
+ width : "100%" ,
212
+ background : "none" ,
213
+ border : 0 ,
214
+ alignItems : "center" ,
215
+ borderRadius : theme . spacing ( 0.5 ) ,
216
+ cursor : "pointer" ,
217
+ "&:disabled" : {
218
+ color : "inherit" ,
219
+ cursor : "initial" ,
220
+ } ,
221
+ "&:hover:not(:disabled)" : {
222
+ backgroundColor : theme . palette . type === "dark" ? theme . palette . grey [ 800 ] : theme . palette . grey [ 100 ] ,
223
+ } ,
184
224
} ,
185
- "&:hover:not(:disabled)" : {
186
- backgroundColor : theme . palette . type === "dark" ? theme . palette . grey [ 800 ] : theme . palette . grey [ 100 ] ,
225
+ statusIcon : {
226
+ width : STATUS_ICON_SIZE ,
227
+ height : STATUS_ICON_SIZE ,
228
+ color : theme . palette . text . secondary ,
187
229
} ,
188
- } ,
189
- } ) )
230
+ statusIconError : {
231
+ color : theme . palette . error . main ,
232
+ } ,
233
+ statusIconSuccess : {
234
+ color : theme . palette . success . main ,
235
+ } ,
236
+ } ) )
190
237
191
238
export const Timeline : React . FC = ( ) => {
192
239
const styles = useStyles ( )
193
240
194
241
const entries = mockEntries
195
242
const groupedByDate = groupByDate ( entries )
196
- const allDates = Object . keys ( groupedByDate ) ;
243
+ const allDates = Object . keys ( groupedByDate )
197
244
const sortedDates = allDates . sort ( ( a , b ) => b . localeCompare ( a ) )
198
245
199
246
const days = sortedDates . map ( ( date ) => {
200
-
201
- const entriesForDay = groupedByDate [ date ] ;
247
+ const entriesForDay = groupedByDate [ date ]
202
248
203
249
const entryElements = entriesForDay . map ( ( entry ) => < Entry entry = { entry } isExpanded = { false } /> )
204
250
205
-
206
- return < div className = { styles . root } >
207
- < Typography className = { styles . header } variant = "caption" color = "textSecondary" > { formatDate ( new Date ( Number . parseInt ( date ) ) ) } </ Typography >
208
- { entryElements }
209
-
210
- </ div >
251
+ return (
252
+ < div className = { styles . root } >
253
+ < Typography className = { styles . header } variant = "caption" color = "textSecondary" >
254
+ { formatDate ( new Date ( Number . parseInt ( date ) ) ) }
255
+ </ Typography >
256
+ { entryElements }
257
+ </ div >
258
+ )
211
259
} )
212
260
213
- return < div className = { styles . root } >
214
- { days }
215
- </ div >
216
-
261
+ return < div className = { styles . root } > { days } </ div >
217
262
}
218
263
219
264
export const useStyles = makeStyles ( ( theme ) => ( {
220
265
root : {
221
266
display : "flex" ,
222
267
width : "100%" ,
223
- flexDirection : "column"
268
+ flexDirection : "column" ,
224
269
} ,
225
270
container : {
226
271
display : "flex" ,
@@ -231,5 +276,5 @@ export const useStyles = makeStyles((theme) => ({
231
276
justifyContent : "center" ,
232
277
alignItems : "center" ,
233
278
//textTransform: "uppercase"
234
- }
235
- } ) )
279
+ } ,
280
+ } ) )
0 commit comments