Skip to content

Commit 0011ee3

Browse files
author
Laurie T. Malau
committed
Implement Usage view
1 parent da75da4 commit 0011ee3

File tree

10 files changed

+290
-7
lines changed

10 files changed

+290
-7
lines changed

components/dashboard/src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const JoinTeam = React.lazy(() => import(/* webpackPrefetch: true */ "./teams/Jo
6868
const Members = React.lazy(() => import(/* webpackPrefetch: true */ "./teams/Members"));
6969
const TeamSettings = React.lazy(() => import(/* webpackPrefetch: true */ "./teams/TeamSettings"));
7070
const TeamBilling = React.lazy(() => import(/* webpackPrefetch: true */ "./teams/TeamBilling"));
71+
const TeamUsage = React.lazy(() => import(/* webpackPrefetch: true */ "./teams/TeamUsage"));
7172
const NewProject = React.lazy(() => import(/* webpackPrefetch: true */ "./projects/NewProject"));
7273
const ConfigureProject = React.lazy(() => import(/* webpackPrefetch: true */ "./projects/ConfigureProject"));
7374
const Projects = React.lazy(() => import(/* webpackPrefetch: true */ "./projects/Projects"));
@@ -440,6 +441,9 @@ function App() {
440441
if (maybeProject === "billing") {
441442
return <TeamBilling />;
442443
}
444+
if (maybeProject === "usage") {
445+
return <TeamUsage />;
446+
}
443447
if (resourceOrPrebuild === "prebuilds") {
444448
return <Prebuilds />;
445449
}

components/dashboard/src/Menu.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export default function Menu() {
6464
"members",
6565
"settings",
6666
"billing",
67+
"usage",
6768
// admin sub-pages
6869
"users",
6970
"workspaces",
@@ -220,7 +221,7 @@ export default function Menu() {
220221
teamSettingsList.push({
221222
title: "Settings",
222223
link: `/t/${team.slug}/settings`,
223-
alternatives: getTeamSettingsMenu({ team, showPaymentUI }).flatMap((e) => e.link),
224+
alternatives: getTeamSettingsMenu({ team, showPaymentUI, showUsageBasedUI }).flatMap((e) => e.link),
224225
});
225226
}
226227

components/dashboard/src/teams/TeamBilling.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export default function TeamBilling() {
3131
const team = getCurrentTeam(location, teams);
3232
const [members, setMembers] = useState<TeamMemberInfo[]>([]);
3333
const [teamSubscription, setTeamSubscription] = useState<TeamSubscription2 | undefined>();
34-
const { showPaymentUI, currency, setCurrency } = useContext(PaymentContext);
34+
const { showPaymentUI, showUsageBasedUI, currency, setCurrency } = useContext(PaymentContext);
3535
const [pendingTeamPlan, setPendingTeamPlan] = useState<PendingPlan | undefined>();
3636
const [pollTeamSubscriptionTimeout, setPollTeamSubscriptionTimeout] = useState<NodeJS.Timeout | undefined>();
3737

@@ -140,7 +140,7 @@ export default function TeamBilling() {
140140

141141
return (
142142
<PageWithSubMenu
143-
subMenu={getTeamSettingsMenu({ team, showPaymentUI })}
143+
subMenu={getTeamSettingsMenu({ team, showPaymentUI, showUsageBasedUI })}
144144
title="Billing"
145145
subtitle="Manage team billing and plans."
146146
>

components/dashboard/src/teams/TeamSettings.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import { getGitpodService, gitpodHostUrl } from "../service/service";
1515
import { UserContext } from "../user-context";
1616
import { getCurrentTeam, TeamsContext } from "./teams-context";
1717

18-
export function getTeamSettingsMenu(params: { team?: Team; showPaymentUI?: boolean }) {
19-
const { team, showPaymentUI } = params;
18+
export function getTeamSettingsMenu(params: { team?: Team; showPaymentUI?: boolean; showUsageBasedUI?: boolean }) {
19+
const { team, showPaymentUI, showUsageBasedUI } = params;
2020
return [
2121
{
2222
title: "General",
@@ -30,6 +30,14 @@ export function getTeamSettingsMenu(params: { team?: Team; showPaymentUI?: boole
3030
},
3131
]
3232
: []),
33+
...(showUsageBasedUI
34+
? [
35+
{
36+
title: "Usage",
37+
link: [`/t/${team?.slug}/usage`],
38+
},
39+
]
40+
: []),
3341
];
3442
}
3543

@@ -41,7 +49,7 @@ export default function TeamSettings() {
4149
const { user } = useContext(UserContext);
4250
const location = useLocation();
4351
const team = getCurrentTeam(location, teams);
44-
const { showPaymentUI } = useContext(PaymentContext);
52+
const { showPaymentUI, showUsageBasedUI } = useContext(PaymentContext);
4553

4654
const close = () => setModal(false);
4755

@@ -68,7 +76,7 @@ export default function TeamSettings() {
6876
return (
6977
<>
7078
<PageWithSubMenu
71-
subMenu={getTeamSettingsMenu({ team, showPaymentUI })}
79+
subMenu={getTeamSettingsMenu({ team, showPaymentUI, showUsageBasedUI })}
7280
title="Settings"
7381
subtitle="Manage general team settings."
7482
>
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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 { useContext, useEffect, useState } from "react";
8+
import { useLocation } from "react-router";
9+
import { PageWithSubMenu } from "../components/PageWithSubMenu";
10+
import { getCurrentTeam, TeamsContext } from "./teams-context";
11+
import { getTeamSettingsMenu } from "./TeamSettings";
12+
import { PaymentContext } from "../payment-context";
13+
import { getGitpodService } from "../service/service";
14+
import { BillableSession } from "@gitpod/gitpod-protocol/lib/usage";
15+
import { Item, ItemField, ItemsList } from "../components/ItemsList";
16+
import moment from "moment";
17+
import Property from "../admin/Property";
18+
import Arrow from "../components/Arrow";
19+
20+
function TeamUsage() {
21+
const { teams } = useContext(TeamsContext);
22+
const { showPaymentUI, showUsageBasedUI } = useContext(PaymentContext);
23+
const location = useLocation();
24+
const team = getCurrentTeam(location, teams);
25+
const [billedUsage, setBilledUsage] = useState<BillableSession[]>([]);
26+
27+
useEffect(() => {
28+
if (!team) {
29+
return;
30+
}
31+
(async () => {
32+
const billedUsageResult = await getGitpodService().server.getBilledUsage("some-attribution-id");
33+
setBilledUsage(billedUsageResult);
34+
})();
35+
}, [team]);
36+
37+
const getType = (type: string) => {
38+
if (type === "regular") {
39+
return "Workspace";
40+
}
41+
return "Prebuild";
42+
};
43+
44+
const getHours = (endTime: number, startTime: number) => {
45+
return (endTime - startTime) / (1000 * 60 * 60) + "hrs";
46+
};
47+
48+
return (
49+
<PageWithSubMenu
50+
subMenu={getTeamSettingsMenu({ team, showPaymentUI, showUsageBasedUI })}
51+
title="Usage"
52+
subtitle="Manage team usage."
53+
>
54+
<div className="flex flex-col w-full">
55+
<div className="flex w-full mt-6 mb-6">
56+
<Property name="Last 30 days">Jun 1 - June 30</Property>
57+
<Property name="Workspaces">4,200 Min</Property>
58+
<Property name="Prebuilds">12,334 Min</Property>
59+
</div>
60+
</div>
61+
<ItemsList className="mt-2 text-gray-500">
62+
<Item header={false} className="grid grid-cols-6 bg-gray-100">
63+
<ItemField className="my-auto">
64+
<span>Type</span>
65+
</ItemField>
66+
<ItemField className="my-auto">
67+
<span>Class</span>
68+
</ItemField>
69+
<ItemField className="my-auto">
70+
<span>Amount</span>
71+
</ItemField>
72+
<ItemField className="my-auto">
73+
<span>Credits</span>
74+
</ItemField>
75+
<ItemField className="my-auto" />
76+
</Item>
77+
{billedUsage.map((usage) => (
78+
<div
79+
key={usage.instanceId}
80+
className="flex p-3 grid grid-cols-6 justify-between transition ease-in-out rounded-xl focus:bg-gitpod-kumquat-light"
81+
>
82+
<div className="my-auto">
83+
<span className={usage.workspaceType === "prebuild" ? "text-orange-400" : "text-green-500"}>
84+
{getType(usage.workspaceType)}
85+
</span>
86+
</div>
87+
<div className="my-auto">
88+
<span className="text-gray-400">{usage.workspaceClass}</span>
89+
</div>
90+
<div className="my-auto">
91+
<span className="text-gray-700">{getHours(usage.endTime, usage.startTime)}</span>
92+
</div>
93+
<div className="my-auto">
94+
<span className="text-gray-700">{usage.credits}</span>
95+
</div>
96+
<div className="my-auto">
97+
<span className="text-gray-400">
98+
{moment(new Date(usage.startTime).toDateString()).fromNow()}
99+
</span>
100+
</div>
101+
<div className="pr-2">
102+
<Arrow up={false} />
103+
</div>
104+
</div>
105+
))}
106+
</ItemsList>
107+
</PageWithSubMenu>
108+
);
109+
}
110+
111+
export default TeamUsage;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import { RemotePageMessage, RemoteTrackMessage, RemoteIdentifyMessage } from "./
6060
import { IDEServer } from "./ide-protocol";
6161
import { InstallationAdminSettings, TelemetryData } from "./installation-admin-protocol";
6262
import { Currency } from "./plans";
63+
import { BillableSession } from "./usage";
6364

6465
export interface GitpodClient {
6566
onInstanceUpdate(instance: WorkspaceInstance): void;
@@ -288,6 +289,8 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
288289
subscribeTeamToStripe(teamId: string, setupIntentId: string, currency: Currency): Promise<void>;
289290
getStripePortalUrlForTeam(teamId: string): Promise<string>;
290291

292+
getBilledUsage(attributionId: string): Promise<BillableSession[]>;
293+
291294
/**
292295
* Analytics
293296
*/
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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 { WorkspaceType } from "./protocol";
8+
9+
export interface BillableSession {
10+
// The id of the one paying the bill
11+
attributionId: string;
12+
13+
// Relevant for workspace type. When prebuild, shows "prebuild"
14+
userId?: string;
15+
teamId?: string;
16+
17+
instanceId: string;
18+
19+
workspaceId: string;
20+
21+
workspaceType: WorkspaceType;
22+
23+
// "standard" or "XL"
24+
workspaceClass: string;
25+
26+
// When the workspace started
27+
startTime: number;
28+
29+
// When the workspace ended
30+
endTime: number;
31+
32+
// The credits used for this session
33+
credits: number;
34+
35+
// TODO - maybe
36+
projectId?: string;
37+
}
38+
39+
export const billableSessionDummyData: BillableSession[] = [
40+
{
41+
attributionId: "some-attribution-id",
42+
userId: "prebuild",
43+
teamId: "prebuild",
44+
instanceId: "some-instance-id",
45+
workspaceId: "some-workspace-id",
46+
workspaceType: "prebuild",
47+
workspaceClass: "XL",
48+
startTime: Date.now() + -3 * 24 * 3600 * 1000, // 3 days ago
49+
endTime: Date.now(),
50+
credits: 320,
51+
projectId: "project-123",
52+
},
53+
{
54+
attributionId: "some-attribution-id2",
55+
userId: "some-user",
56+
teamId: "some-team",
57+
instanceId: "some-instance-id2",
58+
workspaceId: "some-workspace-id2",
59+
workspaceType: "regular",
60+
workspaceClass: "standard",
61+
startTime: Date.now() + -5 * 24 * 3600 * 1000,
62+
endTime: Date.now(),
63+
credits: 130,
64+
projectId: "project-123",
65+
},
66+
{
67+
attributionId: "some-attribution-id3",
68+
userId: "some-other-user",
69+
teamId: "some-other-team",
70+
instanceId: "some-instance-id3",
71+
workspaceId: "some-workspace-id3",
72+
workspaceType: "regular",
73+
workspaceClass: "XL",
74+
startTime: Date.now() + -5 * 24 * 3600 * 1000,
75+
endTime: Date.now() + -4 * 24 * 3600 * 1000,
76+
credits: 150,
77+
projectId: "project-134",
78+
},
79+
{
80+
attributionId: "some-attribution-id4",
81+
userId: "some-other-user2",
82+
teamId: "some-other-team2",
83+
instanceId: "some-instance-id4",
84+
workspaceId: "some-workspace-id4",
85+
workspaceType: "regular",
86+
workspaceClass: "standard",
87+
startTime: Date.now() + -10 * 24 * 3600 * 1000,
88+
endTime: Date.now() + -9 * 24 * 3600 * 1000,
89+
credits: 330,
90+
projectId: "project-137",
91+
},
92+
{
93+
attributionId: "some-attribution-id5",
94+
userId: "some-other-user3",
95+
teamId: "some-other-team3",
96+
instanceId: "some-instance-id5",
97+
workspaceId: "some-workspace-id5",
98+
workspaceType: "regular",
99+
workspaceClass: "XL",
100+
startTime: Date.now() + -2 * 24 * 3600 * 1000,
101+
endTime: Date.now(),
102+
credits: 222,
103+
projectId: "project-138",
104+
},
105+
{
106+
attributionId: "some-attribution-id6",
107+
userId: "some-other-user4",
108+
teamId: "some-other-team4",
109+
instanceId: "some-instance-id6",
110+
workspaceId: "some-workspace-id3",
111+
workspaceType: "regular",
112+
workspaceClass: "XL",
113+
startTime: Date.now() + -7 * 24 * 3600 * 1000,
114+
endTime: Date.now() + -6 * 24 * 3600 * 1000,
115+
credits: 300,
116+
projectId: "project-134",
117+
},
118+
{
119+
attributionId: "some-attribution-id8",
120+
userId: "some-other-user5",
121+
teamId: "some-other-team5",
122+
instanceId: "some-instance-id8",
123+
workspaceId: "some-workspace-id3",
124+
workspaceType: "regular",
125+
workspaceClass: "standard",
126+
startTime: Date.now() + -1 * 24 * 3600 * 1000,
127+
endTime: Date.now(),
128+
credits: 100,
129+
projectId: "project-567",
130+
},
131+
{
132+
attributionId: "some-attribution-id7",
133+
userId: "prebuild",
134+
teamId: "some-other-team7",
135+
instanceId: "some-instance-id7",
136+
workspaceId: "some-workspace-id7",
137+
workspaceType: "prebuild",
138+
workspaceClass: "XL",
139+
startTime: Date.now() + -1 * 24 * 3600 * 1000,
140+
endTime: Date.now(),
141+
credits: 200,
142+
projectId: "project-345",
143+
},
144+
];

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositor
7070
import { EligibilityService } from "../user/eligibility-service";
7171
import { AccountStatementProvider } from "../user/account-statement-provider";
7272
import { GithubUpgradeURL, PlanCoupon } from "@gitpod/gitpod-protocol/lib/payment-protocol";
73+
import { BillableSession, billableSessionDummyData } from "@gitpod/gitpod-protocol/lib/usage";
7374
import {
7475
AssigneeIdentityIdentifier,
7576
TeamSubscription,
@@ -2056,6 +2057,10 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
20562057
}
20572058
}
20582059

2060+
async getBilledUsage(ctx: TraceContext, attributionId: string): Promise<BillableSession[]> {
2061+
return billableSessionDummyData;
2062+
}
2063+
20592064
// (SaaS) – admin
20602065
async adminGetAccountStatement(ctx: TraceContext, userId: string): Promise<AccountStatement> {
20612066
traceAPIParams(ctx, { userId });

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig {
210210
findStripeSubscriptionIdForTeam: { group: "default", points: 1 },
211211
subscribeTeamToStripe: { group: "default", points: 1 },
212212
getStripePortalUrlForTeam: { group: "default", points: 1 },
213+
getBilledUsage: { group: "default", points: 1 },
213214
trackEvent: { group: "default", points: 1 },
214215
trackLocation: { group: "default", points: 1 },
215216
identifyUser: { group: "default", points: 1 },

0 commit comments

Comments
 (0)