Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/Insights.tsx
2496 views
1
/**
2
* Copyright (c) 2024 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 { LoadingState } from "@podkit/loading/LoadingState";
8
import { Heading2, Subheading } from "@podkit/typography/Headings";
9
import classNames from "classnames";
10
import { useCallback, useMemo, useState } from "react";
11
import { Accordion } from "./components/accordion/Accordion";
12
import Alert from "./components/Alert";
13
import Header from "./components/Header";
14
import { Item, ItemField, ItemsList } from "./components/ItemsList";
15
import { useWorkspaceSessions } from "./data/insights/list-workspace-sessions-query";
16
import { WorkspaceSessionGroup } from "./insights/WorkspaceSessionGroup";
17
import { gitpodHostUrl } from "./service/service";
18
import dayjs from "dayjs";
19
import { Timestamp } from "@bufbuild/protobuf";
20
import { LoadingButton } from "@podkit/buttons/LoadingButton";
21
import { TextMuted } from "@podkit/typography/TextMuted";
22
import { DownloadInsightsToast } from "./insights/download/DownloadInsights";
23
import { useCurrentOrg } from "./data/organizations/orgs-query";
24
import { useToast } from "./components/toasts/Toasts";
25
import { useTemporaryState } from "./hooks/use-temporary-value";
26
import { DownloadIcon } from "lucide-react";
27
import { Button } from "@podkit/buttons/Button";
28
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@podkit/dropdown/DropDown";
29
import { useInstallationConfiguration } from "./data/installation/installation-config-query";
30
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
31
32
export const Insights = () => {
33
const toDate = useMemo(() => Timestamp.fromDate(new Date()), []);
34
const {
35
data,
36
error: errorMessage,
37
isLoading,
38
isFetchingNextPage,
39
hasNextPage,
40
fetchNextPage,
41
} = useWorkspaceSessions({
42
from: Timestamp.fromDate(new Date(0)),
43
to: toDate,
44
});
45
const { data: installationConfig } = useInstallationConfiguration();
46
const isDedicatedInstallation = !!installationConfig?.isDedicatedInstallation;
47
48
const hasMoreThanOnePage = (data?.pages.length ?? 0) > 1;
49
const sessions = useMemo(() => data?.pages.flatMap((p) => p) ?? [], [data]);
50
const grouped = Object.groupBy(sessions, (ws) => ws.workspace?.id ?? "unknown");
51
const [page, setPage] = useState(0);
52
53
const isLackingPermissions =
54
errorMessage instanceof ApplicationError && errorMessage.code === ErrorCodes.PERMISSION_DENIED;
55
56
return (
57
<>
58
<Header title="Insights" subtitle="Insights into workspace sessions in your organization" />
59
<div className="app-container pt-5 pb-8">
60
<div
61
className={classNames(
62
"flex flex-col items-start space-y-3 justify-end",
63
"md:flex-row md:items-center md:space-x-4 md:space-y-0",
64
)}
65
>
66
<DownloadInsights to={toDate} disabled={isLackingPermissions} />
67
</div>
68
69
<div
70
className={classNames(
71
"flex flex-col items-start space-y-3 justify-between px-3",
72
"md:flex-row md:items-center md:space-x-4 md:space-y-0",
73
)}
74
></div>
75
76
{errorMessage && (
77
<Alert type="error" className="mt-4">
78
{isLackingPermissions ? (
79
<>
80
You don't have <span className="font-medium">Owner</span> permissions to access this
81
organization's insights.
82
</>
83
) : errorMessage instanceof Error ? (
84
errorMessage.message
85
) : (
86
"An error occurred."
87
)}
88
</Alert>
89
)}
90
91
<div className="flex flex-col w-full mb-8">
92
<ItemsList className="mt-2 text-pk-content-secondary">
93
<Item header={false} className="grid grid-cols-12 gap-x-3 bg-pk-surface-tertiary">
94
<ItemField className="col-span-2 my-auto">
95
<span>Type</span>
96
</ItemField>
97
<ItemField className="col-span-5 my-auto">
98
<span>ID</span>
99
</ItemField>
100
<ItemField className="col-span-3 my-auto">
101
<span>User</span>
102
</ItemField>
103
<ItemField className="col-span-2 my-auto">
104
<span>Sessions</span>
105
</ItemField>
106
</Item>
107
108
{isLoading && (
109
<div className="flex items-center justify-center w-full space-x-2 text-pk-content-primary text-sm pt-16 pb-40">
110
<LoadingState delay={false} />
111
<span>Loading usage...</span>
112
</div>
113
)}
114
115
{!isLoading && (
116
<Accordion type="multiple" className="w-full">
117
{Object.entries(grouped).map(([id, sessions]) => {
118
if (!sessions?.length) {
119
return null;
120
}
121
122
return <WorkspaceSessionGroup key={id} id={id} sessions={sessions} />;
123
})}
124
</Accordion>
125
)}
126
127
{/* No results */}
128
{!isLoading && sessions.length === 0 && !errorMessage && (
129
<div className="flex flex-col w-full mb-8">
130
<Heading2 className="text-center mt-8">No sessions found.</Heading2>
131
<Subheading className="text-center mt-1">
132
Have you started any
133
<a className="gp-link" href={gitpodHostUrl.asWorkspacePage().toString()}>
134
{" "}
135
workspaces
136
</a>{" "}
137
recently{!isDedicatedInstallation && " or checked your other organizations"}?
138
</Subheading>
139
</div>
140
)}
141
</ItemsList>
142
</div>
143
144
<div className="mt-4 flex flex-row justify-center">
145
{hasNextPage ? (
146
<LoadingButton
147
variant="secondary"
148
onClick={() => {
149
setPage(page + 1);
150
fetchNextPage();
151
}}
152
loading={isFetchingNextPage}
153
>
154
Load more
155
</LoadingButton>
156
) : (
157
hasMoreThanOnePage && <TextMuted>All workspace sessions are loaded</TextMuted>
158
)}
159
</div>
160
</div>
161
</>
162
);
163
};
164
165
type DownloadUsageProps = {
166
to: Timestamp;
167
disabled?: boolean;
168
};
169
export const DownloadInsights = ({ to, disabled }: DownloadUsageProps) => {
170
const { data: org } = useCurrentOrg();
171
const { toast } = useToast();
172
// When we start the download, we disable the button for a short time
173
const [downloadDisabled, setDownloadDisabled] = useTemporaryState(false, 1000);
174
175
const handleDownload = useCallback(
176
async ({ daysInPast }: { daysInPast: number }) => {
177
if (!org) {
178
return;
179
}
180
const from = Timestamp.fromDate(dayjs().subtract(daysInPast, "day").toDate());
181
182
setDownloadDisabled(true);
183
toast(
184
<DownloadInsightsToast
185
organizationName={org?.slug ?? org?.id}
186
organizationId={org.id}
187
from={from}
188
to={to}
189
/>,
190
{
191
autoHide: false,
192
},
193
);
194
},
195
[org, setDownloadDisabled, to, toast],
196
);
197
198
return (
199
<DropdownMenu>
200
<DropdownMenuTrigger asChild>
201
<Button variant="secondary" className="gap-1" disabled={disabled || downloadDisabled}>
202
<DownloadIcon strokeWidth={3} className="w-4" />
203
<span>Export as CSV</span>
204
</Button>
205
</DropdownMenuTrigger>
206
<DropdownMenuContent>
207
<DropdownMenuItem onClick={() => handleDownload({ daysInPast: 7 })}>Last 7 days</DropdownMenuItem>
208
<DropdownMenuItem onClick={() => handleDownload({ daysInPast: 30 })}>Last 30 days</DropdownMenuItem>
209
<DropdownMenuItem onClick={() => handleDownload({ daysInPast: 365 })}>Last 365 days</DropdownMenuItem>
210
</DropdownMenuContent>
211
</DropdownMenu>
212
);
213
};
214
215
export default Insights;
216
217