4
4
* See License-AGPL.txt in the project root for license information.
5
5
*/
6
6
7
- import { useEffect , useState } from "react" ;
7
+ import { forwardRef , useEffect , useState } from "react" ;
8
8
import { getGitpodService , gitpodHostUrl } from "../service/service" ;
9
9
import {
10
10
ListUsageRequest ,
@@ -25,6 +25,11 @@ import { toRemoteURL } from "../projects/render-utils";
25
25
import { WorkspaceType } from "@gitpod/gitpod-protocol" ;
26
26
import PillLabel from "./PillLabel" ;
27
27
import { SupportedWorkspaceClass } from "@gitpod/gitpod-protocol/lib/workspace-class" ;
28
+ import DatePicker from "react-datepicker" ;
29
+ import "react-datepicker/dist/react-datepicker.css" ;
30
+ import "./react-datepicker.css" ;
31
+ import { useLocation } from "react-router" ;
32
+ import dayjs from "dayjs" ;
28
33
29
34
interface UsageViewProps {
30
35
attributionId : AttributionId ;
@@ -33,25 +38,29 @@ interface UsageViewProps {
33
38
function UsageView ( { attributionId } : UsageViewProps ) {
34
39
const [ usagePage , setUsagePage ] = useState < ListUsageResponse | undefined > ( undefined ) ;
35
40
const [ errorMessage , setErrorMessage ] = useState ( "" ) ;
36
- const today = new Date ( ) ;
37
- const startOfCurrentMonth = new Date ( today . getFullYear ( ) , today . getMonth ( ) , 1 ) ;
38
- const timestampStartOfCurrentMonth = startOfCurrentMonth . getTime ( ) ;
39
- const [ startDateOfBillMonth , setStartDateOfBillMonth ] = useState ( timestampStartOfCurrentMonth ) ;
40
- const [ endDateOfBillMonth , setEndDateOfBillMonth ] = useState ( Date . now ( ) ) ;
41
+ const startOfCurrentMonth = dayjs ( ) . startOf ( "month" ) ;
42
+ const [ startDate , setStartDate ] = useState ( startOfCurrentMonth ) ;
43
+ const [ endDate , setEndDate ] = useState ( dayjs ( ) ) ;
41
44
const [ totalCreditsUsed , setTotalCreditsUsed ] = useState < number > ( 0 ) ;
42
45
const [ isLoading , setIsLoading ] = useState < boolean > ( true ) ;
43
46
const [ supportedClasses , setSupportedClasses ] = useState < SupportedWorkspaceClass [ ] > ( [ ] ) ;
44
47
48
+ const location = useLocation ( ) ;
45
49
useEffect ( ( ) => {
50
+ const match = / # ( \d { 4 } - \d { 2 } - \d { 2 } ) : ( \d { 4 } - \d { 2 } - \d { 2 } ) / . exec ( location . hash ) ;
51
+ if ( match ) {
52
+ try {
53
+ setStartDate ( dayjs ( match [ 1 ] , "YYYY-MM-DD" ) ) ;
54
+ setEndDate ( dayjs ( match [ 2 ] , "YYYY-MM-DD" ) ) ;
55
+ } catch ( e ) {
56
+ console . error ( e ) ;
57
+ }
58
+ }
46
59
( async ( ) => {
47
60
const classes = await getGitpodService ( ) . server . getSupportedWorkspaceClasses ( ) ;
48
61
setSupportedClasses ( classes ) ;
49
62
} ) ( ) ;
50
- } , [ ] ) ;
51
-
52
- useEffect ( ( ) => {
53
- loadPage ( 1 ) ;
54
- } , [ startDateOfBillMonth , endDateOfBillMonth ] ) ;
63
+ } , [ location ] ) ;
55
64
56
65
const loadPage = async ( page : number = 1 ) => {
57
66
if ( usagePage === undefined ) {
@@ -60,8 +69,8 @@ function UsageView({ attributionId }: UsageViewProps) {
60
69
}
61
70
const request : ListUsageRequest = {
62
71
attributionId : AttributionId . render ( attributionId ) ,
63
- from : startDateOfBillMonth ,
64
- to : endDateOfBillMonth ,
72
+ from : startDate . startOf ( "day" ) . valueOf ( ) ,
73
+ to : endDate . endOf ( "day" ) . valueOf ( ) ,
65
74
order : Ordering . ORDERING_DESCENDING ,
66
75
pagination : {
67
76
perPage : 50 ,
@@ -82,6 +91,18 @@ function UsageView({ attributionId }: UsageViewProps) {
82
91
setIsLoading ( false ) ;
83
92
}
84
93
} ;
94
+ useEffect ( ( ) => {
95
+ if ( startDate . isAfter ( endDate ) ) {
96
+ setErrorMessage ( "The start date needs to be before the end date." ) ;
97
+ return ;
98
+ }
99
+ if ( startDate . add ( 300 , "day" ) . isBefore ( endDate ) ) {
100
+ setErrorMessage ( "Range is too long. Max range is 300 days." ) ;
101
+ return ;
102
+ }
103
+ setErrorMessage ( "" ) ;
104
+ loadPage ( 1 ) ;
105
+ } , [ startDate , endDate ] ) ;
85
106
86
107
const getType = ( type : WorkspaceType ) => {
87
108
if ( type === "regular" ) {
@@ -118,27 +139,24 @@ function UsageView({ attributionId }: UsageViewProps) {
118
139
return inMinutes + " min" ;
119
140
} ;
120
141
121
- const handleMonthClick = ( start : any , end : any ) => {
122
- setStartDateOfBillMonth ( start ) ;
123
- setEndDateOfBillMonth ( end ) ;
142
+ const handleMonthClick = ( start : dayjs . Dayjs , end : dayjs . Dayjs ) => {
143
+ setStartDate ( start ) ;
144
+ setEndDate ( end ) ;
124
145
} ;
125
146
126
147
const getBillingHistory = ( ) => {
127
148
let rows = [ ] ;
128
149
// This goes back 6 months from the current month
129
150
for ( let i = 1 ; i < 7 ; i ++ ) {
130
- const endDateVar = i - 1 ;
131
- const startDate = new Date ( today . getFullYear ( ) , today . getMonth ( ) - i ) ;
132
- const endDate = new Date ( today . getFullYear ( ) , today . getMonth ( ) - endDateVar ) ;
133
- const timeStampOfStartDate = startDate . getTime ( ) ;
134
- const timeStampOfEndDate = endDate . getTime ( ) ;
151
+ const startDate = dayjs ( ) . subtract ( i , "month" ) . startOf ( "month" ) ;
152
+ const endDate = startDate . endOf ( "month" ) ;
135
153
rows . push (
136
154
< div
137
155
key = { `billing${ i } ` }
138
156
className = "text-sm text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 truncate cursor-pointer gp-link"
139
- onClick = { ( ) => handleMonthClick ( timeStampOfStartDate , timeStampOfEndDate ) }
157
+ onClick = { ( ) => handleMonthClick ( startDate , endDate ) }
140
158
>
141
- { startDate . toLocaleString ( "default" , { month : "long" } ) } { startDate . getFullYear ( ) }
159
+ { startDate . format ( "MMMM YYYY" ) }
142
160
</ div > ,
143
161
) ;
144
162
}
@@ -160,13 +178,68 @@ function UsageView({ attributionId }: UsageViewProps) {
160
178
161
179
const headerTitle = attributionId . kind === "team" ? "Team Usage" : "Personal Usage" ;
162
180
181
+ const DateDisplay = forwardRef ( ( arg : any , ref : any ) => (
182
+ < div
183
+ className = "px-2 py-0.5 text-gray-500 bg-gray-50 dark:text-gray-400 dark:bg-gray-800 rounded-md cursor-pointer flex items-center hover:bg-gray-100 dark:hover:bg-gray-700"
184
+ onClick = { arg . onClick }
185
+ ref = { ref }
186
+ >
187
+ < div className = "font-medium" > { arg . value } </ div >
188
+ < div >
189
+ < svg
190
+ width = "20"
191
+ height = "20"
192
+ fill = "currentColor"
193
+ xmlns = "http://www.w3.org/2000/svg"
194
+ onClick = { arg . onClick }
195
+ ref = { ref }
196
+ >
197
+ < path
198
+ fillRule = "evenodd"
199
+ clipRule = "evenodd"
200
+ d = "M5.293 7.293a1 1 0 0 1 1.414 0L10 10.586l3.293-3.293a1 1 0 1 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 0-1.414Z"
201
+ />
202
+ < title > Change Date</ title >
203
+ </ svg >
204
+ </ div >
205
+ </ div >
206
+ ) ) ;
207
+
163
208
return (
164
209
< >
165
210
< Header
166
- title = { headerTitle }
167
- subtitle = { `${ new Date ( startDateOfBillMonth ) . toLocaleDateString ( ) } - ${ new Date (
168
- endDateOfBillMonth ,
169
- ) . toLocaleDateString ( ) } (updated every 15 minutes).`}
211
+ title = {
212
+ < div className = "flex items-baseline" >
213
+ < h1 className = "tracking-tight" > { headerTitle } </ h1 >
214
+ < h2 className = "ml-3" > (updated every 15 minutes).</ h2 >
215
+ </ div >
216
+ }
217
+ subtitle = {
218
+ < div className = "tracking-wide flex mt-3 items-center" >
219
+ < h2 className = "mr-1" > Showing usage from </ h2 >
220
+ < DatePicker
221
+ selected = { startDate . toDate ( ) }
222
+ onChange = { ( date ) => date && setStartDate ( dayjs ( date ) ) }
223
+ selectsStart
224
+ startDate = { startDate . toDate ( ) }
225
+ endDate = { endDate . toDate ( ) }
226
+ maxDate = { endDate . toDate ( ) }
227
+ customInput = { < DateDisplay /> }
228
+ dateFormat = { "MMM d, yyyy" }
229
+ />
230
+ < h2 className = "mx-1" > to</ h2 >
231
+ < DatePicker
232
+ selected = { endDate . toDate ( ) }
233
+ onChange = { ( date ) => date && setEndDate ( dayjs ( date ) ) }
234
+ selectsEnd
235
+ startDate = { startDate . toDate ( ) }
236
+ endDate = { endDate . toDate ( ) }
237
+ minDate = { startDate . toDate ( ) }
238
+ customInput = { < DateDisplay /> }
239
+ dateFormat = { "MMM d, yyyy" }
240
+ />
241
+ </ div >
242
+ }
170
243
/>
171
244
< div className = "app-container pt-5" >
172
245
{ errorMessage && < p className = "text-base" > { errorMessage } </ p > }
@@ -178,18 +251,17 @@ function UsageView({ attributionId }: UsageViewProps) {
178
251
< div className = "text-base text-gray-500 truncate" > Current Month</ div >
179
252
< div
180
253
className = "text-sm text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 truncate cursor-pointer mb-5"
181
- onClick = { ( ) => handleMonthClick ( timestampStartOfCurrentMonth , Date . now ( ) ) }
254
+ onClick = { ( ) => handleMonthClick ( startOfCurrentMonth , dayjs ( ) ) }
182
255
>
183
- { startOfCurrentMonth . toLocaleString ( "default" , { month : "long" } ) } { " " }
184
- { startOfCurrentMonth . getFullYear ( ) }
256
+ { dayjs ( startOfCurrentMonth ) . format ( "MMMM YYYY" ) }
185
257
</ div >
186
258
< div className = "text-base text-gray-500 truncate" > Previous Months</ div >
187
259
{ getBillingHistory ( ) }
188
260
</ div >
189
261
{ ! isLoading && (
190
262
< div >
191
263
< div className = "flex flex-col truncate" >
192
- < div className = "text-base text-gray-500" > Total usage </ div >
264
+ < div className = "text-base text-gray-500" > Total Usage </ div >
193
265
< div className = "flex text-lg text-gray-600 font-semibold" >
194
266
< CreditsSvg className = "my-auto mr-1" />
195
267
< span > { totalCreditsUsed . toLocaleString ( ) } Credits</ span >
@@ -235,11 +307,7 @@ function UsageView({ attributionId }: UsageViewProps) {
235
307
{ " " }
236
308
workspaces
237
309
</ a > { " " }
238
- in{ " " }
239
- { new Date ( startDateOfBillMonth ) . toLocaleString ( "default" , {
240
- month : "long" ,
241
- } ) } { " " }
242
- { new Date ( startDateOfBillMonth ) . getFullYear ( ) } or checked your other teams?
310
+ in { startDate . format ( "MMMM YYYY" ) } or checked your other teams?
243
311
</ p >
244
312
</ div >
245
313
) }
0 commit comments