Skip to content

Commit 5ce2124

Browse files
committed
[server][dashboard] Allow (new) Teams to buy and manage (legacy) Team Plans
1 parent 965ef3c commit 5ce2124

File tree

7 files changed

+134
-4
lines changed

7 files changed

+134
-4
lines changed

components/dashboard/src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const NewTeam = React.lazy(() => import(/* webpackPrefetch: true */ './teams/New
4141
const JoinTeam = React.lazy(() => import(/* webpackPrefetch: true */ './teams/JoinTeam'));
4242
const Members = React.lazy(() => import(/* webpackPrefetch: true */ './teams/Members'));
4343
const TeamSettings = React.lazy(() => import(/* webpackPrefetch: true */ './teams/TeamSettings'));
44+
const TeamPlans = React.lazy(() => import(/* webpackPrefetch: true */ './teams/TeamPlans'));
4445
const NewProject = React.lazy(() => import(/* webpackPrefetch: true */ './projects/NewProject'));
4546
const ConfigureProject = React.lazy(() => import(/* webpackPrefetch: true */ './projects/ConfigureProject'));
4647
const Projects = React.lazy(() => import(/* webpackPrefetch: true */ './projects/Projects'));
@@ -355,6 +356,9 @@ function App() {
355356
if (maybeProject === "settings") {
356357
return <TeamSettings />;
357358
}
359+
if (maybeProject === "plans") {
360+
return <TeamPlans />;
361+
}
358362
if (resourceOrPrebuild === "prebuilds") {
359363
return <Prebuilds />;
360364
}

components/dashboard/src/Menu.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default function Menu() {
4343
const match = useRouteMatch<{ segment1?: string, segment2?: string, segment3?: string }>("/(t/)?:segment1/:segment2?/:segment3?");
4444
const projectSlug = (() => {
4545
const resource = match?.params?.segment2;
46-
if (resource && ![/* team sub-pages */ "projects", "members", "settings", /* admin sub-pages */ "users", "workspaces"].includes(resource)) {
46+
if (resource && ![/* team sub-pages */ "projects", "members", "settings", "plans", /* admin sub-pages */ "users", "workspaces"].includes(resource)) {
4747
return resource;
4848
}
4949
})();
@@ -148,7 +148,7 @@ export default function Menu() {
148148
teamSettingsList.push({
149149
title: 'Settings',
150150
link: `/t/${team.slug}/settings`,
151-
alternatives: getTeamSettingsMenu(team).flatMap(e => e.link),
151+
alternatives: getTeamSettingsMenu({ team, showPaymentUI }).flatMap(e => e.link),
152152
})
153153
}
154154

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 { TeamMemberInfo } from "@gitpod/gitpod-protocol";
8+
import { Currency, Plans } from "@gitpod/gitpod-protocol/lib/plans";
9+
import { useContext, useEffect, useState } from "react";
10+
import { useLocation } from "react-router";
11+
import { PageWithSubMenu } from "../components/PageWithSubMenu";
12+
import SelectableCard from "../components/SelectableCard";
13+
import { PaymentContext } from "../payment-context";
14+
import { getGitpodService } from "../service/service";
15+
import { getCurrentTeam, TeamsContext } from "./teams-context";
16+
import { getTeamSettingsMenu } from "./TeamSettings";
17+
18+
export default function TeamPlans() {
19+
const { teams } = useContext(TeamsContext);
20+
const location = useLocation();
21+
const team = getCurrentTeam(location, teams);
22+
const [ members, setMembers ] = useState<TeamMemberInfo[]>([]);
23+
const { showPaymentUI, currency } = useContext(PaymentContext);
24+
25+
useEffect(() => {
26+
if (!team) {
27+
return;
28+
}
29+
(async () => {
30+
const infos = await getGitpodService().server.getTeamMembers(team.id);
31+
setMembers(infos);
32+
})();
33+
}, [ team ]);
34+
35+
const availableTeamPlans = Plans.getAvailableTeamPlans(currency || 'USD');
36+
37+
return <PageWithSubMenu subMenu={getTeamSettingsMenu({ team, showPaymentUI })} title="Plans" subtitle="Manage team plans and billing.">
38+
<button>Billing</button>
39+
<div className="mt-4 space-x-4 flex">
40+
<SelectableCard className="w-36 h-32" title="Free" selected={true} onClick={() => {}}>
41+
{members.length} x {Currency.getSymbol(currency || 'USD')}0 = {Currency.getSymbol(currency || 'USD')}0
42+
</SelectableCard>
43+
{availableTeamPlans.map(tp => <SelectableCard className="w-36 h-32" title={tp.name} selected={false} onClick={() => {}}>
44+
{members.length} x {Currency.getSymbol(tp.currency)}{tp.pricePerMonth} = {Currency.getSymbol(tp.currency)}{members.length * tp.pricePerMonth}
45+
</SelectableCard>)}
46+
</div>
47+
</PageWithSubMenu>;
48+
}

components/dashboard/src/teams/TeamSettings.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,24 @@ import { Redirect, useLocation } from "react-router";
1010
import CodeText from "../components/CodeText";
1111
import ConfirmationModal from "../components/ConfirmationModal";
1212
import { PageWithSubMenu } from "../components/PageWithSubMenu";
13+
import { PaymentContext } from "../payment-context";
1314
import { getGitpodService, gitpodHostUrl } from "../service/service";
1415
import { UserContext } from "../user-context";
1516
import { getCurrentTeam, TeamsContext } from "./teams-context";
1617

17-
export function getTeamSettingsMenu(team?: Team) {
18+
export function getTeamSettingsMenu(params: { team?: Team, showPaymentUI?: boolean }) {
19+
const { team, showPaymentUI } = params;
1820
return [
1921
{
2022
title: 'General',
2123
link: [`/t/${team?.slug}/settings`],
2224
},
25+
...(showPaymentUI ? [
26+
{
27+
title: 'Paid Plans',
28+
link: [`/t/${team?.slug}/plans`],
29+
},
30+
] : []),
2331
];
2432
}
2533

@@ -31,6 +39,7 @@ export default function TeamSettings() {
3139
const { user } = useContext(UserContext);
3240
const location = useLocation();
3341
const team = getCurrentTeam(location, teams);
42+
const { showPaymentUI } = useContext(PaymentContext);
3443

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

@@ -55,7 +64,7 @@ export default function TeamSettings() {
5564
};
5665

5766
return <>
58-
<PageWithSubMenu subMenu={getTeamSettingsMenu(team)} title="Settings" subtitle="Manage general team settings.">
67+
<PageWithSubMenu subMenu={getTeamSettingsMenu({ team, showPaymentUI })} title="Settings" subtitle="Manage general team settings.">
5968
<h3>Delete Team</h3>
6069
<p className="text-base text-gray-500 pb-4 max-w-2xl">Deleting this team will also remove all associated data with this team, including projects and workspaces. Deleted teams cannot be restored!</p>
6170
<button className="danger secondary" onClick={() => setModal(true)}>Delete Team</button>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3+
* Licensed under the Gitpod Enterprise Source Code License,
4+
* See License.enterprise.txt in the project root folder.
5+
*/
6+
7+
import { Entity, Column, PrimaryColumn, Index } from "typeorm";
8+
9+
import { TeamSubscription2 } from "@gitpod/gitpod-protocol/lib/team-subscription-protocol";
10+
11+
import { TypeORM } from "../../typeorm/typeorm";
12+
import { Transformer } from "../../typeorm/transformer";
13+
14+
@Entity()
15+
@Index("ind_team_paymentReference", ["teamId", "paymentReference"])
16+
@Index("ind_team_startdate", ["teamId", "startDate"])
17+
// on DB but not Typeorm: @Index("ind_lastModified", ["_lastModified"]) // DBSync
18+
export class DBTeamSubscription2 implements TeamSubscription2 {
19+
20+
@PrimaryColumn("uuid")
21+
id: string;
22+
23+
@Column(TypeORM.UUID_COLUMN_TYPE)
24+
teamId: string;
25+
26+
@Column()
27+
paymentReference: string;
28+
29+
@Column()
30+
startDate: string;
31+
32+
@Column({
33+
default: '',
34+
transformer: Transformer.MAP_EMPTY_STR_TO_UNDEFINED
35+
})
36+
endDate?: string;
37+
38+
@Column()
39+
planId: string;
40+
41+
@Column('int')
42+
quantity: number;
43+
44+
@Column({
45+
default: '',
46+
transformer: Transformer.MAP_EMPTY_STR_TO_UNDEFINED
47+
})
48+
cancellationDate?: string;
49+
50+
// This column triggers the db-sync deletion mechanism. It's not intended for public consumption.
51+
@Column()
52+
deleted: boolean;
53+
}

components/gitpod-db/src/typeorm/team-subscription-db-impl.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { TeamSubscription, TeamSubscriptionSlot } from "@gitpod/gitpod-protocol/
1111

1212
import { TeamSubscriptionDB } from "../team-subscription-db";
1313
import { DBTeamSubscription } from "./entity/db-team-subscription";
14+
import { DBTeamSubscription2 } from "./entity/db-team-subscription-2";
1415
import { DBTeamSubscriptionSlot } from "./entity/db-team-subscription-slot";
1516
import { TypeORM } from "./typeorm";
1617

@@ -33,6 +34,10 @@ export class TeamSubscriptionDBImpl implements TeamSubscriptionDB {
3334
return (await this.getEntityManager()).getRepository(DBTeamSubscription);
3435
}
3536

37+
protected async getRepo2(): Promise<Repository<DBTeamSubscription2>> {
38+
return (await this.getEntityManager()).getRepository(DBTeamSubscription2);
39+
}
40+
3641
protected async getSlotsRepo(): Promise<Repository<DBTeamSubscriptionSlot>> {
3742
return (await this.getEntityManager()).getRepository(DBTeamSubscriptionSlot);
3843
}

components/gitpod-protocol/src/team-subscription-protocol.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ export interface TeamSubscription {
2020
deleted?: boolean;
2121
}
2222

23+
export interface TeamSubscription2 {
24+
id: string;
25+
teamId: string;
26+
planId: string;
27+
startDate: string;
28+
endDate?: string;
29+
/** The Chargebee subscription id */
30+
paymentReference: string;
31+
cancellationDate?: string;
32+
}
33+
2334
export namespace TeamSubscription {
2435
export const create = (ts: Omit<TeamSubscription, 'id'>): TeamSubscription => {
2536
const withId = ts as TeamSubscription;

0 commit comments

Comments
 (0)