Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add vm tier setting when creating a devbox #8360

Merged
merged 4 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/app/src/app/components/Create/CreateBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,18 @@ export const CreateBox: React.FC<CreateBoxProps> = ({

const createFromTemplate = (
template: TemplateFragment,
{ name, createAs, permission, editor }: CreateParams
{ name, createAs, permission, editor, customVMTier }: CreateParams
) => {
const { sandbox } = template;
const openInVSCode = editor === 'vscode';

track(`Create ${type} - Create`, {
type: 'fork',
title: name,
template_name:
template.sandbox.title || template.sandbox.alias || template.sandbox.id,
open_in_editor: editor,
...(customVMTier ? { vm_tier: customVMTier } : {}),
});

actions.editor.forkExternalSandbox({
Expand All @@ -147,6 +149,7 @@ export const CreateBox: React.FC<CreateBoxProps> = ({
openInVSCode,
autoLaunchVSCode,
hasBetaEditorExperiment,
customVMTier,
body: {
title: name,
collectionId,
Expand Down
80 changes: 54 additions & 26 deletions packages/app/src/app/components/Create/CreateBox/CreateBoxForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Query } from 'react-apollo';
import { useWorkspaceLimits } from 'app/hooks/useWorkspaceLimits';
import { useWorkspaceAuthorization } from 'app/hooks/useWorkspaceAuthorization';
import { Link } from 'react-router-dom';
import { VMTier } from 'app/overmind/effects/api/types';
import { CreateParams } from '../utils/types';

interface CreateBoxFormProps {
Expand All @@ -42,12 +43,16 @@ export const CreateBoxForm: React.FC<CreateBoxFormProps> = ({
const label = type === 'sandbox' ? 'Sandbox' : 'Devbox';

const { activeTeamInfo, activeTeam } = useAppState();
const { hasReachedSandboxLimit, hasReachedDraftLimit } = useWorkspaceLimits();
const {
hasReachedSandboxLimit,
hasReachedDraftLimit,
highestAllowedVMTier,
} = useWorkspaceLimits();
const { isAdmin } = useWorkspaceAuthorization();
const [name, setName] = useState<string>();
const effects = useEffects();
const nameInputRef = useRef<HTMLInputElement>(null);
const { isPro } = useWorkspaceSubscription();
const { isFree } = useWorkspaceSubscription();
const isDraft = collectionId === undefined;
const canSetPrivacy = !isDraft;
const canCreateDraft = !hasReachedDraftLimit;
Expand All @@ -65,21 +70,9 @@ export const CreateBoxForm: React.FC<CreateBoxFormProps> = ({
'csb'
);

const defaultSpecs =
// eslint-disable-next-line no-nested-ternary
type === 'sandbox'
? 'Browser'
: isPro
? '4 vCPUs - 8GiB RAM - 12GB disk'
: '2 vCPUs - 2GiB RAM - 6GB disk';

const specsInfo =
// eslint-disable-next-line no-nested-ternary
type === 'sandbox'
? 'Sandboxes run in your browser.'
: isPro
? 'VM specs are currently tied to your Pro subscription.'
: 'Better specs are available for Pro workspaces';
const defaultTier = isFree ? 1 : 2;
const [selectedTier, setSelectedTier] = useState<number>(defaultTier);
const [availableTiers, setAvailableTiers] = useState<VMTier[]>([]);

useEffect(() => {
effects.api.getSandboxTitle().then(({ title }) => {
Expand All @@ -89,6 +82,13 @@ export const CreateBoxForm: React.FC<CreateBoxFormProps> = ({
nameInputRef.current.select();
}
});
if (type === 'devbox') {
effects.api.getVMSpecs().then(res => {
setAvailableTiers(
res.vmTiers.filter(t => t.tier <= highestAllowedVMTier)
);
});
}
}, []);

return (
Expand All @@ -109,6 +109,11 @@ export const CreateBoxForm: React.FC<CreateBoxFormProps> = ({
createAs: type,
permission,
editor: type === 'sandbox' ? 'csb' : editor, // ensure 'csb' is always passed when creating a sandbox
customVMTier:
// Only pass customVMTier if user selects something else than the default
availableTiers.length > 0 && selectedTier !== defaultTier
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's tricky, but it looks good to me

? selectedTier
: undefined,
});
}}
>
Expand Down Expand Up @@ -285,15 +290,38 @@ export const CreateBoxForm: React.FC<CreateBoxFormProps> = ({
<Text size={3} as="label">
Runtime
</Text>
<Input
css={{ cursor: 'not-allowed' }}
value={defaultSpecs}
disabled
/>
<Stack gap={1} align="center" css={{ color: '#A8BFFA' }}>
<Icon name="circleBang" />
<Text size={3}>{specsInfo}</Text>
</Stack>
{type === 'sandbox' ? (
<>
<Input css={{ cursor: 'not-allowed' }} value="Browser" disabled />
<Stack gap={1} align="center" css={{ color: '#A8BFFA' }}>
<Icon name="circleBang" />
<Text size={3}>Sandboxes run in your browser.</Text>
</Stack>
</>
) : (
<>
<Select
value={selectedTier}
disabled={availableTiers.length === 0}
onChange={e => setSelectedTier(parseInt(e.target.value, 10))}
>
{availableTiers.map(t => (
<option key={t.shortid} value={t.tier}>
{t.name} ({t.cpu} vCPUs, {t.memory} GiB RAM, {t.storage} GB
Disk for {t.creditBasis} credits/hour)
</option>
))}
</Select>
{isFree && (
<Stack gap={1} align="center" css={{ color: '#A8BFFA' }}>
<Icon name="circleBang" />
<Text size={3}>
Better specs are available for Pro workspaces.
</Text>
</Stack>
)}
</>
)}
</Stack>
</Stack>

Expand Down
1 change: 1 addition & 0 deletions packages/app/src/app/components/Create/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type CreateParams = {
createAs: 'devbox' | 'sandbox';
permission: 0 | 1 | 2;
editor: 'csb' | 'vscode';
customVMTier?: number;
};

export interface TemplateCollection {
Expand Down
14 changes: 14 additions & 0 deletions packages/app/src/app/graphql/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2058,6 +2058,12 @@ export type RootMutationType = {
* Not passing a specific argument will leave it unchanged, explicitly passing `null` will revert it to the default.
*/
updateProjectSettings: ProjectSettings;
/**
* Update the VM tier of a project. All branches will start using this new VM tier as soon as the user connects
* to the branch (also for running branches). To optimistically update a branch without reload/reconnect,
* you can pass a branch ID as well that we'll update immediately.
*/
updateProjectVmTier: Resources;
/**
* Update the settings for a sandbox. All settings are nullable.
* Not passing a specific argument will leave it unchanged, explicitly passing `null` will revert it to the default.
Expand Down Expand Up @@ -2623,6 +2629,12 @@ export type RootMutationTypeUpdateProjectSettingsArgs = {
projectId: Scalars['UUID4'];
};

export type RootMutationTypeUpdateProjectVmTierArgs = {
branchId: InputMaybe<Scalars['String']>;
projectId: Scalars['UUID4'];
vmTier: Scalars['Int'];
};

export type RootMutationTypeUpdateSandboxSettingsArgs = {
aiConsent: InputMaybe<Scalars['Boolean']>;
sandboxId: Scalars['ID'];
Expand Down Expand Up @@ -4719,6 +4731,7 @@ export type CurrentTeamInfoFragmentFragment = {
includedCredits: number;
includedSandboxes: number;
includedDrafts: number;
includedVmTier: number;
onDemandCreditLimit: number | null;
};
usage: { __typename?: 'TeamUsage'; sandboxes: number; credits: number };
Expand Down Expand Up @@ -6127,6 +6140,7 @@ export type GetTeamQuery = {
includedCredits: number;
includedSandboxes: number;
includedDrafts: number;
includedVmTier: number;
onDemandCreditLimit: number | null;
};
usage: { __typename?: 'TeamUsage'; sandboxes: number; credits: number };
Expand Down
10 changes: 9 additions & 1 deletion packages/app/src/app/hooks/useWorkspaceLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const useWorkspaceLimits = (): WorkspaceLimitsReturn => {
isFrozen: undefined,
hasReachedSandboxLimit: undefined,
hasReachedDraftLimit: undefined,
highestAllowedVMTier: undefined,
};
}

Expand Down Expand Up @@ -49,7 +50,11 @@ export const useWorkspaceLimits = (): WorkspaceLimitsReturn => {
const userDrafts =
userAuthorizations.find(ua => ua.userId === user.id)?.drafts ?? 0;
const hasReachedDraftLimit =
applyUbbRestrictions && isFree === true && userDrafts >= limits.includedDrafts;
applyUbbRestrictions &&
isFree === true &&
userDrafts >= limits.includedDrafts;

const highestAllowedVMTier = limits.includedVmTier;

return {
isOutOfCredits,
Expand All @@ -64,6 +69,7 @@ export const useWorkspaceLimits = (): WorkspaceLimitsReturn => {
isAtSpendingLimit ||
isCloseToSpendingLimit,
isFrozen: applyUbbRestrictions && frozen,
highestAllowedVMTier,
};
};

Expand All @@ -77,6 +83,7 @@ export type WorkspaceLimitsReturn =
isFrozen: undefined;
hasReachedSandboxLimit: undefined;
hasReachedDraftLimit: undefined;
highestAllowedVMTier: undefined;
}
| {
isOutOfCredits: boolean;
Expand All @@ -87,4 +94,5 @@ export type WorkspaceLimitsReturn =
isFrozen: boolean;
hasReachedSandboxLimit: boolean;
hasReachedDraftLimit: boolean;
highestAllowedVMTier: number;
};
8 changes: 8 additions & 0 deletions packages/app/src/app/overmind/effects/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,4 +678,12 @@ export default {
// version null ensures no /v1 is in the URL
return api.get('/vm_tiers', undefined, { version: null });
},
setVMSpecs(sandboxId: string, vmTier: number) {
console.log('vm_tier', vmTier);
console.log('type', typeof vmTier);

return api.patch(`/sandboxes/${sandboxId}/vm_tier`, {
vmTier,
});
},
};
1 change: 1 addition & 0 deletions packages/app/src/app/overmind/effects/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type VMTier = {
memory: number;
storage: number;
creditBasis: number;
tier: number;
};

export type APIPricingResult = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ export const currentTeamInfoFragment = gql`
includedCredits
includedSandboxes
includedDrafts
includedVmTier
onDemandCreditLimit
}

Expand Down
9 changes: 8 additions & 1 deletion packages/app/src/app/overmind/namespaces/editor/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -754,13 +754,15 @@ export const forkExternalSandbox = async (
openInVSCode,
autoLaunchVSCode,
hasBetaEditorExperiment,
customVMTier,
body,
}: {
sandboxId: string;
openInNewWindow?: boolean;
openInVSCode?: boolean;
autoLaunchVSCode?: boolean;
hasBetaEditorExperiment?: boolean;
customVMTier?: number;
body?: {
collectionId: string;
alias?: string;
Expand All @@ -770,7 +772,7 @@ export const forkExternalSandbox = async (
};
}
) => {
effects.analytics.track('Fork Sandbox', { type: 'external' });
effects.analytics.track('Fork Sandbox', { type: 'external', sandboxId });

const usedBody: ForkSandboxBody = body || {};

Expand All @@ -780,6 +782,11 @@ export const forkExternalSandbox = async (

try {
const forkedSandbox = await effects.api.forkSandbox(sandboxId, usedBody);

if (customVMTier) {
await effects.api.setVMSpecs(forkedSandbox.id, customVMTier);
}

if (openInVSCode) {
if (autoLaunchVSCode) {
window.open(vsCodeUrl(forkedSandbox.id));
Expand Down
8 changes: 2 additions & 6 deletions packages/app/src/app/utils/fuzzyMatchGithubToCsb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,13 @@ export const fuzzyMatchGithubToCsb = (

if (bestMatch) {
track('Match GH to CSB - success', {
codesandbox: 'V1',
event_source: 'UI',
match: bestMatch.login,
});

return bestMatch;
}

track('Match GH to CSB - fail', {
codesandbox: 'V1',
event_source: 'UI',
});
track('Match GH to CSB - fail');

return accounts[0];
};
Loading