|
8 | 8 |
|
9 | 9 | "use client"; |
10 | 10 |
|
11 | | -import { use, useEffect } from "react"; |
| 11 | +import { use, useEffect, Suspense } from "react"; |
12 | 12 | import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; |
13 | 13 | import { useForm } from "react-hook-form"; |
14 | 14 | import { zodResolver } from "@hookform/resolvers/zod"; |
@@ -48,11 +48,8 @@ const subNavLinks = (id: string) => [ |
48 | 48 | { href: `/tenants/${id}/config`, label: "Config", icon: Settings2 }, |
49 | 49 | ]; |
50 | 50 |
|
51 | | -export default function TenantDetailPage({ |
52 | | - params, |
53 | | -}: { |
54 | | - params: Promise<{ id: string }>; |
55 | | -}) { |
| 51 | +// Inner component — safe to call use(params) here because it's wrapped in Suspense |
| 52 | +function TenantDetailContent({ params }: { params: Promise<{ id: string }> }) { |
56 | 53 | const { id } = use(params); |
57 | 54 | const { toast } = useToast(); |
58 | 55 | const qc = useQueryClient(); |
@@ -97,110 +94,123 @@ export default function TenantDetailPage({ |
97 | 94 | }, [tenant, reset]); |
98 | 95 |
|
99 | 96 | return ( |
100 | | - <Shell> |
101 | | - <div className="space-y-6 max-w-2xl"> |
102 | | - <div className="flex items-center gap-3"> |
103 | | - <Link href="/tenants"> |
104 | | - <Button variant="ghost" size="sm"> |
105 | | - <ArrowLeft className="h-4 w-4" /> |
106 | | - Tenants |
107 | | - </Button> |
108 | | - </Link> |
109 | | - </div> |
110 | | - |
111 | | - {isLoading ? ( |
112 | | - <PageSpinner /> |
113 | | - ) : !tenant ? ( |
114 | | - <p className="text-sm text-slate-500">Tenant not found.</p> |
115 | | - ) : ( |
116 | | - <> |
117 | | - {/* Header */} |
118 | | - <div className="flex items-start justify-between"> |
119 | | - <div> |
120 | | - <div className="flex items-center gap-2"> |
121 | | - <h2 className="text-lg font-semibold text-slate-900">{tenant.name}</h2> |
122 | | - <TenantStatusBadge status={tenant.status} /> |
123 | | - <PlanBadge plan={tenant.plan} /> |
124 | | - </div> |
125 | | - <p className="text-sm text-slate-500 mt-0.5 font-mono">{tenant.slug}</p> |
| 97 | + <div className="space-y-6 max-w-2xl"> |
| 98 | + <div className="flex items-center gap-3"> |
| 99 | + <Link href="/tenants"> |
| 100 | + <Button variant="ghost" size="sm"> |
| 101 | + <ArrowLeft className="h-4 w-4" /> |
| 102 | + Tenants |
| 103 | + </Button> |
| 104 | + </Link> |
| 105 | + </div> |
| 106 | + |
| 107 | + {isLoading ? ( |
| 108 | + <PageSpinner /> |
| 109 | + ) : !tenant ? ( |
| 110 | + <p className="text-sm text-slate-500">Tenant not found.</p> |
| 111 | + ) : ( |
| 112 | + <> |
| 113 | + {/* Header */} |
| 114 | + <div className="flex items-start justify-between"> |
| 115 | + <div> |
| 116 | + <div className="flex items-center gap-2"> |
| 117 | + <h2 className="text-lg font-semibold text-slate-900">{tenant.name}</h2> |
| 118 | + <TenantStatusBadge status={tenant.status} /> |
| 119 | + <PlanBadge plan={tenant.plan} /> |
126 | 120 | </div> |
| 121 | + <p className="text-sm text-slate-500 mt-0.5 font-mono">{tenant.slug}</p> |
127 | 122 | </div> |
| 123 | + </div> |
128 | 124 |
|
129 | | - {/* Sub-nav quick links */} |
130 | | - <div className="flex gap-2 flex-wrap"> |
131 | | - {subNavLinks(id).map((l) => ( |
132 | | - <Link key={l.href} href={l.href}> |
133 | | - <Button variant="outline" size="sm"> |
134 | | - <l.icon className="h-3.5 w-3.5" /> |
135 | | - {l.label} |
136 | | - </Button> |
137 | | - </Link> |
138 | | - ))} |
139 | | - </div> |
| 125 | + {/* Sub-nav quick links */} |
| 126 | + <div className="flex gap-2 flex-wrap"> |
| 127 | + {subNavLinks(id).map((l) => ( |
| 128 | + <Link key={l.href} href={l.href}> |
| 129 | + <Button variant="outline" size="sm"> |
| 130 | + <l.icon className="h-3.5 w-3.5" /> |
| 131 | + {l.label} |
| 132 | + </Button> |
| 133 | + </Link> |
| 134 | + ))} |
| 135 | + </div> |
140 | 136 |
|
141 | | - {/* Meta */} |
142 | | - <Card> |
143 | | - <CardHeader> |
144 | | - <CardTitle>Details</CardTitle> |
145 | | - </CardHeader> |
146 | | - <CardContent className="grid grid-cols-2 gap-4 text-sm"> |
147 | | - <div> |
148 | | - <p className="text-xs font-medium text-slate-500 uppercase tracking-wide">ID</p> |
149 | | - <p className="font-mono text-xs mt-1 text-slate-700">{tenant.id}</p> |
150 | | - </div> |
151 | | - <div> |
152 | | - <p className="text-xs font-medium text-slate-500 uppercase tracking-wide">Created</p> |
153 | | - <p className="mt-1 text-slate-700">{formatDate(tenant.createdAt)}</p> |
154 | | - </div> |
155 | | - <div> |
156 | | - <p className="text-xs font-medium text-slate-500 uppercase tracking-wide">Updated</p> |
157 | | - <p className="mt-1 text-slate-700">{formatDate(tenant.updatedAt)}</p> |
| 137 | + {/* Meta */} |
| 138 | + <Card> |
| 139 | + <CardHeader> |
| 140 | + <CardTitle>Details</CardTitle> |
| 141 | + </CardHeader> |
| 142 | + <CardContent className="grid grid-cols-2 gap-4 text-sm"> |
| 143 | + <div> |
| 144 | + <p className="text-xs font-medium text-slate-500 uppercase tracking-wide">ID</p> |
| 145 | + <p className="font-mono text-xs mt-1 text-slate-700">{tenant.id}</p> |
| 146 | + </div> |
| 147 | + <div> |
| 148 | + <p className="text-xs font-medium text-slate-500 uppercase tracking-wide">Created</p> |
| 149 | + <p className="mt-1 text-slate-700">{formatDate(tenant.createdAt)}</p> |
| 150 | + </div> |
| 151 | + <div> |
| 152 | + <p className="text-xs font-medium text-slate-500 uppercase tracking-wide">Updated</p> |
| 153 | + <p className="mt-1 text-slate-700">{formatDate(tenant.updatedAt)}</p> |
| 154 | + </div> |
| 155 | + </CardContent> |
| 156 | + </Card> |
| 157 | + |
| 158 | + {/* Edit form */} |
| 159 | + <Card> |
| 160 | + <CardHeader> |
| 161 | + <CardTitle>Edit Tenant</CardTitle> |
| 162 | + <CardDescription>Update name, plan, or settings.</CardDescription> |
| 163 | + </CardHeader> |
| 164 | + <CardContent> |
| 165 | + <form |
| 166 | + onSubmit={handleSubmit((v) => updateMutation.mutate(v))} |
| 167 | + className="space-y-4" |
| 168 | + > |
| 169 | + <Input |
| 170 | + label="Name" |
| 171 | + error={errors.name?.message} |
| 172 | + {...register("name")} |
| 173 | + /> |
| 174 | + <Select |
| 175 | + label="Plan" |
| 176 | + options={planOptions} |
| 177 | + error={errors.plan?.message} |
| 178 | + {...register("plan")} |
| 179 | + /> |
| 180 | + <Textarea |
| 181 | + label="Settings (JSON)" |
| 182 | + rows={5} |
| 183 | + className="font-mono text-xs" |
| 184 | + hint="Free-form JSON metadata for this tenant" |
| 185 | + error={errors.settings?.message} |
| 186 | + {...register("settings")} |
| 187 | + /> |
| 188 | + <div className="flex justify-end"> |
| 189 | + <Button type="submit" loading={updateMutation.isPending}> |
| 190 | + <Save className="h-4 w-4" /> |
| 191 | + Save Changes |
| 192 | + </Button> |
158 | 193 | </div> |
159 | | - </CardContent> |
160 | | - </Card> |
161 | | - |
162 | | - {/* Edit form */} |
163 | | - <Card> |
164 | | - <CardHeader> |
165 | | - <CardTitle>Edit Tenant</CardTitle> |
166 | | - <CardDescription>Update name, plan, or settings.</CardDescription> |
167 | | - </CardHeader> |
168 | | - <CardContent> |
169 | | - <form |
170 | | - onSubmit={handleSubmit((v) => updateMutation.mutate(v))} |
171 | | - className="space-y-4" |
172 | | - > |
173 | | - <Input |
174 | | - label="Name" |
175 | | - error={errors.name?.message} |
176 | | - {...register("name")} |
177 | | - /> |
178 | | - <Select |
179 | | - label="Plan" |
180 | | - options={planOptions} |
181 | | - error={errors.plan?.message} |
182 | | - {...register("plan")} |
183 | | - /> |
184 | | - <Textarea |
185 | | - label="Settings (JSON)" |
186 | | - rows={5} |
187 | | - className="font-mono text-xs" |
188 | | - hint="Free-form JSON metadata for this tenant" |
189 | | - error={errors.settings?.message} |
190 | | - {...register("settings")} |
191 | | - /> |
192 | | - <div className="flex justify-end"> |
193 | | - <Button type="submit" loading={updateMutation.isPending}> |
194 | | - <Save className="h-4 w-4" /> |
195 | | - Save Changes |
196 | | - </Button> |
197 | | - </div> |
198 | | - </form> |
199 | | - </CardContent> |
200 | | - </Card> |
201 | | - </> |
202 | | - )} |
203 | | - </div> |
| 194 | + </form> |
| 195 | + </CardContent> |
| 196 | + </Card> |
| 197 | + </> |
| 198 | + )} |
| 199 | + </div> |
| 200 | + ); |
| 201 | +} |
| 202 | + |
| 203 | +// Outer page wraps the content in Suspense so use(params) has a boundary above it |
| 204 | +export default function TenantDetailPage({ |
| 205 | + params, |
| 206 | +}: { |
| 207 | + params: Promise<{ id: string }>; |
| 208 | +}) { |
| 209 | + return ( |
| 210 | + <Shell> |
| 211 | + <Suspense fallback={<PageSpinner />}> |
| 212 | + <TenantDetailContent params={params} /> |
| 213 | + </Suspense> |
204 | 214 | </Shell> |
205 | 215 | ); |
206 | 216 | } |
0 commit comments