Multitenantperorg enabling
All checks were successful
Build and Push / build (push) Successful in 1m21s
All checks were successful
Build and Push / build (push) Successful in 1m21s
This commit is contained in:
@@ -6,64 +6,81 @@ import { Card } from "@/components/ui/card";
|
||||
import { StatusBadge } from "@/components/ui/status-badge";
|
||||
import { formatDateTime, formatRelative } from "@/lib/format";
|
||||
|
||||
interface OnboardingState {
|
||||
state: string;
|
||||
request?: {
|
||||
id: string;
|
||||
status: string;
|
||||
companyName: string;
|
||||
agentName: string;
|
||||
adminNotes?: string;
|
||||
createdAt?: string;
|
||||
};
|
||||
tenant?: {
|
||||
name: string;
|
||||
phase: string;
|
||||
message?: string;
|
||||
conditions?: Array<{
|
||||
type: string;
|
||||
status: string;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
lastTransitionTime?: string;
|
||||
}>;
|
||||
};
|
||||
interface RequestSummary {
|
||||
id: string;
|
||||
instanceName?: string | null;
|
||||
agentName: string;
|
||||
packages: string[];
|
||||
status: string;
|
||||
adminNotes?: string;
|
||||
tenantName?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export function ProvisioningStatus() {
|
||||
interface TenantSummary {
|
||||
name: string;
|
||||
displayName: string;
|
||||
phase: string;
|
||||
conditions: Array<{
|
||||
type: string;
|
||||
status: string;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
lastTransitionTime?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface SingleRequestState {
|
||||
request: RequestSummary;
|
||||
tenant: TenantSummary | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ProvisioningStatus
|
||||
*
|
||||
* Polls /api/onboarding?id=<requestId> every 5s until the request reaches
|
||||
* a terminal state. Slice 3: takes a `requestId` prop so multiple of these
|
||||
* can render on the same dashboard for different in-flight requests.
|
||||
*
|
||||
* The pre-Slice-3 version polled /api/onboarding with no params and
|
||||
* assumed one-request-per-org — that endpoint shape is gone now.
|
||||
*/
|
||||
export function ProvisioningStatus({ requestId }: { requestId: string }) {
|
||||
const t = useTranslations("onboarding");
|
||||
const f = useFormatter();
|
||||
const [data, setData] = useState<OnboardingState | null>(null);
|
||||
const [data, setData] = useState<SingleRequestState | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const poll = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/onboarding");
|
||||
const res = await fetch(
|
||||
`/api/onboarding?id=${encodeURIComponent(requestId)}`
|
||||
);
|
||||
if (!res.ok) throw new Error("Failed to fetch status");
|
||||
const json = await res.json();
|
||||
setData(json);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
}
|
||||
}, []);
|
||||
}, [requestId]);
|
||||
|
||||
useEffect(() => {
|
||||
poll();
|
||||
|
||||
// Poll every 5 seconds while not in a terminal state
|
||||
const interval = setInterval(() => {
|
||||
if (
|
||||
data?.state === "provisioned" ||
|
||||
data?.state === "rejected" ||
|
||||
data?.state === "active"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
poll();
|
||||
}, 5000);
|
||||
const status = data?.request?.status;
|
||||
const phase = data?.tenant?.phase;
|
||||
const terminal =
|
||||
status === "rejected" ||
|
||||
status === "active" ||
|
||||
phase === "Ready" ||
|
||||
phase === "Running";
|
||||
|
||||
if (terminal) return;
|
||||
|
||||
const interval = setInterval(poll, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [poll, data?.state]);
|
||||
}, [poll, data?.request?.status, data?.tenant?.phase]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@@ -84,8 +101,14 @@ export function ProvisioningStatus() {
|
||||
);
|
||||
}
|
||||
|
||||
const status = data.request.status;
|
||||
const label =
|
||||
data.request.instanceName ||
|
||||
data.request.tenantName ||
|
||||
data.request.agentName;
|
||||
|
||||
// Pending admin approval
|
||||
if (data.state === "pending") {
|
||||
if (status === "pending") {
|
||||
return (
|
||||
<Card className="animate-in">
|
||||
<div className="text-center py-6">
|
||||
@@ -107,10 +130,13 @@ export function ProvisioningStatus() {
|
||||
<h2 className="font-display text-lg font-semibold text-text-primary mb-2">
|
||||
{t("pendingTitle")}
|
||||
</h2>
|
||||
{label && (
|
||||
<p className="text-xs font-mono text-text-secondary mb-2">{label}</p>
|
||||
)}
|
||||
<p className="text-sm text-text-secondary max-w-sm mx-auto">
|
||||
{t("pendingDescription")}
|
||||
</p>
|
||||
{data.request?.createdAt && (
|
||||
{data.request.createdAt && (
|
||||
<p
|
||||
className="text-xs text-text-muted mt-4"
|
||||
title={formatDateTime(data.request.createdAt, f)}
|
||||
@@ -130,7 +156,7 @@ export function ProvisioningStatus() {
|
||||
}
|
||||
|
||||
// Rejected
|
||||
if (data.state === "rejected") {
|
||||
if (status === "rejected") {
|
||||
return (
|
||||
<Card className="animate-in">
|
||||
<div className="text-center py-6">
|
||||
@@ -152,10 +178,13 @@ export function ProvisioningStatus() {
|
||||
<h2 className="font-display text-lg font-semibold text-text-primary mb-2">
|
||||
{t("rejectedTitle")}
|
||||
</h2>
|
||||
{label && (
|
||||
<p className="text-xs font-mono text-text-secondary mb-2">{label}</p>
|
||||
)}
|
||||
<p className="text-sm text-text-secondary max-w-sm mx-auto">
|
||||
{t("rejectedDescription")}
|
||||
</p>
|
||||
{data.request?.adminNotes && (
|
||||
{data.request.adminNotes && (
|
||||
<p className="text-xs text-text-muted mt-3 bg-surface-2 border border-border rounded-lg p-3 max-w-sm mx-auto">
|
||||
{data.request.adminNotes}
|
||||
</p>
|
||||
@@ -165,10 +194,11 @@ export function ProvisioningStatus() {
|
||||
);
|
||||
}
|
||||
|
||||
// Provisioning in progress
|
||||
// Provisioning in progress (status approved/provisioning, optionally with tenant phase < Ready)
|
||||
if (
|
||||
data.state === "approved" ||
|
||||
data.state === "provisioning"
|
||||
status === "approved" ||
|
||||
status === "provisioning" ||
|
||||
(status === "active" && data.tenant && data.tenant.phase !== "Ready")
|
||||
) {
|
||||
const phase = data.tenant?.phase ?? "Pending";
|
||||
const conditions = data.tenant?.conditions ?? [];
|
||||
@@ -182,6 +212,9 @@ export function ProvisioningStatus() {
|
||||
<h2 className="font-display text-lg font-semibold text-text-primary mb-2">
|
||||
{t("provisioningTitle")}
|
||||
</h2>
|
||||
{label && (
|
||||
<p className="text-xs font-mono text-text-secondary mb-2">{label}</p>
|
||||
)}
|
||||
<p className="text-sm text-text-secondary">
|
||||
{t("provisioningDescription")}
|
||||
</p>
|
||||
@@ -216,8 +249,8 @@ export function ProvisioningStatus() {
|
||||
);
|
||||
}
|
||||
|
||||
// Provisioned / Running
|
||||
if (data.state === "provisioned") {
|
||||
// Active / Ready
|
||||
if (status === "active") {
|
||||
return (
|
||||
<Card className="animate-in">
|
||||
<div className="text-center py-6">
|
||||
@@ -239,6 +272,9 @@ export function ProvisioningStatus() {
|
||||
<h2 className="font-display text-lg font-semibold text-text-primary mb-2">
|
||||
{t("readyTitle")}
|
||||
</h2>
|
||||
{label && (
|
||||
<p className="text-xs font-mono text-text-secondary mb-2">{label}</p>
|
||||
)}
|
||||
<p className="text-sm text-text-secondary max-w-sm mx-auto mb-4">
|
||||
{t("readyDescription")}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user