Skip to content

Commit a7373f1

Browse files
author
Laurie T. Malau
committed
Show workspace and user details
1 parent e75f602 commit a7373f1

File tree

6 files changed

+147
-39
lines changed

6 files changed

+147
-39
lines changed
Loading

components/dashboard/src/teams/TeamUsage.tsx

+92-38
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,22 @@ import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
2222
import { ReactComponent as CreditsSvg } from "../images/credits.svg";
2323
import { ReactComponent as Spinner } from "../icons/Spinner.svg";
2424
import { ReactComponent as SortArrow } from "../images/sort-arrow.svg";
25+
import { ReactComponent as UsageIcon } from "../images/usage-default.svg";
2526
import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode";
27+
import { toRemoteURL } from "../projects/render-utils";
28+
import { User } from "@gitpod/gitpod-protocol";
29+
30+
interface ExtendedBillableSession extends BillableSession {
31+
contextURL: string;
32+
user?: User;
33+
}
2634

2735
function TeamUsage() {
2836
const { teams } = useContext(TeamsContext);
2937
const location = useLocation();
3038
const team = getCurrentTeam(location, teams);
3139
const [teamBillingMode, setTeamBillingMode] = useState<BillingMode | undefined>(undefined);
32-
const [billedUsage, setBilledUsage] = useState<BillableSession[]>([]);
40+
const [billedUsage, setBilledUsage] = useState<ExtendedBillableSession[]>([]);
3341
const [currentPage, setCurrentPage] = useState(1);
3442
const [resultsPerPage] = useState(50);
3543
const [errorMessage, setErrorMessage] = useState("");
@@ -55,6 +63,9 @@ function TeamUsage() {
5563
if (!team) {
5664
return;
5765
}
66+
if (billedUsage.length === 0) {
67+
setIsLoading(true);
68+
}
5869
(async () => {
5970
const attributionId = AttributionId.render({ kind: "team", teamId: team.id });
6071
const request: BillableSessionRequest = {
@@ -64,8 +75,20 @@ function TeamUsage() {
6475
to: endDateOfBillMonth,
6576
};
6677
try {
67-
const billedUsageResult = await getGitpodService().server.listBilledUsage(request);
68-
setBilledUsage(billedUsageResult);
78+
const { server } = getGitpodService();
79+
const billedUsageResult = await server.listBilledUsage(request);
80+
const extendedResult = await Promise.all(
81+
billedUsageResult.map(async (res) => {
82+
const ws = await server.getWorkspaceForBillableSession(res.workspaceId, attributionId);
83+
if (!res.userId) {
84+
return Object.assign(res, { contextURL: ws.contextURL });
85+
} else {
86+
const user = await server.getUserForBillableSession(res.userId, res.attributionId);
87+
return Object.assign(res, { contextURL: ws.contextURL, user: user });
88+
}
89+
}),
90+
);
91+
setBilledUsage(extendedResult);
6992
} catch (error) {
7093
if (error.code === ErrorCodes.PERMISSION_DENIED) {
7194
setErrorMessage("Access to usage details is restricted to team owners.");
@@ -141,7 +164,7 @@ function TeamUsage() {
141164
const displayTime = (time: string) => {
142165
const options: Intl.DateTimeFormatOptions = {
143166
day: "numeric",
144-
month: "long",
167+
month: "short",
145168
year: "numeric",
146169
hour: "numeric",
147170
minute: "numeric",
@@ -184,7 +207,7 @@ function TeamUsage() {
184207
</div>
185208
</div>
186209
</div>
187-
{billedUsage.length === 0 && !errorMessage && !isLoading && (
210+
{!isLoading && billedUsage.length === 0 && !errorMessage && (
188211
<div className="flex flex-col w-full mb-8">
189212
<h3 className="text-center text-gray-500 mt-8">No sessions found.</h3>
190213
<p className="text-center text-gray-500 mt-1">
@@ -212,26 +235,23 @@ function TeamUsage() {
212235
{billedUsage.length > 0 && !isLoading && (
213236
<div className="flex flex-col w-full mb-8">
214237
<ItemsList className="mt-2 text-gray-500">
215-
<Item header={false} className="grid grid-cols-5 bg-gray-100 mb-5">
216-
<ItemField className="my-auto">
238+
<Item header={false} className="grid grid-cols-12 gap-x-3 bg-gray-100 mb-5">
239+
<ItemField className="col-span-2 my-auto">
217240
<span>Type</span>
218241
</ItemField>
219-
<ItemField className="my-auto">
220-
<span>Class</span>
242+
<ItemField className="col-span-5 my-auto">
243+
<span>ID</span>
221244
</ItemField>
222245
<ItemField className="my-auto">
223-
<span>Usage</span>
224-
</ItemField>
225-
<ItemField className="flex my-auto">
226-
<CreditsSvg className="my-auto mr-1" />
227246
<span>Credits</span>
228247
</ItemField>
229-
<ItemField className="my-auto cursor-pointer">
248+
<ItemField className="my-auto" />
249+
<ItemField className="col-span-3 my-auto cursor-pointer">
230250
<span
231251
className="flex my-auto"
232252
onClick={() => setIsStartedTimeDescending(!isStartedTimeDescending)}
233253
>
234-
Started
254+
Timestamp
235255
<SortArrow
236256
className={`h-4 w-4 my-auto ${
237257
isStartedTimeDescending ? "" : " transform rotate-180"
@@ -241,30 +261,64 @@ function TeamUsage() {
241261
</ItemField>
242262
</Item>
243263
{currentPaginatedResults &&
244-
currentPaginatedResults.map((usage) => (
245-
<div
246-
key={usage.instanceId}
247-
className="flex p-3 grid grid-cols-5 justify-between transition ease-in-out rounded-xl focus:bg-gitpod-kumquat-light"
248-
>
249-
<div className="my-auto">
250-
<span>{getType(usage.workspaceType)}</span>
251-
</div>
252-
<div className="my-auto">
253-
<span className="text-gray-400">{usage.workspaceClass}</span>
254-
</div>
255-
<div className="my-auto">
256-
<span className="text-gray-700">{getMinutes(usage)}</span>
257-
</div>
258-
<div className="my-auto">
259-
<span className="text-gray-700">{usage.credits.toFixed(1)}</span>
260-
</div>
261-
<div className="my-auto">
262-
<span className="text-gray-400">
263-
{displayTime(usage.startTime)}
264-
</span>
264+
currentPaginatedResults.map((usage) => {
265+
return (
266+
<div
267+
key={usage.instanceId}
268+
className="flex p-3 grid grid-cols-12 gap-x-3 justify-between transition ease-in-out rounded-xl focus:bg-gitpod-kumquat-light"
269+
>
270+
<div className="flex flex-col col-span-2 my-auto">
271+
<span>{getType(usage.workspaceType)}</span>
272+
<span className="text-sm text-gray-400">
273+
{usage.workspaceClass}
274+
</span>
275+
</div>
276+
<div className="flex flex-col col-span-5 my-auto">
277+
<span className="truncate text-gray-700">
278+
{usage.workspaceId}
279+
</span>
280+
<span className="text-sm truncate text-gray-400">
281+
{toRemoteURL(usage.contextURL)}
282+
</span>
283+
</div>
284+
<div className="flex flex-col my-auto">
285+
<span className="text-right text-gray-700">
286+
{usage.credits.toFixed(1)}
287+
</span>
288+
<span className="text-right text-sm text-gray-400">
289+
{getMinutes(usage)}
290+
</span>
291+
</div>
292+
<div className="my-auto" />
293+
<div className="flex flex-col col-span-3 my-auto">
294+
<span className="text-gray-400 truncate">
295+
{displayTime(usage.startTime)}
296+
</span>
297+
<div className="flex">
298+
{usage.workspaceType === "prebuild" ? (
299+
<UsageIcon className="my-auto" />
300+
) : (
301+
""
302+
)}
303+
{usage.workspaceType === "prebuild" ? (
304+
<span className="text-sm text-gray-400">Gitpod</span>
305+
) : (
306+
<div className="flex">
307+
<img
308+
className="my-auto rounded-full w-4 h-4 inline-block align-text-bottom mr-2 overflow-hidden"
309+
src={usage.user?.avatarUrl || ""}
310+
alt="user avatar"
311+
/>
312+
<span className="text-sm text-gray-400">
313+
{usage.user?.name}
314+
</span>
315+
</div>
316+
)}
317+
</div>
318+
</div>
265319
</div>
266-
</div>
267-
))}
320+
);
321+
})}
268322
</ItemsList>
269323
{billedUsage.length > resultsPerPage && (
270324
<Pagination

components/gitpod-protocol/src/gitpod-service.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import { JsonRpcProxy, JsonRpcServer } from "./messaging/proxy-factory";
4343
import { Disposable, CancellationTokenSource } from "vscode-jsonrpc";
4444
import { HeadlessLogUrls } from "./headless-workspace-log";
4545
import { WorkspaceInstance, WorkspaceInstancePort, WorkspaceInstancePhase } from "./workspace-instance";
46-
import { AdminServer } from "./admin-protocol";
46+
import { AdminServer, WorkspaceAndInstance } from "./admin-protocol";
4747
import { GitpodHostUrl } from "./util/gitpod-host-url";
4848
import { WebSocketConnectionProvider } from "./messaging/browser/connection";
4949
import { PermissionName } from "./permission";
@@ -296,6 +296,8 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
296296
setSpendingLimitForTeam(teamId: string, spendingLimit: number): Promise<void>;
297297

298298
listBilledUsage(req: BillableSessionRequest): Promise<BillableSession[]>;
299+
getUserForBillableSession(userId: string, attributionId: string): Promise<User>;
300+
getWorkspaceForBillableSession(workspaceId: string, attributionId: string): Promise<WorkspaceAndInstance>;
299301

300302
setUsageAttribution(usageAttribution: string): Promise<void>;
301303

components/server/ee/src/workspace/gitpod-server-impl.ts

+36
Original file line numberDiff line numberDiff line change
@@ -2229,6 +2229,42 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
22292229
return this.billingModes.getBillingModeForTeam(team, new Date());
22302230
}
22312231

2232+
async getUserForBillableSession(ctx: TraceContextWithSpan, userId: string, attributionId: string): Promise<User> {
2233+
traceAPIParams(ctx, { userId });
2234+
2235+
await this.guardCostCenterAccess(ctx, userId, attributionId, "get");
2236+
2237+
let result: User | undefined;
2238+
try {
2239+
result = await this.userDB.findUserById(userId);
2240+
} catch (e) {
2241+
throw new ResponseError(ErrorCodes.INTERNAL_SERVER_ERROR, e.toString());
2242+
}
2243+
2244+
if (!result) {
2245+
throw new ResponseError(ErrorCodes.NOT_FOUND, "not found");
2246+
}
2247+
return this.censorUser(result);
2248+
}
2249+
2250+
async getWorkspaceForBillableSession(
2251+
ctx: TraceContext,
2252+
workspaceId: string,
2253+
attributionId: string,
2254+
): Promise<WorkspaceAndInstance> {
2255+
traceAPIParams(ctx, { workspaceId });
2256+
traceWI(ctx, { workspaceId });
2257+
2258+
const user = this.checkAndBlockUser("getWorkspaceForBillableSession");
2259+
await this.guardCostCenterAccess(ctx, user.id, attributionId, "get");
2260+
2261+
const result = await this.workspaceDb.trace(ctx).findWorkspaceAndInstance(workspaceId);
2262+
if (!result) {
2263+
throw new ResponseError(ErrorCodes.NOT_FOUND, "not found");
2264+
}
2265+
return result;
2266+
}
2267+
22322268
// (SaaS) – admin
22332269
async adminGetAccountStatement(ctx: TraceContext, userId: string): Promise<AccountStatement> {
22342270
traceAPIParams(ctx, { userId });

components/server/src/auth/rate-limiter.ts

+2
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig {
211211
subscribeTeamToStripe: { group: "default", points: 1 },
212212
getStripePortalUrlForTeam: { group: "default", points: 1 },
213213
listBilledUsage: { group: "default", points: 1 },
214+
getUserForBillableSession: { group: "default", points: 1 },
215+
getWorkspaceForBillableSession: { group: "default", points: 1 },
214216
getBillingModeForTeam: { group: "default", points: 1 },
215217
getBillingModeForUser: { group: "default", points: 1 },
216218

components/server/src/workspace/gitpod-server-impl.ts

+13
Original file line numberDiff line numberDiff line change
@@ -3216,6 +3216,19 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
32163216
async listBilledUsage(ctx: TraceContext, req: BillableSessionRequest): Promise<BillableSession[]> {
32173217
throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`);
32183218
}
3219+
3220+
async getUserForBillableSession(ctx: TraceContextWithSpan, userId: string, attributionId: string): Promise<User> {
3221+
throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`);
3222+
}
3223+
3224+
async getWorkspaceForBillableSession(
3225+
ctx: TraceContext,
3226+
workspaceId: string,
3227+
attributionId: string,
3228+
): Promise<WorkspaceAndInstance> {
3229+
throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`);
3230+
}
3231+
32193232
async getSpendingLimitForTeam(ctx: TraceContext, teamId: string): Promise<number | undefined> {
32203233
throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`);
32213234
}

0 commit comments

Comments
 (0)