Skip to content

Commit

Permalink
Implement Usage view
Browse files Browse the repository at this point in the history
  • Loading branch information
laushinka committed Jul 11, 2022
1 parent afc82bc commit 351754a
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 7 deletions.
4 changes: 4 additions & 0 deletions components/dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const JoinTeam = React.lazy(() => import(/* webpackPrefetch: true */ "./teams/Jo
const Members = React.lazy(() => import(/* webpackPrefetch: true */ "./teams/Members"));
const TeamSettings = React.lazy(() => import(/* webpackPrefetch: true */ "./teams/TeamSettings"));
const TeamBilling = React.lazy(() => import(/* webpackPrefetch: true */ "./teams/TeamBilling"));
const TeamUsage = React.lazy(() => import(/* webpackPrefetch: true */ "./teams/TeamUsage"));
const NewProject = React.lazy(() => import(/* webpackPrefetch: true */ "./projects/NewProject"));
const ConfigureProject = React.lazy(() => import(/* webpackPrefetch: true */ "./projects/ConfigureProject"));
const Projects = React.lazy(() => import(/* webpackPrefetch: true */ "./projects/Projects"));
Expand Down Expand Up @@ -440,6 +441,9 @@ function App() {
if (maybeProject === "billing") {
return <TeamBilling />;
}
if (maybeProject === "usage") {
return <TeamUsage />;
}
if (resourceOrPrebuild === "prebuilds") {
return <Prebuilds />;
}
Expand Down
3 changes: 2 additions & 1 deletion components/dashboard/src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export default function Menu() {
"members",
"settings",
"billing",
"usage",
// admin sub-pages
"users",
"workspaces",
Expand Down Expand Up @@ -220,7 +221,7 @@ export default function Menu() {
teamSettingsList.push({
title: "Settings",
link: `/t/${team.slug}/settings`,
alternatives: getTeamSettingsMenu({ team, showPaymentUI }).flatMap((e) => e.link),
alternatives: getTeamSettingsMenu({ team, showPaymentUI, showUsageBasedUI }).flatMap((e) => e.link),
});
}

Expand Down
4 changes: 2 additions & 2 deletions components/dashboard/src/teams/TeamBilling.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function TeamBilling() {
const team = getCurrentTeam(location, teams);
const [members, setMembers] = useState<TeamMemberInfo[]>([]);
const [teamSubscription, setTeamSubscription] = useState<TeamSubscription2 | undefined>();
const { showPaymentUI, currency, setCurrency } = useContext(PaymentContext);
const { showPaymentUI, showUsageBasedUI, currency, setCurrency } = useContext(PaymentContext);
const [pendingTeamPlan, setPendingTeamPlan] = useState<PendingPlan | undefined>();
const [pollTeamSubscriptionTimeout, setPollTeamSubscriptionTimeout] = useState<NodeJS.Timeout | undefined>();

Expand Down Expand Up @@ -140,7 +140,7 @@ export default function TeamBilling() {

return (
<PageWithSubMenu
subMenu={getTeamSettingsMenu({ team, showPaymentUI })}
subMenu={getTeamSettingsMenu({ team, showPaymentUI, showUsageBasedUI })}
title="Billing"
subtitle="Manage team billing and plans."
>
Expand Down
16 changes: 12 additions & 4 deletions components/dashboard/src/teams/TeamSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import { getGitpodService, gitpodHostUrl } from "../service/service";
import { UserContext } from "../user-context";
import { getCurrentTeam, TeamsContext } from "./teams-context";

export function getTeamSettingsMenu(params: { team?: Team; showPaymentUI?: boolean }) {
const { team, showPaymentUI } = params;
export function getTeamSettingsMenu(params: { team?: Team; showPaymentUI?: boolean; showUsageBasedUI?: boolean }) {
const { team, showPaymentUI, showUsageBasedUI } = params;
return [
{
title: "General",
Expand All @@ -30,6 +30,14 @@ export function getTeamSettingsMenu(params: { team?: Team; showPaymentUI?: boole
},
]
: []),
...(showUsageBasedUI
? [
{
title: "Usage",
link: [`/t/${team?.slug}/usage`],
},
]
: []),
];
}

Expand All @@ -41,7 +49,7 @@ export default function TeamSettings() {
const { user } = useContext(UserContext);
const location = useLocation();
const team = getCurrentTeam(location, teams);
const { showPaymentUI } = useContext(PaymentContext);
const { showPaymentUI, showUsageBasedUI } = useContext(PaymentContext);

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

Expand All @@ -68,7 +76,7 @@ export default function TeamSettings() {
return (
<>
<PageWithSubMenu
subMenu={getTeamSettingsMenu({ team, showPaymentUI })}
subMenu={getTeamSettingsMenu({ team, showPaymentUI, showUsageBasedUI })}
title="Settings"
subtitle="Manage general team settings."
>
Expand Down
116 changes: 116 additions & 0 deletions components/dashboard/src/teams/TeamUsage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/

import { useContext, useEffect, useState } from "react";
import { Redirect, useLocation } from "react-router";
import { PageWithSubMenu } from "../components/PageWithSubMenu";
import { getCurrentTeam, TeamsContext } from "./teams-context";
import { getTeamSettingsMenu } from "./TeamSettings";
import { PaymentContext } from "../payment-context";
import { getGitpodService } from "../service/service";
import { BillableSession } from "@gitpod/gitpod-protocol/lib/usage";
import { Item, ItemField, ItemsList } from "../components/ItemsList";
import moment from "moment";
import Property from "../admin/Property";
import Arrow from "../components/Arrow";
import { WorkspaceType } from "@gitpod/gitpod-protocol/lib/protocol";

function TeamUsage() {
const { teams } = useContext(TeamsContext);
const { showPaymentUI, showUsageBasedUI } = useContext(PaymentContext);
const location = useLocation();
const team = getCurrentTeam(location, teams);
const [billedUsage, setBilledUsage] = useState<BillableSession[]>([]);

useEffect(() => {
if (!team) {
return;
}
(async () => {
const billedUsageResult = await getGitpodService().server.getBilledUsage("some-attribution-id");
setBilledUsage(billedUsageResult);
})();
}, [team]);

if (!showUsageBasedUI) {
return <Redirect to="/" />;
}

const getType = (type: Omit<WorkspaceType, "probe">) => {
if (type === "regular") {
return "Workspace";
}
return "Prebuild";
};

const getHours = (endTime: number, startTime: number) => {
return (endTime - startTime) / (1000 * 60 * 60) + "hrs";
};

return (
<PageWithSubMenu
subMenu={getTeamSettingsMenu({ team, showPaymentUI, showUsageBasedUI })}
title="Usage"
subtitle="Manage team usage."
>
<div className="flex flex-col w-full">
<div className="flex w-full mt-6 mb-6">
<Property name="Last 30 days">Jun 1 - June 30</Property>
<Property name="Workspaces">4,200 Min</Property>
<Property name="Prebuilds">12,334 Min</Property>
</div>
</div>
<ItemsList className="mt-2 text-gray-500">
<Item header={false} className="grid grid-cols-6 bg-gray-100">
<ItemField className="my-auto">
<span>Type</span>
</ItemField>
<ItemField className="my-auto">
<span>Class</span>
</ItemField>
<ItemField className="my-auto">
<span>Amount</span>
</ItemField>
<ItemField className="my-auto">
<span>Credits</span>
</ItemField>
<ItemField className="my-auto" />
</Item>
{billedUsage.map((usage) => (
<div
key={usage.instanceId}
className="flex p-3 grid grid-cols-6 justify-between transition ease-in-out rounded-xl focus:bg-gitpod-kumquat-light"
>
<div className="my-auto">
<span className={usage.workspaceType === "prebuild" ? "text-orange-400" : "text-green-500"}>
{getType(usage.workspaceType)}
</span>
</div>
<div className="my-auto">
<span className="text-gray-400">{usage.workspaceClass}</span>
</div>
<div className="my-auto">
<span className="text-gray-700">{getHours(usage.endTime, usage.startTime)}</span>
</div>
<div className="my-auto">
<span className="text-gray-700">{usage.credits}</span>
</div>
<div className="my-auto">
<span className="text-gray-400">
{moment(new Date(usage.startTime).toDateString()).fromNow()}
</span>
</div>
<div className="pr-2">
<Arrow up={false} />
</div>
</div>
))}
</ItemsList>
</PageWithSubMenu>
);
}

export default TeamUsage;
3 changes: 3 additions & 0 deletions components/gitpod-protocol/src/gitpod-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { RemotePageMessage, RemoteTrackMessage, RemoteIdentifyMessage } from "./
import { IDEServer } from "./ide-protocol";
import { InstallationAdminSettings, TelemetryData } from "./installation-admin-protocol";
import { Currency } from "./plans";
import { BillableSession } from "./usage";

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

getBilledUsage(attributionId: string): Promise<BillableSession[]>;

/**
* Analytics
*/
Expand Down
144 changes: 144 additions & 0 deletions components/gitpod-protocol/src/usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/

import { WorkspaceType } from "./protocol";

export interface BillableSession {
// The id of the one paying the bill
attributionId: string;

// Relevant for workspace type. When prebuild, shows "prebuild"
userId?: string;
teamId?: string;

instanceId: string;

workspaceId: string;

workspaceType: Omit<WorkspaceType, "probe">;

// "standard" or "XL"
workspaceClass: string;

// When the workspace started
startTime: number;

// When the workspace ended
endTime: number;

// The credits used for this session
credits: number;

// TODO - maybe
projectId?: string;
}

export const billableSessionDummyData: BillableSession[] = [
{
attributionId: "some-attribution-id",
userId: "prebuild",
teamId: "prebuild",
instanceId: "some-instance-id",
workspaceId: "some-workspace-id",
workspaceType: "prebuild",
workspaceClass: "XL",
startTime: Date.now() + -3 * 24 * 3600 * 1000, // 3 days ago
endTime: Date.now(),
credits: 320,
projectId: "project-123",
},
{
attributionId: "some-attribution-id2",
userId: "some-user",
teamId: "some-team",
instanceId: "some-instance-id2",
workspaceId: "some-workspace-id2",
workspaceType: "regular",
workspaceClass: "standard",
startTime: Date.now() + -5 * 24 * 3600 * 1000,
endTime: Date.now(),
credits: 130,
projectId: "project-123",
},
{
attributionId: "some-attribution-id3",
userId: "some-other-user",
teamId: "some-other-team",
instanceId: "some-instance-id3",
workspaceId: "some-workspace-id3",
workspaceType: "regular",
workspaceClass: "XL",
startTime: Date.now() + -5 * 24 * 3600 * 1000,
endTime: Date.now() + -4 * 24 * 3600 * 1000,
credits: 150,
projectId: "project-134",
},
{
attributionId: "some-attribution-id4",
userId: "some-other-user2",
teamId: "some-other-team2",
instanceId: "some-instance-id4",
workspaceId: "some-workspace-id4",
workspaceType: "regular",
workspaceClass: "standard",
startTime: Date.now() + -10 * 24 * 3600 * 1000,
endTime: Date.now() + -9 * 24 * 3600 * 1000,
credits: 330,
projectId: "project-137",
},
{
attributionId: "some-attribution-id5",
userId: "some-other-user3",
teamId: "some-other-team3",
instanceId: "some-instance-id5",
workspaceId: "some-workspace-id5",
workspaceType: "regular",
workspaceClass: "XL",
startTime: Date.now() + -2 * 24 * 3600 * 1000,
endTime: Date.now(),
credits: 222,
projectId: "project-138",
},
{
attributionId: "some-attribution-id6",
userId: "some-other-user4",
teamId: "some-other-team4",
instanceId: "some-instance-id6",
workspaceId: "some-workspace-id3",
workspaceType: "regular",
workspaceClass: "XL",
startTime: Date.now() + -7 * 24 * 3600 * 1000,
endTime: Date.now() + -6 * 24 * 3600 * 1000,
credits: 300,
projectId: "project-134",
},
{
attributionId: "some-attribution-id8",
userId: "some-other-user5",
teamId: "some-other-team5",
instanceId: "some-instance-id8",
workspaceId: "some-workspace-id3",
workspaceType: "regular",
workspaceClass: "standard",
startTime: Date.now() + -1 * 24 * 3600 * 1000,
endTime: Date.now(),
credits: 100,
projectId: "project-567",
},
{
attributionId: "some-attribution-id7",
userId: "prebuild",
teamId: "some-other-team7",
instanceId: "some-instance-id7",
workspaceId: "some-workspace-id7",
workspaceType: "prebuild",
workspaceClass: "XL",
startTime: Date.now() + -1 * 24 * 3600 * 1000,
endTime: Date.now(),
credits: 200,
projectId: "project-345",
},
];
5 changes: 5 additions & 0 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositor
import { EligibilityService } from "../user/eligibility-service";
import { AccountStatementProvider } from "../user/account-statement-provider";
import { GithubUpgradeURL, PlanCoupon } from "@gitpod/gitpod-protocol/lib/payment-protocol";
import { BillableSession, billableSessionDummyData } from "@gitpod/gitpod-protocol/lib/usage";
import {
AssigneeIdentityIdentifier,
TeamSubscription,
Expand Down Expand Up @@ -2056,6 +2057,10 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
}
}

async getBilledUsage(ctx: TraceContext, attributionId: string): Promise<BillableSession[]> {
return billableSessionDummyData;
}

// (SaaS) – admin
async adminGetAccountStatement(ctx: TraceContext, userId: string): Promise<AccountStatement> {
traceAPIParams(ctx, { userId });
Expand Down
Loading

0 comments on commit 351754a

Please sign in to comment.