Skip to content

Commit b1f7e52

Browse files
committed
[dashboard] flexible usage range
1 parent 184fec8 commit b1f7e52

File tree

4 files changed

+202
-37
lines changed

4 files changed

+202
-37
lines changed

components/dashboard/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
"@gitpod/public-api": "0.1.5",
1010
"@stripe/react-stripe-js": "^1.7.2",
1111
"@stripe/stripe-js": "^1.29.0",
12+
"@types/react-datepicker": "^4.8.0",
1213
"configcat-js": "^6.0.0",
1314
"countries-list": "^2.6.1",
1415
"dayjs": "^1.11.5",
1516
"js-cookie": "^3.0.1",
1617
"monaco-editor": "^0.25.2",
1718
"query-string": "^7.1.1",
1819
"react": "^17.0.1",
20+
"react-datepicker": "^4.8.0",
1921
"react-dom": "^17.0.1",
2022
"react-intl-tel-input": "^8.2.0",
2123
"react-router-dom": "^5.2.0",

components/dashboard/src/components/UsageView.tsx

Lines changed: 100 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* See License-AGPL.txt in the project root for license information.
55
*/
66

7-
import { useEffect, useState } from "react";
7+
import { forwardRef, useEffect, useState } from "react";
88
import { getGitpodService, gitpodHostUrl } from "../service/service";
99
import {
1010
ListUsageRequest,
@@ -25,6 +25,11 @@ import { toRemoteURL } from "../projects/render-utils";
2525
import { WorkspaceType } from "@gitpod/gitpod-protocol";
2626
import PillLabel from "./PillLabel";
2727
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";
2833

2934
interface UsageViewProps {
3035
attributionId: AttributionId;
@@ -33,25 +38,29 @@ interface UsageViewProps {
3338
function UsageView({ attributionId }: UsageViewProps) {
3439
const [usagePage, setUsagePage] = useState<ListUsageResponse | undefined>(undefined);
3540
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());
4144
const [totalCreditsUsed, setTotalCreditsUsed] = useState<number>(0);
4245
const [isLoading, setIsLoading] = useState<boolean>(true);
4346
const [supportedClasses, setSupportedClasses] = useState<SupportedWorkspaceClass[]>([]);
4447

48+
const location = useLocation();
4549
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+
}
4659
(async () => {
4760
const classes = await getGitpodService().server.getSupportedWorkspaceClasses();
4861
setSupportedClasses(classes);
4962
})();
50-
}, []);
51-
52-
useEffect(() => {
53-
loadPage(1);
54-
}, [startDateOfBillMonth, endDateOfBillMonth]);
63+
}, [location]);
5564

