11import { Theme , TenantTag } from '@logto/schemas' ;
22import { condArray } from '@silverhand/essentials' ;
33import { useCallback , useMemo , useState } from 'react' ;
4- import { Controller , FormProvider , useForm } from 'react-hook-form' ;
4+ import { Controller , type ControllerRenderProps , FormProvider , useForm } from 'react-hook-form' ;
55import { toast } from 'react-hot-toast' ;
66import { useTranslation } from 'react-i18next' ;
77import Modal from 'react-modal' ;
@@ -12,9 +12,10 @@ import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
1212import { type TenantResponse , type RegionResponse as RegionType } from '@/cloud/types/router' ;
1313import Region , {
1414 defaultRegionName ,
15- logtoDropdownItem ,
15+ publicInstancesDropdownItem ,
1616 type InstanceDropdownItemProps ,
1717} from '@/components/Region' ;
18+ import { isDevFeaturesEnabled } from '@/consts/env' ;
1819import Button from '@/ds-components/Button' ;
1920import DangerousRaw from '@/ds-components/DangerousRaw' ;
2021import FormField from '@/ds-components/FormField' ;
@@ -48,26 +49,28 @@ const getInstanceDropdownItems = (regions: RegionType[]): InstanceDropdownItemPr
4849 . filter ( ( { isPrivate } ) => isPrivate )
4950 . map ( ( { id, name, country, tags, displayName } ) => ( { id, name, country, tags, displayName } ) ) ;
5051
51- return condArray ( hasPublicRegions && logtoDropdownItem , ...privateInstances ) ;
52+ return condArray ( hasPublicRegions && publicInstancesDropdownItem , ...privateInstances ) ;
5253} ;
5354
55+ const defaultFormValues = Object . freeze ( {
56+ tag : TenantTag . Development ,
57+ instanceId : publicInstancesDropdownItem . name ,
58+ regionName : defaultRegionName ,
59+ } ) ;
60+
5461function CreateTenantModal ( { isOpen, onClose } : Props ) {
5562 const [ tenantData , setTenantData ] = useState < CreateTenantData > ( ) ;
5663 const theme = useTheme ( ) ;
5764 const cloudApi = useCloudApi ( ) ;
5865 const { regions, regionsError, getRegionByName } = useAvailableRegions ( ) ;
5966
60- const defaultValues = Object . freeze ( {
61- tag : TenantTag . Development ,
62- instanceId : logtoDropdownItem . name ,
63- regionName : defaultRegionName ,
64- } ) ;
6567 const methods = useForm < CreateTenantData > ( {
66- defaultValues,
68+ defaultValues : defaultFormValues ,
6769 } ) ;
6870
6971 const {
7072 reset,
73+ setValue,
7174 control,
7275 handleSubmit,
7376 formState : { errors, isSubmitting } ,
@@ -84,53 +87,73 @@ function CreateTenantModal({ isOpen, onClose }: Props) {
8487 [ reset , watch ]
8588 ) ;
8689
87- const instanceOptions = useMemo ( ( ) => getInstanceDropdownItems ( regions ?? [ ] ) , [ regions ] ) ;
90+ const instanceDropdownItems = useMemo ( ( ) => getInstanceDropdownItems ( regions ?? [ ] ) , [ regions ] ) ;
8891 const hasPrivateRegionsAccess = useMemo ( ( ) => checkPrivateRegionAccess ( regions ?? [ ] ) , [ regions ] ) ;
8992
9093 const publicRegions = useMemo (
9194 ( ) => regions ?. filter ( ( region ) => ! region . isPrivate ) ?? [ ] ,
9295 [ regions ]
9396 ) ;
9497
95- const isLogtoInstance = useMemo ( ( ) => instanceId === logtoDropdownItem . name , [ instanceId ] ) ;
96-
97- const currentRegion = useMemo (
98- ( ) => getRegionByName ( isLogtoInstance ? regionName : instanceId ) ,
99- [ isLogtoInstance , regionName , instanceId , getRegionByName ]
98+ const isPublicInstanceSelected = useMemo (
99+ ( ) => instanceId === publicInstancesDropdownItem . name ,
100+ [ instanceId ]
100101 ) ;
101102
102- const getFinalRegionName = useCallback (
103- ( instanceId : string , regionName : string ) => {
104- return isLogtoInstance ? regionName : instanceId ;
105- } ,
106- [ isLogtoInstance ]
107- ) ;
103+ const currentRegion = useMemo ( ( ) => getRegionByName ( regionName ) , [ regionName , getRegionByName ] ) ;
108104
109- const createTenant = async ( { name, tag, instanceId, regionName } : CreateTenantData ) => {
110- // For Logto public instance, use the selected region
111- // For private instances, use the instance ID as the region
112- const finalRegionName = getFinalRegionName ( instanceId , regionName ) ;
105+ const createTenant = async ( { name, tag, regionName } : CreateTenantData ) => {
113106 const newTenant = await cloudApi . post ( '/api/tenants' , {
114- body : { name, tag, regionName : finalRegionName } ,
107+ body : { name, tag, regionName } ,
115108 } ) ;
116109 onClose ( newTenant ) ;
117110 } ;
118111 const { t } = useTranslation ( undefined , { keyPrefix : 'admin_console' } ) ;
119112
120113 const onCreateClick = handleSubmit (
121114 trySubmitSafe ( async ( data : CreateTenantData ) => {
122- const { tag } = data ;
115+ const { tag, instanceId } = data ;
123116 if ( tag === TenantTag . Development ) {
124117 await createTenant ( data ) ;
125118 toast . success ( t ( 'tenants.create_modal.tenant_created' ) ) ;
126119 return ;
127120 }
128121
122+ // TODO: remove the dev feature guard once the enterprise subscription is ready
123+ // Private region production tenant creation
124+ if ( isDevFeaturesEnabled && instanceId !== publicInstancesDropdownItem . name ) {
125+ // Directly call the create tenant API instead of going through the plan selection modal.
126+ // Based on product design, private region can only have one production tenant plan,
127+ // and should not go through the subscription checkout flow,
128+ // always associated the new tenant with the existing enterprise subscription of the private region.
129+ await createTenant ( data ) ;
130+ toast . success ( t ( 'tenants.create_modal.tenant_created' ) ) ;
131+ return ;
132+ }
133+
129134 // For production tenants, store creation parameters with the correct regionName for later use after plan selection.
130- setTenantData ( { ... data , regionName : getFinalRegionName ( data . instanceId , data . regionName ) } ) ;
135+ setTenantData ( data ) ;
131136 } )
132137 ) ;
133138
139+ const handleInstanceIdChange = useCallback (
140+ (
141+ nextId : string ,
142+ onChange : ControllerRenderProps < CreateTenantData , 'instanceId' > [ 'onChange' ]
143+ ) => {
144+ onChange ( nextId ) ;
145+
146+ if ( nextId === publicInstancesDropdownItem . name && publicRegions [ 0 ] ) {
147+ // Otherwise, reset to the first public region when switching to public instance.
148+ setValue ( 'regionName' , publicRegions [ 0 ] . name , { shouldValidate : true , shouldDirty : true } ) ;
149+ } else {
150+ // If switching to a private instance, reset regionName using the instanceId.
151+ setValue ( 'regionName' , nextId , { shouldValidate : true , shouldDirty : true } ) ;
152+ }
153+ } ,
154+ [ publicRegions , setValue ]
155+ ) ;
156+
134157 return (
135158 < Modal
136159 shouldCloseOnOverlayClick
@@ -139,7 +162,7 @@ function CreateTenantModal({ isOpen, onClose }: Props) {
139162 className = { modalStyles . content }
140163 overlayClassName = { modalStyles . overlay }
141164 onAfterClose = { ( ) => {
142- reset ( defaultValues ) ;
165+ reset ( defaultFormValues ) ;
143166 } }
144167 onRequestClose = { ( ) => {
145168 onClose ( ) ;
@@ -191,18 +214,20 @@ function CreateTenantModal({ isOpen, onClose }: Props) {
191214 rules = { { required : true } }
192215 render = { ( { field : { onChange, value } } ) => (
193216 < InstanceSelector
194- instances = { instanceOptions }
217+ instances = { instanceDropdownItems }
195218 value = { value }
196219 isDisabled = { isSubmitting }
197220 setTenantTagInForm = { setTenantTagInForm }
198- onChange = { onChange }
221+ onChange = { ( nextId ) => {
222+ handleInstanceIdChange ( nextId , onChange ) ;
223+ } }
199224 />
200225 ) }
201226 />
202227 ) }
203228 </ FormField >
204229 ) }
205- { isLogtoInstance && (
230+ { isPublicInstanceSelected && (
206231 < FormField
207232 title = "tenants.settings.tenant_region"
208233 tip = { t ( 'tenants.settings.tenant_region_description' ) }
0 commit comments