mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
633 lines
21 KiB
TypeScript
633 lines
21 KiB
TypeScript
/**
|
|
* Copyright (c) 2021 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.
|
|
*/
|
|
|
|
export type Currency = 'USD' | 'EUR';
|
|
export namespace Currency {
|
|
export const getAll = (): Currency[] => {
|
|
return ['USD', 'EUR'];
|
|
}
|
|
export const getSymbol = (c: Currency) => {
|
|
return c === 'USD' ? '$' : '€';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Different plans of the same type MAY have different prices ($/€, for example) but MUST have the same feature set.
|
|
*/
|
|
export type PlanType = 'free' | 'free-50' | 'free-open-source' | 'student' | 'basic' | 'personal' | 'professional' | 'professional-new';
|
|
export type HoursPerMonthType = number | 'unlimited';
|
|
export interface Plan {
|
|
chargebeeId: string
|
|
githubId?: number
|
|
githubPlanNumber?: number
|
|
|
|
name: string
|
|
currency: Currency
|
|
/** In full currencies (Euro, US Dollar, ...) */
|
|
pricePerMonth: number
|
|
hoursPerMonth: HoursPerMonthType
|
|
type: PlanType
|
|
team?: boolean
|
|
}
|
|
export namespace Plan {
|
|
export const is = (o: any): o is Plan => {
|
|
return 'chargebeeId' in o
|
|
&& 'name' in o
|
|
&& 'currency' in o
|
|
&& 'pricePerMonth' in o
|
|
&& 'hoursPerMonth' in o
|
|
&& 'type' in o;
|
|
}
|
|
}
|
|
|
|
export const MAX_PARALLEL_WORKSPACES = 16;
|
|
|
|
export interface Coupon {
|
|
id: string;
|
|
isGithubStudentCoupon?: boolean;
|
|
}
|
|
export namespace Coupon {
|
|
export const is = (o: any): o is Coupon => {
|
|
return 'id' in o;
|
|
}
|
|
}
|
|
export namespace Coupons {
|
|
export const INTERNAL_GITPOD_GHSP: Coupon = {
|
|
id: "INTERNAL_GITPOD_GHSP",
|
|
isGithubStudentCoupon: true
|
|
};
|
|
export const INTERNAL_GITPOD_GHSP_2: Coupon = {
|
|
id: "INTERNAL_GITPOD_GHSP_2",
|
|
isGithubStudentCoupon: true
|
|
};
|
|
export const GITHUBSTUDENTPACKFORFACULTY: Coupon = {
|
|
id: "GITHUBSTUDENTPACKFORFACULTY",
|
|
isGithubStudentCoupon: true
|
|
};
|
|
export const isGithubStudentCoupon = (id: string): boolean | undefined => {
|
|
const c = getAllCoupons().find(ic => ic.id === id);
|
|
if (!c) {
|
|
return undefined;
|
|
}
|
|
return !!c.isGithubStudentCoupon;
|
|
};
|
|
export const getAllCoupons = (): Coupon[] => {
|
|
return Object.keys(Coupons)
|
|
.map(k => (Coupons as any)[k])
|
|
.filter(a => typeof a === 'object' && Coupon.is(a));
|
|
};
|
|
}
|
|
|
|
// Theoretical maximum of workspace hours: 16 workspaces for 24h a day for 31 days as permitted by the v3 unlimited plan
|
|
// Other unlimited hour plans are restricted by the number of Parallel Workspaces they can start.
|
|
export const ABSOLUTE_MAX_USAGE = MAX_PARALLEL_WORKSPACES * 24 * 31;
|
|
|
|
/**
|
|
* Version history:
|
|
* - v1:
|
|
* - Free
|
|
* - Basic
|
|
* - Professional
|
|
* - Team Professional
|
|
* - v2:
|
|
* - Free
|
|
* - Personal
|
|
* - Unlimited: rebranded professional with unlimited hours
|
|
* - Team Unlimited: rebranded professional with unlimited hours
|
|
* - dropped: Basic
|
|
* - v2.5:
|
|
* + Student Unlimited
|
|
* + Team Unlimited Student
|
|
* - V3:
|
|
* - Free: reduced to 50h (stays default, but not advertised directly anymore)
|
|
* - Personal (8/9)
|
|
* - Professional (23/25)
|
|
* - Unlimited (35/39)
|
|
* - v4:
|
|
* - Professional Open Source (free)
|
|
* - v5:
|
|
* - Unleashed: rebranded Unlimited
|
|
*/
|
|
export namespace Plans {
|
|
/**
|
|
* The old default plan (v1): 100h hours for public repos
|
|
*/
|
|
export const FREE: Plan = {
|
|
chargebeeId: 'free',
|
|
githubId: 2034,
|
|
githubPlanNumber: 1,
|
|
|
|
type: 'free',
|
|
name: 'Open Source',
|
|
currency: 'USD',
|
|
pricePerMonth: 0,
|
|
hoursPerMonth: 100
|
|
};
|
|
|
|
/**
|
|
* The new default plan (v3): 50h hours for public repos
|
|
*/
|
|
export const FREE_50: Plan = {
|
|
chargebeeId: 'free-50',
|
|
githubId: 4902,
|
|
githubPlanNumber: 5,
|
|
|
|
type: 'free-50',
|
|
name: 'Open Source',
|
|
currency: 'USD',
|
|
pricePerMonth: 0,
|
|
hoursPerMonth: 50
|
|
};
|
|
|
|
/**
|
|
* Users created after this date get the FREE_50 plan (v3) instead of the (old) FREE plan (v1)
|
|
*/
|
|
export const FREE_50_START_DATE = '2019-12-19T00:00:00.000Z';
|
|
|
|
/**
|
|
* The 'Professional Open Source' plan was introduced to offer professional open-souce developers unlimited hours.
|
|
*/
|
|
export const FREE_OPEN_SOURCE: Plan = {
|
|
chargebeeId: 'free-open-source',
|
|
type: 'free-open-source',
|
|
name: 'Professional Open Source',
|
|
currency: 'USD',
|
|
pricePerMonth: 0,
|
|
hoursPerMonth: 'unlimited'
|
|
};
|
|
|
|
/**
|
|
* The 'Student Unleashed' plans were introduced to give students access to the highly-priced unlimited plans.
|
|
*/
|
|
export const PROFESSIONAL_STUDENT_EUR: Plan = {
|
|
chargebeeId: 'professional-student-eur',
|
|
type: 'student',
|
|
name: 'Student Unleashed',
|
|
currency: 'EUR',
|
|
pricePerMonth: 8,
|
|
hoursPerMonth: 'unlimited'
|
|
};
|
|
|
|
/**
|
|
* The 'Student Unleashed' plans were introduced to give students access to the highly-priced unlimited plans.
|
|
*/
|
|
export const PROFESSIONAL_STUDENT_USD: Plan = {
|
|
chargebeeId: 'professional-student-usd',
|
|
type: 'student',
|
|
name: 'Student Unleashed',
|
|
currency: 'USD',
|
|
pricePerMonth: 9,
|
|
hoursPerMonth: 'unlimited'
|
|
};
|
|
|
|
/**
|
|
* The 'Student Unleashed' plans were introduced to give students access to the highly-priced unlimited plans.
|
|
*/
|
|
export const TEAM_PROFESSIONAL_STUDENT_EUR: Plan = {
|
|
chargebeeId: 'team-professional-student-eur',
|
|
type: 'student',
|
|
name: 'Team Student Unleashed',
|
|
team: true,
|
|
currency: 'EUR',
|
|
pricePerMonth: 8,
|
|
hoursPerMonth: 'unlimited'
|
|
};
|
|
|
|
/**
|
|
* The 'Student Unleashed' plans were introduced to give students access to the highly-priced unlimited plans.
|
|
*/
|
|
export const TEAM_PROFESSIONAL_STUDENT_USD: Plan = {
|
|
chargebeeId: 'team-professional-student-usd',
|
|
type: 'student',
|
|
name: 'Team Student Unleashed',
|
|
team: true,
|
|
currency: 'USD',
|
|
pricePerMonth: 9,
|
|
hoursPerMonth: 'unlimited'
|
|
};
|
|
|
|
/**
|
|
* The 'basic' plan was the original differentiator between FREE and Professional (v1) but got discarded soon.
|
|
*/
|
|
export const BASIC_EUR: Plan = {
|
|
chargebeeId: 'basic-eur',
|
|
type: 'basic',
|
|
name: 'Standard',
|
|
currency: 'EUR',
|
|
pricePerMonth: 17,
|
|
hoursPerMonth: 100
|
|
};
|
|
|
|
/**
|
|
* The 'basic' plan was the original differentiator between FREE and Professional (v1) but got discarded soon.
|
|
*/
|
|
export const BASIC_USD: Plan = {
|
|
chargebeeId: 'basic-usd',
|
|
githubId: 2035,
|
|
githubPlanNumber: 2,
|
|
|
|
type: 'basic',
|
|
name: 'Standard',
|
|
currency: 'USD',
|
|
pricePerMonth: 19,
|
|
hoursPerMonth: 100
|
|
};
|
|
|
|
/**
|
|
* The 'personal' plan was introduced to superseed the 'basic' plan (introduced with v2) to be more attractive to hobbyists.
|
|
*/
|
|
export const PERSONAL_EUR: Plan = {
|
|
chargebeeId: 'personal-eur',
|
|
type: 'personal',
|
|
name: 'Personal',
|
|
currency: 'EUR',
|
|
pricePerMonth: 8,
|
|
hoursPerMonth: 100
|
|
};
|
|
|
|
/**
|
|
* The 'personal' plan was introduced to superseed the 'basic' plan (introduced with v2) to be more attractive to hobbyists.
|
|
*/
|
|
export const PERSONAL_USD: Plan = {
|
|
chargebeeId: 'personal-usd',
|
|
githubId: 2274,
|
|
githubPlanNumber: 4,
|
|
|
|
type: 'personal',
|
|
name: 'Personal',
|
|
currency: 'USD',
|
|
pricePerMonth: 9,
|
|
hoursPerMonth: 100
|
|
};
|
|
|
|
/**
|
|
* This is the 'new' Professional plan (v3), which is meant to fit well between Personal (9$/8€) on the left and
|
|
* Unleashed (39$/35€) on the right.
|
|
*/
|
|
export const PROFESSIONAL_NEW_EUR: Plan = {
|
|
chargebeeId: 'professional-new-eur',
|
|
type: 'professional-new',
|
|
name: 'Professional',
|
|
currency: 'EUR',
|
|
pricePerMonth: 23,
|
|
hoursPerMonth: 'unlimited'
|
|
};
|
|
|
|
/**
|
|
* This is the 'new' Professional plan (v3), which is meant to fit well between Personal (9$/8€) on the left and
|
|
* Unleashed (39$/35€) on the right.
|
|
*/
|
|
export const PROFESSIONAL_NEW_USD: Plan = {
|
|
chargebeeId: 'professional-new-usd',
|
|
type: 'professional-new',
|
|
name: 'Professional',
|
|
currency: 'USD',
|
|
pricePerMonth: 25,
|
|
hoursPerMonth: 'unlimited'
|
|
};
|
|
|
|
/**
|
|
* This is the 'new' Team Professional plan (v3), which is meant to fit well between Personal (9$/8€) on the left and
|
|
* Unleashed (39$/35€) on the right.
|
|
*/
|
|
export const TEAM_PROFESSIONAL_NEW_EUR: Plan = {
|
|
chargebeeId: 'team-professional-new-eur',
|
|
type: 'professional-new',
|
|
name: 'Team Professional',
|
|
currency: 'EUR',
|
|
team: true,
|
|
pricePerMonth: 23,
|
|
hoursPerMonth: 'unlimited'
|
|
};
|
|
|
|
/**
|
|
* This is the 'new' Team Professional plan (v3), which is meant to fit well between Personal (9$/8€) on the left and
|
|
* Unleashed (39$/35€) on the right.
|
|
*/
|
|
export const TEAM_PROFESSIONAL_NEW_USD: Plan = {
|
|
chargebeeId: 'team-professional-new-usd',
|
|
type: 'professional-new',
|
|
name: 'Team Professional',
|
|
currency: 'USD',
|
|
team: true,
|
|
pricePerMonth: 25,
|
|
hoursPerMonth: 'unlimited'
|
|
};
|
|
|
|
/**
|
|
* This is the 'Unleashed' plan (v1, rebranded v2, v5)
|
|
* It was originally introduced as 'Professional', and we cannot update the ids, so it stays that way in the code.
|
|
*/
|
|
export const PROFESSIONAL_EUR: Plan = {
|
|
chargebeeId: 'professional-eur',
|
|
type: 'professional',
|
|
name: 'Unleashed',
|
|
currency: 'EUR',
|
|
pricePerMonth: 35,
|
|
hoursPerMonth: 'unlimited'
|
|
};
|
|
|
|
/**
|
|
* This is the 'Unleashed' plan (v1, rebranded v2, v5)
|
|
* It was originally introduced as 'Professional', and we cannot update the ids, so it stays that way in the code.
|
|
*/
|
|
export const PROFESSIONAL_USD: Plan = {
|
|
chargebeeId: 'professional-usd',
|
|
githubId: 2036,
|
|
githubPlanNumber: 3,
|
|
|
|
type: 'professional',
|
|
name: 'Unleashed',
|
|
currency: 'USD',
|
|
pricePerMonth: 39,
|
|
hoursPerMonth: 'unlimited'
|
|
};
|
|
|
|
/**
|
|
* This is the Team-'Unleashed' plan (v1, rebranded v2, v5)
|
|
* It was originally introduced as 'Professional', and we cannot update the ids, so it stays that way in the code.
|
|
*/
|
|
export const TEAM_PROFESSIONAL_USD: Plan = {
|
|
chargebeeId: 'team-professional-usd',
|
|
type: 'professional',
|
|
name: 'Team Unleashed',
|
|
currency: 'USD',
|
|
team: true,
|
|
pricePerMonth: 39,
|
|
hoursPerMonth: 'unlimited'
|
|
};
|
|
|
|
/**
|
|
* This is the Team-'Unleashed' plan (v1, rebranded v2, v5)
|
|
* It was originally introduced as 'Professional', and we cannot update the ids, so it stays that way in the code.
|
|
*/
|
|
export const TEAM_PROFESSIONAL_EUR: Plan = {
|
|
chargebeeId: 'team-professional-eur',
|
|
type: 'professional',
|
|
name: 'Team Unleashed',
|
|
currency: 'EUR',
|
|
team: true,
|
|
pricePerMonth: 35,
|
|
hoursPerMonth: 'unlimited'
|
|
};
|
|
|
|
const getAllPlans = (): Plan[] => {
|
|
return Object.keys(Plans)
|
|
.map(k => (Plans as any)[k])
|
|
.filter(a => typeof a === 'object' && Plan.is(a));
|
|
};
|
|
|
|
|
|
/**
|
|
* This function returns all individual plans that might be active (= we have subscriptions for) at the moment
|
|
*/
|
|
export function getAvailablePlans(currency: Currency): Plan[] {
|
|
const availablePaidPlans = [
|
|
Plans.BASIC_EUR,
|
|
Plans.BASIC_USD,
|
|
Plans.PERSONAL_EUR,
|
|
Plans.PERSONAL_USD,
|
|
Plans.PROFESSIONAL_NEW_EUR,
|
|
Plans.PROFESSIONAL_NEW_USD,
|
|
Plans.PROFESSIONAL_EUR,
|
|
Plans.PROFESSIONAL_USD
|
|
];
|
|
return [
|
|
Plans.FREE,
|
|
Plans.FREE_50,
|
|
Plans.FREE_OPEN_SOURCE,
|
|
...availablePaidPlans.filter(p => p.currency)
|
|
]
|
|
};
|
|
|
|
export const getAvailableTeamPlans = (currency?: Currency): Plan[] => {
|
|
const teamPlans = getAllPlans().filter(p => !!p.team);
|
|
return currency ? teamPlans.filter(p => p.currency === currency) : teamPlans;
|
|
};
|
|
|
|
export function getById(id: string | undefined): Plan | undefined {
|
|
if (id === undefined) {
|
|
return undefined;
|
|
}
|
|
return getAllPlans()
|
|
.find(p => p.chargebeeId === id) || undefined;
|
|
};
|
|
|
|
export function getByTypeAndCurrency(type: PlanType, currency: Currency): Plan | undefined {
|
|
return getAllPlans()
|
|
.filter(p => p.type)
|
|
.find(p => p.currency === currency);
|
|
}
|
|
|
|
export function getProPlan(currency: Currency): Plan {
|
|
switch (currency) {
|
|
case "EUR": return Plans.PROFESSIONAL_EUR;
|
|
case "USD": return Plans.PROFESSIONAL_USD;
|
|
}
|
|
}
|
|
|
|
export function getNewProPlan(currency: Currency): Plan {
|
|
switch (currency) {
|
|
case "EUR": return Plans.PROFESSIONAL_NEW_EUR;
|
|
case "USD": return Plans.PROFESSIONAL_NEW_USD;
|
|
}
|
|
}
|
|
|
|
export function getStudentProPlan(currency: Currency): Plan {
|
|
switch (currency) {
|
|
case "EUR": return Plans.PROFESSIONAL_STUDENT_EUR;
|
|
case "USD": return Plans.PROFESSIONAL_STUDENT_USD;
|
|
}
|
|
}
|
|
|
|
export function getBasicPlan(currency: Currency): Plan {
|
|
switch (currency) {
|
|
case "EUR": return Plans.BASIC_EUR;
|
|
case "USD": return Plans.BASIC_USD;
|
|
}
|
|
}
|
|
|
|
export function getPersonalPlan(currency: Currency): Plan {
|
|
switch (currency) {
|
|
case "EUR": return Plans.PERSONAL_EUR;
|
|
case "USD": return Plans.PERSONAL_USD;
|
|
}
|
|
}
|
|
|
|
export function getFreePlan(userCreationDate: string): Plan {
|
|
return userCreationDate < Plans.FREE_50_START_DATE ? Plans.FREE : Plans.FREE_50;
|
|
}
|
|
|
|
export function isFreePlan(chargebeeId: string | undefined): boolean {
|
|
return chargebeeId === Plans.FREE.chargebeeId
|
|
|| chargebeeId === Plans.FREE_50.chargebeeId
|
|
|| chargebeeId === Plans.FREE_OPEN_SOURCE.chargebeeId;
|
|
}
|
|
|
|
export function isFreeNonTransientPlan(chargebeeId: string | undefined): boolean {
|
|
return chargebeeId === Plans.FREE_OPEN_SOURCE.chargebeeId;
|
|
}
|
|
|
|
export function getHoursPerMonth(plan: Plan): number {
|
|
return plan.hoursPerMonth == 'unlimited' ? ABSOLUTE_MAX_USAGE : plan.hoursPerMonth;
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum number of parallel workspaces for the given plan
|
|
* @param plan
|
|
*/
|
|
export function getParallelWorkspacesById(planId: string | undefined): number {
|
|
return getParallelWorkspaces(Plans.getById(planId));
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum number of parallel workspaces for the given plan
|
|
* @param plan
|
|
*/
|
|
export function getParallelWorkspaces(plan: Plan | undefined): number {
|
|
const DEFAULT = 4;
|
|
if (!plan) {
|
|
return DEFAULT;
|
|
}
|
|
|
|
switch (plan.type) {
|
|
case "professional-new":
|
|
return 8;
|
|
|
|
case "professional":
|
|
case "student":
|
|
return 16;
|
|
}
|
|
return DEFAULT;
|
|
}
|
|
|
|
|
|
/**
|
|
* This declares the plan structure we have in Gitpod: All entries in a sub-array have the same arity in this structure.
|
|
* This is used to impose a partial order on plan types (cmp. compareTypes(...)).
|
|
* The order inside the sub-array carries meaning, too: The first one is the current, preferred plan (we advertise) for the given arity.
|
|
* This is used to be able to get the next "higher" plan (cmp. getNextHigherPlanType).
|
|
*/
|
|
const planStructure: PlanType[][] = [
|
|
["free-50", "free", "free-open-source"],
|
|
["personal", "basic"],
|
|
["professional-new"],
|
|
["professional", "student"]
|
|
];
|
|
|
|
function getPlanTypeArity(type: PlanType) {
|
|
return planStructure.findIndex((types: PlanType[]) => types.includes(type));
|
|
}
|
|
|
|
function getPlanTypeForArity(arity: number): PlanType | undefined {
|
|
if (arity >= planStructure.length) {
|
|
return undefined;
|
|
}
|
|
return planStructure[arity][0];
|
|
}
|
|
|
|
/**
|
|
* Returns the preferred plan type with the next higher arity
|
|
* @param type
|
|
*/
|
|
export function getNextHigherPlanType(type: PlanType): PlanType {
|
|
const arity = getPlanTypeArity(type);
|
|
const nextHigherType = getPlanTypeForArity(arity + 1);
|
|
return nextHigherType || "professional";
|
|
}
|
|
|
|
/**
|
|
* This imposes a partial order on the plan types
|
|
* @param planTypeA
|
|
* @param planTypeB
|
|
*/
|
|
export function compareTypes(planTypeA: PlanType, planTypeB: PlanType) {
|
|
const va = getPlanTypeArity(planTypeA);
|
|
const vb = getPlanTypeArity(planTypeB);
|
|
return va < vb ? -1 : va > vb ? 1 : 0;
|
|
}
|
|
|
|
export function subscriptionChange(fromType: PlanType, toType: PlanType): "upgrade" | "downgrade" | "none" {
|
|
const cmp = Plans.compareTypes(fromType, toType);
|
|
if (cmp < 0) {
|
|
return "upgrade";
|
|
} else if (cmp > 0) {
|
|
return "downgrade";
|
|
} else {
|
|
return "none";
|
|
}
|
|
}
|
|
|
|
export interface Feature {
|
|
title: string;
|
|
emph?: boolean;
|
|
link?: string;
|
|
tooltip?: string;
|
|
}
|
|
export namespace Feature {
|
|
export const getFeaturesFor = (p: Plan): Feature[] => {
|
|
switch (p.type) {
|
|
case "free":
|
|
return [
|
|
{ title: 'Public repositories' }
|
|
];
|
|
|
|
case "free-50":
|
|
return [
|
|
{ title: 'Public repositories' }
|
|
];
|
|
|
|
case "free-open-source":
|
|
return [
|
|
{ title: 'Public repositories' },
|
|
];
|
|
|
|
case "student":
|
|
return [
|
|
{ title: 'Private & Public repos' },
|
|
{ title: `${Plans.getParallelWorkspaces(p)} Parallel Workspaces`, tooltip: 'The number of workspaces running at the same time' },
|
|
{ title: 'Team Manageable', link: "/teams/", tooltip: 'Setup Gitpod for an entire Team with a single invoice and credit card' },
|
|
{ title: '1h Timeout', tooltip: 'Workspaces without user activity are stopped after 1 hour' },
|
|
{ title: '3h Timeout Boost', tooltip: 'You can manually boost the timeout to 3 hours within a running workspace' }
|
|
];
|
|
|
|
case "basic":
|
|
return [
|
|
{ title: 'Private & Public repos' },
|
|
{ title: `${Plans.getParallelWorkspaces(p)} Parallel Workspaces`, tooltip: 'The number of workspaces running at the same time.' },
|
|
];
|
|
|
|
// Personal
|
|
case "personal":
|
|
return [
|
|
{ title: 'Private & Public repos' },
|
|
{ title: `${Plans.getParallelWorkspaces(p)} Parallel Workspaces`, tooltip: 'The number of workspaces running at the same time' },
|
|
{ title: '30min Timeout', tooltip: 'Workspaces without user activity are stopped after 30 minutes' }
|
|
];
|
|
|
|
// Professional
|
|
case "professional-new":
|
|
return [
|
|
{ title: 'Private & Public repos' },
|
|
{ title: `${Plans.getParallelWorkspaces(p)} Parallel Workspaces`, tooltip: 'The number of workspaces running at the same time' },
|
|
{ title: 'Team Manageable', link: "/teams/", tooltip: 'Setup Gitpod for an entire Team with a single invoice and credit card' },
|
|
{ title: '30min Timeout', tooltip: 'Workspaces without user activity are stopped after 30 minutes' }
|
|
];
|
|
|
|
// Unleashed
|
|
case "professional":
|
|
return [
|
|
{ title: 'Private & Public repos' },
|
|
{ title: `${Plans.getParallelWorkspaces(p)} Parallel Workspaces`, tooltip: 'The number of workspaces running at the same time' },
|
|
{ title: 'Team Manageable', link: "/teams/", tooltip: 'Setup Gitpod for an entire Team with a single invoice and credit card' },
|
|
{ title: '1h Timeout', tooltip: 'Workspaces without user activity are stopped after 1 hour' },
|
|
{ title: '3h Timeout Boost', tooltip: 'You can manually boost the timeout to 3 hours within a running workspace' }
|
|
];
|
|
}
|
|
};
|
|
}
|
|
}
|