Wire production domain, CORS for tenant subdomains, celery volume mounts, and nginx reverse proxy configs for apex, API, identity, auth, and wildcard tenants. Co-authored-by: Cursor <cursoragent@cursor.com>
125 lines
4.4 KiB
TypeScript
125 lines
4.4 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, useState } from "react";
|
||
import Link from "next/link";
|
||
import { api, ApiError, type Tenant } from "@/lib/api";
|
||
import {
|
||
AdminPageHeader,
|
||
Banner,
|
||
Button,
|
||
EmptyState,
|
||
StatusBadge,
|
||
} from "@/components/admin/controls";
|
||
|
||
export default function AdminTenantsPage() {
|
||
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState("");
|
||
const [busyId, setBusyId] = useState<string | null>(null);
|
||
|
||
const load = useCallback(async () => {
|
||
setLoading(true);
|
||
setError("");
|
||
try {
|
||
const page = await api.platformTenants.list(1, 100);
|
||
setTenants(page.items);
|
||
} catch (e) {
|
||
setError(e instanceof ApiError ? e.message : "خطا در بارگذاری Tenantها");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
void load();
|
||
}, [load]);
|
||
|
||
const toggleStatus = async (t: Tenant) => {
|
||
setBusyId(t.id);
|
||
setError("");
|
||
try {
|
||
if (t.status === "suspended") {
|
||
await api.platformTenants.activate(t.id);
|
||
} else {
|
||
await api.platformTenants.suspend(t.id);
|
||
}
|
||
await load();
|
||
} catch (e) {
|
||
setError(e instanceof ApiError ? e.message : "تغییر وضعیت ناموفق بود");
|
||
} finally {
|
||
setBusyId(null);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<AdminPageHeader
|
||
title="مدیریت Tenantها"
|
||
subtitle="ایجاد، ویرایش و مدیریت وضعیت workspaceها"
|
||
action={
|
||
<Link href="/admin/tenants/new">
|
||
<Button>ایجاد Tenant</Button>
|
||
</Link>
|
||
}
|
||
/>
|
||
|
||
{error && <Banner variant="error">{error}</Banner>}
|
||
|
||
<section className="overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-sm">
|
||
{loading ? (
|
||
<EmptyState message="در حال بارگذاری..." />
|
||
) : tenants.length === 0 ? (
|
||
<EmptyState message="هنوز Tenantی ایجاد نشده است." />
|
||
) : (
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full min-w-[720px] text-sm">
|
||
<thead className="bg-gray-50 text-right">
|
||
<tr>
|
||
<th className="px-4 py-3 font-semibold text-secondary">نام</th>
|
||
<th className="px-4 py-3 font-semibold text-secondary">Slug</th>
|
||
<th className="px-4 py-3 font-semibold text-secondary">وضعیت</th>
|
||
<th className="px-4 py-3 font-semibold text-secondary">Onboarding</th>
|
||
<th className="px-4 py-3 font-semibold text-secondary">عملیات</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{tenants.map((t) => (
|
||
<tr key={t.id} className="border-t border-gray-100">
|
||
<td className="px-4 py-3 font-medium text-secondary">{t.name}</td>
|
||
<td className="px-4 py-3 font-mono text-gray-600" dir="ltr">
|
||
{t.slug}
|
||
</td>
|
||
<td className="px-4 py-3">
|
||
<StatusBadge status={t.status} />
|
||
</td>
|
||
<td className="px-4 py-3 text-gray-600">
|
||
{t.onboarding_completed ? "تکمیلشده" : "ناتمام"}
|
||
</td>
|
||
<td className="px-4 py-3">
|
||
<div className="flex flex-wrap gap-2">
|
||
<Link href={`/admin/tenants/${t.id}`}>
|
||
<Button variant="outline" size="sm">
|
||
مدیریت
|
||
</Button>
|
||
</Link>
|
||
<Button
|
||
variant={t.status === "suspended" ? "primary" : "ghost"}
|
||
size="sm"
|
||
loading={busyId === t.id}
|
||
onClick={() => toggleStatus(t)}
|
||
>
|
||
{t.status === "suspended" ? "فعالسازی" : "تعلیق"}
|
||
</Button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|