5665
const loadPage = async (page: number = 1) => {
5766
if (usagePage === undefined) {
@@ -60,8 +69,8 @@ function UsageView({ attributionId }: UsageViewProps) {
6069
}
6170
const request: ListUsageRequest = {
6271
attributionId: AttributionId.render(attributionId),
63-
from: startDateOfBillMonth,
64-
to: endDateOfBillMonth,
72+
from: startDate.startOf("day").valueOf(),
73+
to: endDate.endOf("day").valueOf(),
6574
order: Ordering.ORDERING_DESCENDING,
6675
pagination: {
6776
perPage: 50,
@@ -82,6 +91,15 @@ function UsageView({ attributionId }: UsageViewProps) {
8291
setIsLoading(false);
8392
}
8493
};
94+
useEffect(() => {
95+
if (startDate.isAfter(endDate)) {
96+
setErrorMessage("The start date needs to be before the end date.");
97+
return;
98+
} else {
99+
setErrorMessage("");
100+
}
101+
loadPage(1);
102+
}, [startDate, endDate]);
85103

86104
const getType = (type: WorkspaceType) => {
87105
if (type === "regular") {
@@ -118,27 +136,24 @@ function UsageView({ attributionId }: UsageViewProps) {
118136
return inMinutes + " min";
119137
};
120138

121-
const handleMonthClick = (start: any, end: any) => {
122-
setStartDateOfBillMonth(start);
123-
setEndDateOfBillMonth(end);
139+
const handleMonthClick = (start: dayjs.Dayjs, end: dayjs.Dayjs) => {
140+
setStartDate(start);
141+
setEndDate(end);
124142
};
125143

126144
const getBillingHistory = () => {
127145
let rows = [];
128146
// This goes back 6 months from the current month
129147
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();
148+
const startDate = dayjs().subtract(i, "month").startOf("month");
149+
const endDate = startDate.endOf("month");
135150
rows.push(
136151
<div
137152
key={`billing${i}`}
138153
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)}
154+
onClick={() => handleMonthClick(startDate, endDate)}
140155
>
141-
{startDate.toLocaleString("default", { month: "long" })} {startDate.getFullYear()}
156+
{startDate.format("MMMM YYYY")}
142157
</div>,
143158
);
144159
}
@@ -160,13 +175,67 @@ function UsageView({ attributionId }: UsageViewProps) {
160175

161176
const headerTitle = attributionId.kind === "team" ? "Team Usage" : "Personal Usage";
162177

178+
const DateDisplay = forwardRef((arg: any, ref: any) => (
179+
<div
180+
className="px-2 p-1 text-gray-500 bg-gray-100 dark:text-gray-400 dark:bg-gray-800 rounded-md cursor-pointer flex items-center"
181+
onClick={arg.onClick}
182+
ref={ref}
183+
>
184+
<div className="font-medium">{arg.value}</div>
185+
<div>
186+
<svg
187+
width="20"
188+
height="20"
189+
fill="currentColor"
190+
xmlns="http://www.w3.org/2000/svg"
191+
onClick={arg.onClick}
192+
ref={ref}
193+
>
194+
<path
195+
fillRule="evenodd"
196+
clipRule="evenodd"
197+
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"
198+
/>
199+
<title>Change Date</title>
200+
</svg>
201+
</div>
202+
</div>
203+
));
204+
163205
return (
164206
<>
165207
<Header
166-
title={headerTitle}
167-
subtitle={`${new Date(startDateOfBillMonth).toLocaleDateString()} - ${new Date(
168-
endDateOfBillMonth,
169-
).toLocaleDateString()} (updated every 15 minutes).`}
208+
title={
209+
<div className="flex items-baseline">
210+
<h1 className="tracking-tight">{headerTitle}</h1>
211+
</div>
212+
}
213+
subtitle={
214+
<div className="tracking-wide flex mt-3 items-center">
215+
<h2 className="mr-2">Showing usage from </h2>
216+
<DatePicker
217+
selected={startDate.toDate()}
218+
onChange={(date) => date && setStartDate(dayjs(date))}
219+
selectsStart
220+
startDate={startDate.toDate()}
221+
endDate={endDate.toDate()}
222+
maxDate={endDate.toDate()}
223+
customInput={<DateDisplay />}
224+
dateFormat={"MMM d, yyyy"}
225+
/>
226+
<h2 className="mx-2">to</h2>
227+
<DatePicker
228+
selected={endDate.toDate()}
229+
onChange={(date) => date && setEndDate(dayjs(date))}
230+
selectsEnd
231+
startDate={startDate.toDate()}
232+
endDate={endDate.toDate()}
233+
minDate={startDate.toDate()}
234+
customInput={<DateDisplay />}
235+
dateFormat={"MMM d, yyyy"}
236+
/>
237+
</div>
238+
}
170239
/>
171240
<div className="app-container pt-5">
172241
{errorMessage && <p className="text-base">{errorMessage}</p>}
@@ -178,18 +247,17 @@ function UsageView({ attributionId }: UsageViewProps) {
178247
<div className="text-base text-gray-500 truncate">Current Month</div>
179248
<div
180249
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())}
250+
onClick={() => handleMonthClick(startOfCurrentMonth, dayjs())}
182251
>
183-
{startOfCurrentMonth.toLocaleString("default", { month: "long" })}{" "}
184-
{startOfCurrentMonth.getFullYear()}
252+
{dayjs(startOfCurrentMonth).format("MMMM YYYY")}
185253
</div>
186254
<div className="text-base text-gray-500 truncate">Previous Months</div>
187255
{getBillingHistory()}
188256
</div>
189257
{!isLoading && (
190258
<div>
191259
<div className="flex flex-col truncate">
192-
<div className="text-base text-gray-500">Total usage</div>
260+
<div className="text-base text-gray-500">Total Usage</div>
193261
<div className="flex text-lg text-gray-600 font-semibold">
194262
<CreditsSvg className="my-auto mr-1" />
195263
<span>{totalCreditsUsed.toLocaleString()} Credits</span>
@@ -235,11 +303,7 @@ function UsageView({ attributionId }: UsageViewProps) {
235303
{" "}
236304
workspaces
237305
</a>{" "}
238-
in{" "}
239-
{new Date(startDateOfBillMonth).toLocaleString("default", {
240-
month: "long",
241-
})}{" "}
242-
{new Date(startDateOfBillMonth).getFullYear()} or checked your other teams?
306+
in {startDate.format("MMMM YYYY")} or checked your other teams?
243307
</p>
244308
</div>
245309
)}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
.react-datepicker-wrapper {
8+
width: fit-content !important;
9+
}
10+
11+
.react-datepicker {
12+
border: 0px !important;
13+
}
14+
15+
.react-datepicker div {
16+
@apply bg-gray-100 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-700
17+
}
18+
19+
.react-datepicker div.react-datepicker__day--in-selecting-range {
20+
@apply bg-gray-400 dark:bg-gray-600
21+
}
22+
23+
.react-datepicker div.react-datepicker__day--selected {
24+
@apply dark:bg-gray-300 dark:text-gray-800 text-gray-200
25+
}
26+
27+
.react-datepicker div.react-datepicker__day--selecting-range-start {
28+
@apply dark:bg-gray-300 dark:text-gray-800
29+
}
30+
31+
.react-datepicker div.react-datepicker__day--selecting-range-end {
32+
@apply dark:bg-gray-300 dark:text-gray-800
33+
}
34+
35+
.react-datepicker button {
36+
@apply dark:bg-gray-800 dark:text-gray-200
37+
}

0 commit comments

Comments
 (0)