Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/usage/UsageView.tsx
2499 views
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
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
8
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
9
import { Ordering } from "@gitpod/gitpod-protocol/lib/usage";
10
import dayjs, { Dayjs } from "dayjs";
11
import { FC, useCallback, useMemo } from "react";
12
import { useHistory, useLocation } from "react-router";
13
import Header from "../components/Header";
14
import { Item, ItemField, ItemsList } from "../components/ItemsList";
15
import { useListUsage } from "../data/usage/usage-query";
16
import Spinner from "../icons/Spinner.svg";
17
import Pagination from "../Pagination/Pagination";
18
import { gitpodHostUrl } from "../service/service";
19
import { Heading2, Subheading } from "../components/typography/headings";
20
import { UsageSummaryData } from "./UsageSummary";
21
import { UsageEntry } from "./UsageEntry";
22
import Alert from "../components/Alert";
23
import classNames from "classnames";
24
import { UsageDateFilters } from "./UsageDateFilters";
25
import { DownloadUsage } from "./download/DownloadUsage";
26
import { useQueryParams } from "../hooks/use-query-params";
27
28
const DATE_PARAM_FORMAT = "YYYY-MM-DD";
29
30
interface UsageViewProps {
31
attributionId: AttributionId;
32
}
33
34
export const UsageView: FC<UsageViewProps> = ({ attributionId }) => {
35
const location = useLocation();
36
const history = useHistory();
37
const params = useQueryParams();
38
39
// page filter params are all in the url as querystring params
40
const startOfCurrentMonth = dayjs().startOf("month");
41
const startDate = getDateFromParam(params.get("start")) || startOfCurrentMonth;
42
const endDate = getDateFromParam(params.get("end")) || dayjs();
43
const page = getNumberFromParam(params.get("page")) || 1;
44
45
const usagePage = useListUsage({
46
attributionId: AttributionId.render(attributionId),
47
from: startDate.startOf("day").valueOf(),
48
to: endDate.endOf("day").valueOf(),
49
order: Ordering.ORDERING_DESCENDING,
50
pagination: {
51
perPage: 50,
52
page,
53
},
54
});
55
56
// Updates the query params w/ new values overlaid onto existing values
57
const updatePageParams = useCallback(
58
(pageParams: { start?: Dayjs; end?: Dayjs; page?: number }) => {
59
const newParams = new URLSearchParams(params);
60
newParams.set("start", (pageParams.start || startDate).format(DATE_PARAM_FORMAT));
61
newParams.set("end", (pageParams.end || endDate).format(DATE_PARAM_FORMAT));
62
newParams.set("page", `${pageParams.page || page}`);
63
64
history.push(`${location.pathname}?${newParams}`);
65
},
66
[endDate, history, location.pathname, page, params, startDate],
67
);
68
69
const handlePageChange = useCallback(
70
(val: number) => {
71
updatePageParams({ page: val });
72
},
73
[updatePageParams],
74
);
75
76
const errorMessage = useMemo(() => {
77
let errorMessage = "";
78
79
if (usagePage.error) {
80
if ((usagePage.error as any).code === ErrorCodes.PERMISSION_DENIED) {
81
errorMessage = "Access to usage details is restricted to team owners.";
82
} else {
83
errorMessage = `${usagePage.error?.message}`;
84
}
85
}
86
87
return errorMessage;
88
}, [usagePage.error]);
89
90
const usageEntries = usagePage.data?.usageEntriesList ?? [];
91
92
const readableSchedulerDuration = useMemo(() => {
93
const intervalMinutes = usagePage.data?.ledgerIntervalMinutes;
94
if (!intervalMinutes) {
95
return "";
96
}
97
98
return `${intervalMinutes} minute${intervalMinutes !== 1 ? "s" : ""}`;
99
}, [usagePage.data]);
100
101
return (
102
<>
103
<Header
104
title="Usage"
105
subtitle={
106
"Organization usage" +
107
(readableSchedulerDuration ? ", updated every " + readableSchedulerDuration : "") +
108
"."
109
}
110
/>
111
<div className="app-container pt-5">
112
<div
113
className={classNames(
114
"flex flex-col items-start space-y-3 justify-between px-3",
115
"md:flex-row md:items-center md:space-x-4 md:space-y-0",
116
)}
117
>
118
<UsageDateFilters startDate={startDate} endDate={endDate} onDateRangeChange={updatePageParams} />
119
<DownloadUsage attributionId={attributionId} startDate={startDate} endDate={endDate} />
120
</div>
121
122
{errorMessage && (
123
<Alert type="error" className="mt-4">
124
{errorMessage}
125
</Alert>
126
)}
127
128
<UsageSummaryData creditsUsed={usagePage.data?.creditsUsed} />
129
130
<div className="flex flex-col w-full mb-8">
131
<ItemsList className="mt-2 text-gray-400 dark:text-gray-500">
132
<Item header={false} className="grid grid-cols-12 gap-x-3 bg-pk-surface-secondary">
133
<ItemField className="col-span-2 my-auto ">
134
<span>Type</span>
135
</ItemField>
136
<ItemField className="col-span-5 my-auto">
137
<span>ID</span>
138
</ItemField>
139
<ItemField className="my-auto">
140
<span>Credits</span>
141
</ItemField>
142
<ItemField className="my-auto" />
143
<ItemField className="my-auto">
144
<span>Timestamp</span>
145
</ItemField>
146
</Item>
147
148
{/* results loading */}
149
{usagePage.isLoading && (
150
<div className="flex items-center justify-center w-full space-x-2 text-gray-400 text-sm pt-16 pb-40">
151
<img alt="Loading Spinner" className="h-4 w-4 animate-spin" src={Spinner} />
152
<span>Loading usage...</span>
153
</div>
154
)}
155
156
{/* results */}
157
{!usagePage.isLoading &&
158
usageEntries.map((usage) => {
159
return <UsageEntry key={usage.id} usage={usage} />;
160
})}
161
162
{/* No results */}
163
{!usagePage.isLoading && usageEntries.length === 0 && !errorMessage && (
164
<div className="flex flex-col w-full mb-8">
165
<Heading2 className="text-center mt-8">No sessions found.</Heading2>
166
<Subheading className="text-center mt-1">
167
Have you started any
168
<a className="gp-link" href={gitpodHostUrl.asWorkspacePage().toString()}>
169
{" "}
170
workspaces
171
</a>{" "}
172
in {startDate.format("MMMM YYYY")} or checked your other organizations?
173
</Subheading>
174
</div>
175
)}
176
</ItemsList>
177
178
{usagePage.data && usagePage.data.pagination && usagePage.data.pagination.totalPages > 1 && (
179
<Pagination
180
currentPage={usagePage.data.pagination.page}
181
setPage={handlePageChange}
182
totalNumberOfPages={usagePage.data.pagination.totalPages}
183
/>
184
)}
185
</div>
186
</div>
187
</>
188
);
189
};
190
191
const getDateFromParam = (paramValue: string | null) => {
192
if (!paramValue) {
193
return null;
194
}
195
196
try {
197
const date = dayjs(paramValue, DATE_PARAM_FORMAT, true);
198
if (!date.isValid()) {
199
return null;
200
}
201
202
return date;
203
} catch (e) {
204
return null;
205
}
206
};
207
208
const getNumberFromParam = (paramValue: string | null) => {
209
if (!paramValue) {
210
return null;
211
}
212
213
try {
214
const number = Number.parseInt(paramValue, 10);
215
if (Number.isNaN(number)) {
216
return null;
217
}
218
219
return number;
220
} catch (e) {
221
return null;
222
}
223
};
224
225