feat(canvas): default tier T3 and hide T1/T2 on SaaS
On SaaS every workspace gets its own EC2 VM — the Docker-sandbox distinction between T1 (sandboxed), T2 (standard Docker), and T3 (full host access) doesn't apply. A SaaS workspace is always a dedicated VM, which is "full access" by construction. Showing T1/T2 in that UI is a category error: users pick a sandbox level that has no effect on the actual EC2 machine they get. Changes: - tenant.ts: export isSaaSTenant() — returns true when canvas is served at <slug>.moleculesai.app (SSR-safe: false on server) - CreateWorkspaceDialog: when isSaaSTenant(), render only the T3 option, default tier=3, grid collapses to a single column. Label gets a " — dedicated VM" hint so the user knows what they're getting. On self-hosted the full T1/T2/T3 picker is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2b603164de
commit
8b1af9708c
@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef, useCallback, useId } from "react";
|
||||
import { useState, useEffect, useRef, useCallback, useId, useMemo } from "react";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { api } from "@/lib/api";
|
||||
import { isSaaSTenant } from "@/lib/tenant";
|
||||
|
||||
interface WorkspaceOption {
|
||||
id: string;
|
||||
@ -39,7 +40,6 @@ export function CreateWorkspaceButton() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [role, setRole] = useState("");
|
||||
const [tier, setTier] = useState(1);
|
||||
const [template, setTemplate] = useState("");
|
||||
const [parentId, setParentId] = useState("");
|
||||
const [budgetLimit, setBudgetLimit] = useState("");
|
||||
@ -51,13 +51,29 @@ export function CreateWorkspaceButton() {
|
||||
const [hermesProvider, setHermesProvider] = useState("anthropic");
|
||||
const [hermesApiKey, setHermesApiKey] = useState("");
|
||||
|
||||
// Tier picker: on SaaS every workspace gets its own EC2 VM (Full Access
|
||||
// by construction), so we hide the T1/T2 sandbox options and lock to T3.
|
||||
// On self-hosted we still offer all three because Docker-sandbox tiers
|
||||
// are a real choice there. useMemo is SSR-safe via the isSaaSTenant()
|
||||
// contract (returns false on server); first client render may flip the
|
||||
// picker — acceptable one-frame reflow, no data loss.
|
||||
const isSaaS = useMemo(() => isSaaSTenant(), []);
|
||||
const TIERS = useMemo(
|
||||
() =>
|
||||
isSaaS
|
||||
? [{ value: 3, label: "T3", desc: "Full Access" }]
|
||||
: [
|
||||
{ value: 1, label: "T1", desc: "Sandboxed" },
|
||||
{ value: 2, label: "T2", desc: "Standard" },
|
||||
{ value: 3, label: "T3", desc: "Full Access" },
|
||||
],
|
||||
[isSaaS],
|
||||
);
|
||||
const defaultTier = isSaaS ? 3 : 1;
|
||||
const [tier, setTier] = useState(defaultTier);
|
||||
|
||||
// Refs for roving tabIndex on the tier radio group (WCAG 2.1 arrow-key nav)
|
||||
const radioRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
||||
const TIERS = [
|
||||
{ value: 1, label: "T1", desc: "Sandboxed" },
|
||||
{ value: 2, label: "T2", desc: "Standard" },
|
||||
{ value: 3, label: "T3", desc: "Full Access" },
|
||||
];
|
||||
|
||||
const handleRadioKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent, currentIndex: number) => {
|
||||
@ -85,7 +101,7 @@ export function CreateWorkspaceButton() {
|
||||
if (!open) return;
|
||||
setName("");
|
||||
setRole("");
|
||||
setTier(1);
|
||||
setTier(defaultTier);
|
||||
setTemplate("");
|
||||
setParentId("");
|
||||
setBudgetLimit("");
|
||||
@ -96,6 +112,9 @@ export function CreateWorkspaceButton() {
|
||||
.get<WorkspaceOption[]>("/workspaces")
|
||||
.then((ws) => setWorkspaces(ws))
|
||||
.catch(() => {});
|
||||
// defaultTier is stable for the session (derived from window.location),
|
||||
// safe to omit from deps.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
const handleCreate = async () => {
|
||||
@ -209,10 +228,10 @@ export function CreateWorkspaceButton() {
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="Workspace tier"
|
||||
className="grid grid-cols-3 gap-1.5"
|
||||
className={`grid gap-1.5 ${isSaaS ? "grid-cols-1" : "grid-cols-3"}`}
|
||||
>
|
||||
<div className="col-span-3 text-[11px] text-zinc-400 mb-1">
|
||||
Tier
|
||||
<div className={`text-[11px] text-zinc-400 mb-1 ${isSaaS ? "" : "col-span-3"}`}>
|
||||
Tier{isSaaS ? " — dedicated VM" : ""}
|
||||
</div>
|
||||
{TIERS.map((t, idx) => (
|
||||
<button
|
||||
|
||||
@ -54,3 +54,18 @@ export function getTenantSlug(): string {
|
||||
if (reservedSubdomains.has(slug)) return "";
|
||||
return slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* isSaaSTenant reports whether the canvas is running as the UI for a
|
||||
* SaaS tenant (served at <slug>.moleculesai.app). Use for client-side
|
||||
* UX branches that should behave differently on SaaS vs self-hosted —
|
||||
* e.g. the workspace tier picker hides T1/T2 sandbox tiers because every
|
||||
* SaaS workspace gets its own EC2 VM (inherently T3 Full Access).
|
||||
*
|
||||
* SSR-safe: returns false on the server to avoid hydration drift; call
|
||||
* sites should tolerate a flip from false→true on first client render.
|
||||
*/
|
||||
export function isSaaSTenant(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
return getTenantSlug() !== "";
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user