Skip to content

Commit 7f075ea

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

File tree

4 files changed

+218
-37
lines changed

4 files changed

+218
-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: 104 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,18 @@ 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+
}
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]);
85106

86107
const getType = (type: WorkspaceType) => {
87108
if (type === "regular") {
@@ -118,27 +139,24 @@ function UsageView({ attributionId }: UsageViewProps) {
118139
return inMinutes + " min";
119140
};
120141

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);
124145
};
125146

126147
const getBillingHistory = () => {
127148
let rows = [];
128149
// This goes back 6 months from the current month
129150
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");
135153
rows.push(
136154
<div
137155
key={`billing${i}`}
138156
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)}
140158
>
141-
{startDate.toLocaleString("default", { month: "long" })} {startDate.getFullYear()}
159+
{startDate.format("MMMM YYYY")}
142160
</div>,
143161
);
144162
}
@@ -160,13 +178,68 @@ function UsageView({ attributionId }: UsageViewProps) {
160178

161179
const headerTitle = attributionId.kind === "team" ? "Team Usage" : "Personal Usage";
162180

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+
163208
return (
164209
<>
165210
<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+
}
170243
/>
171244
<div className="app-container pt-5">
172245
{errorMessage && <p className="text-base">{errorMessage}</p>}
@@ -178,18 +251,17 @@ function UsageView({ attributionId }: UsageViewProps) {
178251
<div className="text-base text-gray-500 truncate">Current Month</div>
179252
<div
180253
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())}
182255
>
183-
{startOfCurrentMonth.toLocaleString("default", { month: "long" })}{" "}
184-
{startOfCurrentMonth.getFullYear()}
256+
{dayjs(startOfCurrentMonth).format("MMMM YYYY")}
185257
</div>
186258
<div className="text-base text-gray-500 truncate">Previous Months</div>
187259
{getBillingHistory()}
188260
</div>
189261
{!isLoading && (
190262
<div>
191263
<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>
193265
<div className="flex text-lg text-gray-600 font-semibold">
194266
<CreditsSvg className="my-auto mr-1" />
195267
<span>{totalCreditsUsed.toLocaleString()} Credits</span>
@@ -235,11 +307,7 @@ function UsageView({ attributionId }: UsageViewProps) {
235307
{" "}
236308
workspaces
237309
</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?
243311
</p>
244312
</div>
245313
)}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
border-radius: 1rem !important;
14+
}
15+
16+
.react-datepicker__month-container {
17+
border-radius: 0.75rem !important;
18+
}
19+
20+
.react-datepicker div {
21+
@apply bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-700
22+
}
23+
24+
.react-datepicker div.react-datepicker__day--in-selecting-range {
25+
@apply bg-gray-400 dark:bg-gray-600 text-gray-300
26+
}
27+
28+
.react-datepicker div.react-datepicker__day--selected {
29+
@apply dark:bg-gray-300 dark:text-gray-800 bg-gray-500 text-gray-100
30+
}
31+
32+
.react-datepicker div.react-datepicker__day--selecting-range-start {
33+
@apply dark:bg-gray-300 dark:text-gray-800 text-gray-200
34+
}
35+
36+
.react-datepicker div.react-datepicker__day--selecting-range-end {
37+
@apply dark:bg-gray-300 dark:text-gray-800 text-gray-200
38+
}
39+
40+
.react-datepicker button {
41+
@apply dark:bg-gray-800 dark:text-gray-200
42+
}
43+
44+
.react-datepicker__triangle::before {
45+
border-bottom-color: transparent !important;
46+
}
47+
.react-datepicker__triangle::after {
48+
border-bottom-color: transparent !important;
49+
}

0 commit comments

Comments
 (0)