mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-19 11:26:37 +00:00
support creating multiple orgs in saas
This commit is contained in:
@@ -4,19 +4,14 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@app/components/ui/card";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
import { build } from "@server/build";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { z } from "zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
@@ -35,7 +30,7 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger
|
||||
} from "@app/components/ui/collapsible";
|
||||
import { ChevronsUpDown } from "lucide-react";
|
||||
import { ArrowRight, ChevronsUpDown } from "lucide-react";
|
||||
import { cn } from "@app/lib/cn";
|
||||
|
||||
type Step = "org" | "site" | "resources";
|
||||
@@ -45,6 +40,7 @@ export default function StepperForm() {
|
||||
const [orgIdTaken, setOrgIdTaken] = useState(false);
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const { user } = useUserContext();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
@@ -71,12 +67,27 @@ export default function StepperForm() {
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const isFirstOrg = searchParams.get("firstOrg") != null;
|
||||
|
||||
// Fetch default subnet on component mount
|
||||
useEffect(() => {
|
||||
fetchDefaultSubnet();
|
||||
}, []);
|
||||
|
||||
// Prefill org name and id when build is saas and firstOrg query param is set
|
||||
useEffect(() => {
|
||||
if (build !== "saas" || !user || !isFirstOrg) return;
|
||||
|
||||
const orgName = user.email
|
||||
? `${user.email}'s Organization`
|
||||
: "My Organization";
|
||||
const orgId = `org_${user.userId}`;
|
||||
orgForm.setValue("orgName", orgName);
|
||||
orgForm.setValue("orgId", orgId);
|
||||
debouncedCheckOrgIdAvailability(orgId);
|
||||
}, []);
|
||||
|
||||
const fetchDefaultSubnet = async () => {
|
||||
try {
|
||||
const res = await api.get(`/pick-org-defaults`);
|
||||
@@ -161,263 +172,239 @@ export default function StepperForm() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("setupNewOrg")}</CardTitle>
|
||||
<CardDescription>{t("setupCreate")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<section className="space-y-6">
|
||||
<div className="flex justify-between mb-2">
|
||||
<div className="flex flex-col items-center">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center mb-2 ${
|
||||
currentStep === "org"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
1
|
||||
</div>
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
currentStep === "org"
|
||||
? "text-primary"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{t("setupCreateOrg")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center mb-2 ${
|
||||
currentStep === "site"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
2
|
||||
</div>
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
currentStep === "site"
|
||||
? "text-primary"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{t("siteCreate")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center mb-2 ${
|
||||
currentStep === "resources"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
3
|
||||
</div>
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
currentStep === "resources"
|
||||
? "text-primary"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{t("setupCreateResources")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<section className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
{t("setupNewOrg")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm mt-1">
|
||||
{t("setupCreate")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between mb-2">
|
||||
<div className="flex flex-col items-center">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center mb-2 ${
|
||||
currentStep === "org"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
1
|
||||
</div>
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
currentStep === "org"
|
||||
? "text-primary"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{t("setupCreateOrg")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center mb-2 ${
|
||||
currentStep === "site"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
2
|
||||
</div>
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
currentStep === "site"
|
||||
? "text-primary"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{t("siteCreate")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center mb-2 ${
|
||||
currentStep === "resources"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
3
|
||||
</div>
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
currentStep === "resources"
|
||||
? "text-primary"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{t("setupCreateResources")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<Separator />
|
||||
|
||||
{currentStep === "org" && (
|
||||
<Form {...orgForm}>
|
||||
<form
|
||||
onSubmit={orgForm.handleSubmit(orgSubmit)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={orgForm.control}
|
||||
name="orgName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("setupOrgName")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
// Prevent "/" in orgName input
|
||||
const sanitizedValue =
|
||||
e.target.value.replace(
|
||||
/\//g,
|
||||
"-"
|
||||
);
|
||||
const orgId =
|
||||
generateId(
|
||||
sanitizedValue
|
||||
);
|
||||
orgForm.setValue(
|
||||
"orgId",
|
||||
orgId
|
||||
);
|
||||
orgForm.setValue(
|
||||
"orgName",
|
||||
sanitizedValue
|
||||
);
|
||||
debouncedCheckOrgIdAvailability(
|
||||
orgId
|
||||
);
|
||||
}}
|
||||
value={field.value.replace(
|
||||
/\//g,
|
||||
"-"
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t("orgDisplayName")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={orgForm.control}
|
||||
name="orgId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("orgId")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"setupIdentifierMessage"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{currentStep === "org" && (
|
||||
<Form {...orgForm}>
|
||||
<form
|
||||
onSubmit={orgForm.handleSubmit(orgSubmit)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={orgForm.control}
|
||||
name="orgName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("setupOrgName")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
// Prevent "/" in orgName input
|
||||
const sanitizedValue =
|
||||
e.target.value.replace(
|
||||
/\//g,
|
||||
"-"
|
||||
);
|
||||
const orgId =
|
||||
generateId(sanitizedValue);
|
||||
orgForm.setValue(
|
||||
"orgId",
|
||||
orgId
|
||||
);
|
||||
orgForm.setValue(
|
||||
"orgName",
|
||||
sanitizedValue
|
||||
);
|
||||
debouncedCheckOrgIdAvailability(
|
||||
orgId
|
||||
);
|
||||
}}
|
||||
value={field.value.replace(
|
||||
/\//g,
|
||||
"-"
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t("orgDisplayName")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={orgForm.control}
|
||||
name="orgId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("orgId")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t("setupIdentifierMessage")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Collapsible
|
||||
open={isAdvancedOpen}
|
||||
onOpenChange={setIsAdvancedOpen}
|
||||
className="space-y-2"
|
||||
<Collapsible
|
||||
open={isAdvancedOpen}
|
||||
onOpenChange={setIsAdvancedOpen}
|
||||
className="space-y-2"
|
||||
>
|
||||
<div className="flex items-center justify-between space-x-4">
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="text"
|
||||
size="sm"
|
||||
className="p-0 flex items-center justify-between w-full"
|
||||
>
|
||||
<div className="flex items-center justify-between space-x-4">
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="text"
|
||||
size="sm"
|
||||
className="p-0 flex items-center justify-between w-full"
|
||||
>
|
||||
<h4 className="text-sm">
|
||||
{t("advancedSettings")}
|
||||
</h4>
|
||||
<div>
|
||||
<ChevronsUpDown className="h-4 w-4" />
|
||||
<span className="sr-only">
|
||||
{t("toggle")}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<h4 className="text-sm">
|
||||
{t("advancedSettings")}
|
||||
</h4>
|
||||
<div>
|
||||
<ChevronsUpDown className="h-4 w-4" />
|
||||
<span className="sr-only">
|
||||
{t("toggle")}
|
||||
</span>
|
||||
</div>
|
||||
<CollapsibleContent className="space-y-4">
|
||||
<FormField
|
||||
control={orgForm.control}
|
||||
name="subnet"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"setupSubnetAdvanced"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"setupSubnetDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
<CollapsibleContent className="space-y-4">
|
||||
<FormField
|
||||
control={orgForm.control}
|
||||
name="subnet"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("setupSubnetAdvanced")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t("setupSubnetDescription")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={orgForm.control}
|
||||
name="utilitySubnet"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("setupUtilitySubnet")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"setupUtilitySubnetDescription"
|
||||
)}
|
||||
/>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
<FormField
|
||||
control={orgForm.control}
|
||||
name="utilitySubnet"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"setupUtilitySubnet"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"setupUtilitySubnetDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
{orgIdTaken && !orgCreated ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
{t("setupErrorIdentifier")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{orgIdTaken && !orgCreated ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
{t("setupErrorIdentifier")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
{/* Error Alert removed, errors now shown as toast */}
|
||||
|
||||
{/* Error Alert removed, errors now shown as toast */}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
loading={loading}
|
||||
disabled={loading || orgIdTaken}
|
||||
>
|
||||
{t("setupCreateOrg")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
loading={loading}
|
||||
disabled={loading || orgIdTaken}
|
||||
>
|
||||
{t("setupCreateOrg")}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user