Add possibility for admin to suspend/delete
This commit is contained in:
39
src/app/api/admin/tenants/[name]/delete/route.ts
Normal file
39
src/app/api/admin/tenants/[name]/delete/route.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requirePlatformRole } from "@/lib/session";
|
||||
import { getTenant, deleteTenant } from "@/lib/k8s";
|
||||
|
||||
/**
|
||||
* POST /api/admin/tenants/[name]/delete
|
||||
* Delete a PiecedTenant CR. The operator handles cleanup
|
||||
* (namespace, vault, litellm team, etc.).
|
||||
*/
|
||||
export async function POST(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ name: string }> }
|
||||
) {
|
||||
try {
|
||||
await requirePlatformRole();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { name } = await params;
|
||||
|
||||
const tenant = await getTenant(name);
|
||||
if (!tenant) {
|
||||
return NextResponse.json({ error: "Tenant not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteTenant(name);
|
||||
return NextResponse.json({
|
||||
message: "Tenant deletion initiated. The operator will clean up all resources.",
|
||||
});
|
||||
} catch (e: any) {
|
||||
console.error("Failed to delete tenant:", e);
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to delete tenant: ${e.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
42
src/app/api/admin/tenants/[name]/suspend/route.ts
Normal file
42
src/app/api/admin/tenants/[name]/suspend/route.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requirePlatformRole } from "@/lib/session";
|
||||
import { getTenant, patchTenantSpec } from "@/lib/k8s";
|
||||
|
||||
/**
|
||||
* POST /api/admin/tenants/[name]/suspend
|
||||
* Toggle suspend on a PiecedTenant CR.
|
||||
* Body: { suspend: true } or { suspend: false }
|
||||
*/
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ name: string }> }
|
||||
) {
|
||||
try {
|
||||
await requirePlatformRole();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { name } = await params;
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const suspend = body.suspend === true;
|
||||
|
||||
const tenant = await getTenant(name);
|
||||
if (!tenant) {
|
||||
return NextResponse.json({ error: "Tenant not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await patchTenantSpec(name, { suspend });
|
||||
return NextResponse.json({
|
||||
message: suspend ? "Tenant suspended." : "Tenant resumed.",
|
||||
tenant: updated,
|
||||
});
|
||||
} catch (e: any) {
|
||||
console.error("Failed to update tenant suspend state:", e);
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to update tenant: ${e.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,24 @@ interface AdminPanelProps {
|
||||
export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
const t = useTranslations("admin");
|
||||
const [tab, setTab] = useState<Tab>("requests");
|
||||
|
||||
// Requests state
|
||||
const [requests, setRequests] = useState<TenantRequest[]>([]);
|
||||
const [filter, setFilter] = useState<RequestFilter>("all");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingRequests, setLoadingRequests] = useState(true);
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
||||
const [rejectModal, setRejectModal] = useState<string | null>(null);
|
||||
const [rejectNotes, setRejectNotes] = useState("");
|
||||
|
||||
// Tenants state
|
||||
const [tenants, setTenants] = useState<PiecedTenant[]>(initialTenants);
|
||||
const [loadingTenants, setLoadingTenants] = useState(false);
|
||||
const [deleteModal, setDeleteModal] = useState<string | null>(null);
|
||||
|
||||
// Shared
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// ─── Requests fetching ───
|
||||
const fetchRequests = useCallback(async () => {
|
||||
try {
|
||||
const url =
|
||||
@@ -37,17 +47,39 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoadingRequests(false);
|
||||
}
|
||||
}, [filter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tab === "requests") {
|
||||
setLoading(true);
|
||||
setLoadingRequests(true);
|
||||
fetchRequests();
|
||||
}
|
||||
}, [tab, filter, fetchRequests]);
|
||||
|
||||
// ─── Tenants fetching ───
|
||||
const fetchTenants = useCallback(async () => {
|
||||
setLoadingTenants(true);
|
||||
try {
|
||||
const res = await fetch("/api/tenants");
|
||||
if (!res.ok) throw new Error("Failed to fetch tenants");
|
||||
const data = await res.json();
|
||||
setTenants(data);
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setLoadingTenants(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (tab === "tenants") {
|
||||
fetchTenants();
|
||||
}
|
||||
}, [tab, fetchTenants]);
|
||||
|
||||
// ─── Request actions ───
|
||||
const handleApprove = async (id: string) => {
|
||||
setActionLoading(id);
|
||||
setError("");
|
||||
@@ -90,6 +122,48 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Tenant actions ───
|
||||
const handleSuspend = async (name: string, suspend: boolean) => {
|
||||
setActionLoading(name);
|
||||
setError("");
|
||||
try {
|
||||
const res = await fetch(`/api/admin/tenants/${name}/suspend`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ suspend }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "Suspend failed");
|
||||
}
|
||||
await fetchTenants();
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (name: string) => {
|
||||
setActionLoading(name);
|
||||
setError("");
|
||||
try {
|
||||
const res = await fetch(`/api/admin/tenants/${name}/delete`, {
|
||||
method: "POST",
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "Delete failed");
|
||||
}
|
||||
setDeleteModal(null);
|
||||
await fetchTenants();
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const FILTERS: RequestFilter[] = [
|
||||
"all",
|
||||
"pending",
|
||||
@@ -132,7 +206,7 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
>
|
||||
{t("allTenants")}
|
||||
<span className="ml-1.5 text-xs text-text-muted tabular-nums">
|
||||
{initialTenants.length}
|
||||
{tenants.length}
|
||||
</span>
|
||||
{tab === "tenants" && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent" />
|
||||
@@ -172,7 +246,7 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
{loadingRequests ? (
|
||||
<div className="bg-surface-1 border border-border rounded-xl p-12 text-center">
|
||||
<div className="h-5 w-5 border-2 border-accent border-t-transparent rounded-full animate-spin mx-auto mb-2" />
|
||||
<p className="text-text-muted text-xs">{t("loadingRequests")}</p>
|
||||
@@ -266,7 +340,8 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
</>
|
||||
)}
|
||||
{(req.status === "provisioning" ||
|
||||
req.status === "approved") &&
|
||||
req.status === "approved" ||
|
||||
req.status === "active") &&
|
||||
req.tenantName && (
|
||||
<Link
|
||||
href={`/tenants/${req.tenantName}`}
|
||||
@@ -304,40 +379,41 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
{/* ───── TENANTS TAB ───── */}
|
||||
{tab === "tenants" && (
|
||||
<>
|
||||
{/* Summary cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
|
||||
<SummaryCard
|
||||
label={t("totalTenants")}
|
||||
value={initialTenants.length}
|
||||
value={tenants.length}
|
||||
/>
|
||||
<SummaryCard
|
||||
label={t("running")}
|
||||
value={
|
||||
initialTenants.filter((t) => t.status?.phase === "Running")
|
||||
.length
|
||||
tenants.filter(
|
||||
(t) => t.status?.phase === "Running" || t.status?.phase === "Ready"
|
||||
).length
|
||||
}
|
||||
color="text-emerald-400"
|
||||
/>
|
||||
<SummaryCard
|
||||
label={t("provisioning")}
|
||||
value={
|
||||
initialTenants.filter(
|
||||
(t) =>
|
||||
t.status?.phase === "Provisioning" ||
|
||||
t.status?.phase === "Pending"
|
||||
).length
|
||||
}
|
||||
label={t("suspended")}
|
||||
value={tenants.filter((t) => t.spec.suspend).length}
|
||||
color="text-amber-400"
|
||||
/>
|
||||
<SummaryCard
|
||||
label={t("errors")}
|
||||
value={
|
||||
initialTenants.filter((t) => t.status?.phase === "Error").length
|
||||
tenants.filter((t) => t.status?.phase === "Error").length
|
||||
}
|
||||
color="text-red-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{initialTenants.length === 0 ? (
|
||||
{loadingTenants ? (
|
||||
<div className="bg-surface-1 border border-border rounded-xl p-12 text-center">
|
||||
<div className="h-5 w-5 border-2 border-accent border-t-transparent rounded-full animate-spin mx-auto mb-2" />
|
||||
<p className="text-text-muted text-xs">{t("loadingTenants")}</p>
|
||||
</div>
|
||||
) : tenants.length === 0 ? (
|
||||
<div className="bg-surface-1 border border-border rounded-xl p-12 text-center">
|
||||
<p className="text-text-secondary text-sm">{t("noTenants")}</p>
|
||||
</div>
|
||||
@@ -347,60 +423,94 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-left">
|
||||
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
{t("name")}
|
||||
</th>
|
||||
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
{t("displayName")}
|
||||
</th>
|
||||
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
{t("phase")}
|
||||
</th>
|
||||
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted hidden md:table-cell">
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted hidden md:table-cell">
|
||||
{t("packages")}
|
||||
</th>
|
||||
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted hidden md:table-cell">
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted hidden md:table-cell">
|
||||
{t("created")}
|
||||
</th>
|
||||
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
{t("actions")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{initialTenants.map((tenant) => (
|
||||
{tenants.map((tenant) => (
|
||||
<tr
|
||||
key={tenant.metadata.name}
|
||||
className="border-b border-border last:border-0 hover:bg-surface-2/50 transition-colors"
|
||||
className={`border-b border-border last:border-0 hover:bg-surface-2/50 transition-colors ${
|
||||
tenant.spec.suspend ? "opacity-60" : ""
|
||||
}`}
|
||||
>
|
||||
<td className="px-5 py-3 font-mono text-xs text-accent">
|
||||
<td className="px-4 py-3 font-mono text-xs text-accent">
|
||||
{tenant.metadata.name}
|
||||
</td>
|
||||
<td className="px-5 py-3 text-text-primary">
|
||||
{tenant.spec.displayName}
|
||||
<td className="px-4 py-3 text-text-primary">
|
||||
<span>{tenant.spec.displayName}</span>
|
||||
{tenant.spec.suspend && (
|
||||
<span className="ml-2 px-1.5 py-0.5 text-[10px] font-medium bg-amber-400/15 text-amber-400 rounded">
|
||||
{t("suspendedBadge")}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-5 py-3">
|
||||
<td className="px-4 py-3">
|
||||
<StatusBadge
|
||||
phase={tenant.status?.phase ?? "Pending"}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-5 py-3 text-xs text-text-secondary font-mono hidden md:table-cell">
|
||||
<td className="px-4 py-3 text-xs text-text-secondary font-mono hidden md:table-cell">
|
||||
{tenant.spec.packages?.join(", ") || "—"}
|
||||
</td>
|
||||
<td className="px-5 py-3 text-xs text-text-muted tabular-nums hidden md:table-cell">
|
||||
<td className="px-4 py-3 text-xs text-text-muted tabular-nums hidden md:table-cell">
|
||||
{tenant.metadata.creationTimestamp
|
||||
? new Date(
|
||||
tenant.metadata.creationTimestamp
|
||||
).toLocaleDateString()
|
||||
: "—"}
|
||||
</td>
|
||||
<td className="px-5 py-3">
|
||||
<Link
|
||||
href={`/tenants/${tenant.metadata.name}`}
|
||||
className="px-2.5 py-1 text-xs font-medium bg-accent/15 text-accent rounded-md hover:bg-accent/25 transition-colors"
|
||||
>
|
||||
{t("manage")}
|
||||
</Link>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
<Link
|
||||
href={`/tenants/${tenant.metadata.name}`}
|
||||
className="px-2.5 py-1 text-xs font-medium bg-accent/15 text-accent rounded-md hover:bg-accent/25 transition-colors"
|
||||
>
|
||||
{t("manage")}
|
||||
</Link>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleSuspend(
|
||||
tenant.metadata.name,
|
||||
!tenant.spec.suspend
|
||||
)
|
||||
}
|
||||
disabled={actionLoading === tenant.metadata.name}
|
||||
className="px-2.5 py-1 text-xs font-medium bg-amber-500/15 text-amber-400 rounded-md hover:bg-amber-500/25 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{actionLoading === tenant.metadata.name
|
||||
? "…"
|
||||
: tenant.spec.suspend
|
||||
? t("resume")
|
||||
: t("suspend")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setDeleteModal(tenant.metadata.name)
|
||||
}
|
||||
disabled={actionLoading === tenant.metadata.name}
|
||||
className="px-2.5 py-1 text-xs font-medium bg-red-500/15 text-red-400 rounded-md hover:bg-red-500/25 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{t("deleteTenant")}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -450,6 +560,38 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ───── DELETE MODAL ───── */}
|
||||
{deleteModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="bg-surface-1 border border-border rounded-xl p-6 max-w-md w-full mx-4 shadow-2xl">
|
||||
<h3 className="font-display text-lg font-semibold text-text-primary mb-2">
|
||||
{t("deleteTitle")}
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary mb-2">
|
||||
{t("deleteWarning")}
|
||||
</p>
|
||||
<p className="text-xs font-mono text-accent bg-surface-2 border border-border rounded-lg px-3 py-2 mb-4">
|
||||
{deleteModal}
|
||||
</p>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button
|
||||
onClick={() => setDeleteModal(null)}
|
||||
className="px-4 py-2 text-sm text-text-secondary hover:text-text-primary transition-colors"
|
||||
>
|
||||
{t("cancelAction")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(deleteModal)}
|
||||
disabled={actionLoading === deleteModal}
|
||||
className="px-4 py-2 text-sm font-medium bg-red-500/15 text-red-400 rounded-lg hover:bg-red-500/25 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{actionLoading === deleteModal ? "…" : t("confirmDelete")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,49 +88,6 @@
|
||||
"noInstanceDescription": "Richten Sie Ihre KI-Assistenten-Instanz ein, um mit PieCed IT zu starten.",
|
||||
"manage": "Instanz & Pakete verwalten"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Plattform-Admin",
|
||||
"allTenants": "Tenants",
|
||||
"noTenants": "Noch keine Tenants bereitgestellt.",
|
||||
"noAccess": "Unzureichende Berechtigungen für diese Ansicht.",
|
||||
"name": "Name",
|
||||
"displayName": "Anzeigename",
|
||||
"phase": "Phase",
|
||||
"packages": "Pakete",
|
||||
"created": "Erstellt",
|
||||
"manage": "Verwalten",
|
||||
"pendingRequests": "Offene Anträge",
|
||||
"approve": "Genehmigen",
|
||||
"reject": "Ablehnen",
|
||||
"company": "Firma",
|
||||
"contact": "Kontakt",
|
||||
"status": "Status",
|
||||
"submitted": "Eingereicht",
|
||||
"approveConfirm": "Diesen Antrag genehmigen und Bereitstellung starten?",
|
||||
"rejectConfirm": "Diesen Antrag ablehnen?",
|
||||
"subtitle": "Onboarding-Anfragen und Mandanten-Lebenszyklus verwalten",
|
||||
"requests": "Anfragen",
|
||||
"reApprove": "Erneut genehmigen",
|
||||
"agentName": "Agent",
|
||||
"actions": "Aktionen",
|
||||
"noRequests": "Keine Anfragen gefunden.",
|
||||
"loadingRequests": "Anfragen werden geladen…",
|
||||
"rejectTitle": "Anfrage ablehnen",
|
||||
"adminNotesLabel": "Notizen (optional)",
|
||||
"adminNotesPlaceholder": "Grund der Ablehnung…",
|
||||
"cancelAction": "Abbrechen",
|
||||
"confirmReject": "Ablehnen",
|
||||
"viewTenant": "Anzeigen",
|
||||
"filter_all": "Alle",
|
||||
"filter_pending": "Ausstehend",
|
||||
"filter_provisioning": "Bereitstellung",
|
||||
"filter_approved": "Genehmigt",
|
||||
"filter_rejected": "Abgelehnt",
|
||||
"totalTenants": "Gesamt",
|
||||
"running": "Aktiv",
|
||||
"provisioning": "Bereitstellung",
|
||||
"errors": "Fehler"
|
||||
},
|
||||
"tenantDetail": {
|
||||
"agent": "Agent",
|
||||
"packages": "Pakete",
|
||||
@@ -200,5 +157,57 @@
|
||||
"documentProcessing": {
|
||||
"description": "Aktivieren Sie Dokumentenverarbeitung, Zusammenfassung und Extraktion."
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"title": "Plattform-Admin",
|
||||
"subtitle": "Onboarding-Anfragen und Mandanten-Lebenszyklus verwalten",
|
||||
"allTenants": "Mandanten",
|
||||
"noTenants": "Noch keine Mandanten bereitgestellt.",
|
||||
"noAccess": "Unzureichende Berechtigungen für diese Ansicht.",
|
||||
"name": "Name",
|
||||
"displayName": "Anzeigename",
|
||||
"phase": "Phase",
|
||||
"packages": "Pakete",
|
||||
"created": "Erstellt",
|
||||
"manage": "Verwalten",
|
||||
"requests": "Anfragen",
|
||||
"pendingRequests": "Offene Anträge",
|
||||
"approve": "Genehmigen",
|
||||
"reject": "Ablehnen",
|
||||
"reApprove": "Erneut genehmigen",
|
||||
"company": "Firma",
|
||||
"contact": "Kontakt",
|
||||
"agentName": "Agent",
|
||||
"status": "Status",
|
||||
"submitted": "Eingereicht",
|
||||
"actions": "Aktionen",
|
||||
"noRequests": "Keine Anfragen gefunden.",
|
||||
"loadingRequests": "Anfragen werden geladen…",
|
||||
"approveConfirm": "Diesen Antrag genehmigen und Bereitstellung starten?",
|
||||
"rejectConfirm": "Diesen Antrag ablehnen?",
|
||||
"rejectTitle": "Anfrage ablehnen",
|
||||
"adminNotesLabel": "Notizen (optional)",
|
||||
"adminNotesPlaceholder": "Grund der Ablehnung…",
|
||||
"cancelAction": "Abbrechen",
|
||||
"confirmReject": "Ablehnen",
|
||||
"viewTenant": "Anzeigen",
|
||||
"filter_all": "Alle",
|
||||
"filter_pending": "Ausstehend",
|
||||
"filter_provisioning": "Bereitstellung",
|
||||
"filter_approved": "Genehmigt",
|
||||
"filter_rejected": "Abgelehnt",
|
||||
"totalTenants": "Gesamt",
|
||||
"running": "Aktiv",
|
||||
"provisioning": "Bereitstellung",
|
||||
"errors": "Fehler",
|
||||
"suspend": "Suspendieren",
|
||||
"resume": "Fortsetzen",
|
||||
"suspended": "Suspendiert",
|
||||
"suspendedBadge": "SUSPENDIERT",
|
||||
"deleteTenant": "Löschen",
|
||||
"deleteTitle": "Mandant löschen",
|
||||
"deleteWarning": "Dies löscht den Mandanten, seinen Namespace, Secrets und alle zugehörigen Daten unwiderruflich.",
|
||||
"confirmDelete": "Endgültig löschen",
|
||||
"loadingTenants": "Mandanten werden geladen…"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,49 +88,6 @@
|
||||
"noInstanceDescription": "Set up your AI assistant instance to get started with PieCed IT.",
|
||||
"manage": "Manage instance & packages"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Platform Admin",
|
||||
"subtitle": "Manage onboarding requests and tenant lifecycle",
|
||||
"allTenants": "Tenants",
|
||||
"noTenants": "No tenants provisioned yet.",
|
||||
"noAccess": "Insufficient permissions for this view.",
|
||||
"name": "Name",
|
||||
"displayName": "Display Name",
|
||||
"phase": "Phase",
|
||||
"packages": "Packages",
|
||||
"created": "Created",
|
||||
"manage": "Manage",
|
||||
"requests": "Requests",
|
||||
"pendingRequests": "Pending Requests",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"reApprove": "Re-approve",
|
||||
"company": "Company",
|
||||
"contact": "Contact",
|
||||
"agentName": "Agent",
|
||||
"status": "Status",
|
||||
"submitted": "Submitted",
|
||||
"actions": "Actions",
|
||||
"noRequests": "No requests found.",
|
||||
"loadingRequests": "Loading requests…",
|
||||
"approveConfirm": "Approve this request and start provisioning?",
|
||||
"rejectConfirm": "Reject this request?",
|
||||
"rejectTitle": "Reject request",
|
||||
"adminNotesLabel": "Notes (optional)",
|
||||
"adminNotesPlaceholder": "Reason for rejection…",
|
||||
"cancelAction": "Cancel",
|
||||
"confirmReject": "Reject",
|
||||
"viewTenant": "View",
|
||||
"filter_all": "All",
|
||||
"filter_pending": "Pending",
|
||||
"filter_provisioning": "Provisioning",
|
||||
"filter_approved": "Approved",
|
||||
"filter_rejected": "Rejected",
|
||||
"totalTenants": "Total",
|
||||
"running": "Running",
|
||||
"provisioning": "Provisioning",
|
||||
"errors": "Errors"
|
||||
},
|
||||
"tenantDetail": {
|
||||
"agent": "Agent",
|
||||
"packages": "Packages",
|
||||
@@ -200,5 +157,57 @@
|
||||
"documentProcessing": {
|
||||
"description": "Enable document parsing, summarization, and extraction."
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"title": "Platform Admin",
|
||||
"subtitle": "Manage onboarding requests and tenant lifecycle",
|
||||
"allTenants": "Tenants",
|
||||
"noTenants": "No tenants provisioned yet.",
|
||||
"noAccess": "Insufficient permissions for this view.",
|
||||
"name": "Name",
|
||||
"displayName": "Display Name",
|
||||
"phase": "Phase",
|
||||
"packages": "Packages",
|
||||
"created": "Created",
|
||||
"manage": "Manage",
|
||||
"requests": "Requests",
|
||||
"pendingRequests": "Pending Requests",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"reApprove": "Re-approve",
|
||||
"company": "Company",
|
||||
"contact": "Contact",
|
||||
"agentName": "Agent",
|
||||
"status": "Status",
|
||||
"submitted": "Submitted",
|
||||
"actions": "Actions",
|
||||
"noRequests": "No requests found.",
|
||||
"loadingRequests": "Loading requests…",
|
||||
"approveConfirm": "Approve this request and start provisioning?",
|
||||
"rejectConfirm": "Reject this request?",
|
||||
"rejectTitle": "Reject request",
|
||||
"adminNotesLabel": "Notes (optional)",
|
||||
"adminNotesPlaceholder": "Reason for rejection…",
|
||||
"cancelAction": "Cancel",
|
||||
"confirmReject": "Reject",
|
||||
"viewTenant": "View",
|
||||
"filter_all": "All",
|
||||
"filter_pending": "Pending",
|
||||
"filter_provisioning": "Provisioning",
|
||||
"filter_approved": "Approved",
|
||||
"filter_rejected": "Rejected",
|
||||
"totalTenants": "Total",
|
||||
"running": "Running",
|
||||
"provisioning": "Provisioning",
|
||||
"errors": "Errors",
|
||||
"suspend": "Suspend",
|
||||
"resume": "Resume",
|
||||
"suspended": "Suspended",
|
||||
"suspendedBadge": "SUSPENDED",
|
||||
"deleteTenant": "Delete",
|
||||
"deleteTitle": "Delete tenant",
|
||||
"deleteWarning": "This will permanently delete the tenant, its namespace, secrets, and all associated data. This action cannot be undone.",
|
||||
"confirmDelete": "Delete permanently",
|
||||
"loadingTenants": "Loading tenants…"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,49 +88,6 @@
|
||||
"noInstanceDescription": "Configurez votre instance d'assistant IA pour commencer avec PieCed IT.",
|
||||
"manage": "Gérer l'instance et les paquets"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Admin plateforme",
|
||||
"allTenants": "Tenants",
|
||||
"noTenants": "Aucun tenant provisionné.",
|
||||
"noAccess": "Permissions insuffisantes pour cette vue.",
|
||||
"name": "Nom",
|
||||
"displayName": "Nom d'affichage",
|
||||
"phase": "Phase",
|
||||
"packages": "Paquets",
|
||||
"created": "Créé",
|
||||
"manage": "Gérer",
|
||||
"pendingRequests": "Demandes en attente",
|
||||
"approve": "Approuver",
|
||||
"reject": "Refuser",
|
||||
"company": "Entreprise",
|
||||
"contact": "Contact",
|
||||
"status": "Statut",
|
||||
"submitted": "Envoyé",
|
||||
"approveConfirm": "Approuver cette demande et lancer la mise en service ?",
|
||||
"rejectConfirm": "Refuser cette demande ?",
|
||||
"subtitle": "Gérer les demandes d'intégration et le cycle de vie des locataires",
|
||||
"requests": "Demandes",
|
||||
"reApprove": "Ré-approuver",
|
||||
"agentName": "Agent",
|
||||
"actions": "Actions",
|
||||
"noRequests": "Aucune demande trouvée.",
|
||||
"loadingRequests": "Chargement des demandes…",
|
||||
"rejectTitle": "Rejeter la demande",
|
||||
"adminNotesLabel": "Notes (optionnel)",
|
||||
"adminNotesPlaceholder": "Raison du rejet…",
|
||||
"cancelAction": "Annuler",
|
||||
"confirmReject": "Rejeter",
|
||||
"viewTenant": "Voir",
|
||||
"filter_all": "Tous",
|
||||
"filter_pending": "En attente",
|
||||
"filter_provisioning": "Provisionnement",
|
||||
"filter_approved": "Approuvé",
|
||||
"filter_rejected": "Rejeté",
|
||||
"totalTenants": "Total",
|
||||
"running": "Actif",
|
||||
"provisioning": "Provisionnement",
|
||||
"errors": "Erreurs"
|
||||
},
|
||||
"tenantDetail": {
|
||||
"agent": "Agent",
|
||||
"packages": "Paquets",
|
||||
@@ -200,5 +157,57 @@
|
||||
"documentProcessing": {
|
||||
"description": "Activez l'analyse, le résumé et l'extraction de documents."
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"title": "Admin plateforme",
|
||||
"subtitle": "Gérer les demandes d'intégration et le cycle de vie des locataires",
|
||||
"allTenants": "Locataires",
|
||||
"noTenants": "Aucun locataire provisionné.",
|
||||
"noAccess": "Permissions insuffisantes pour cette vue.",
|
||||
"name": "Nom",
|
||||
"displayName": "Nom d'affichage",
|
||||
"phase": "Phase",
|
||||
"packages": "Paquets",
|
||||
"created": "Créé",
|
||||
"manage": "Gérer",
|
||||
"requests": "Demandes",
|
||||
"pendingRequests": "Demandes en attente",
|
||||
"approve": "Approuver",
|
||||
"reject": "Rejeter",
|
||||
"reApprove": "Ré-approuver",
|
||||
"company": "Entreprise",
|
||||
"contact": "Contact",
|
||||
"agentName": "Agent",
|
||||
"status": "Statut",
|
||||
"submitted": "Soumis",
|
||||
"actions": "Actions",
|
||||
"noRequests": "Aucune demande trouvée.",
|
||||
"loadingRequests": "Chargement des demandes…",
|
||||
"approveConfirm": "Approuver cette demande et lancer le provisionnement ?",
|
||||
"rejectConfirm": "Rejeter cette demande ?",
|
||||
"rejectTitle": "Rejeter la demande",
|
||||
"adminNotesLabel": "Notes (optionnel)",
|
||||
"adminNotesPlaceholder": "Raison du rejet…",
|
||||
"cancelAction": "Annuler",
|
||||
"confirmReject": "Rejeter",
|
||||
"viewTenant": "Voir",
|
||||
"filter_all": "Tous",
|
||||
"filter_pending": "En attente",
|
||||
"filter_provisioning": "Provisionnement",
|
||||
"filter_approved": "Approuvé",
|
||||
"filter_rejected": "Rejeté",
|
||||
"totalTenants": "Total",
|
||||
"running": "Actif",
|
||||
"provisioning": "Provisionnement",
|
||||
"errors": "Erreurs",
|
||||
"suspend": "Suspendre",
|
||||
"resume": "Reprendre",
|
||||
"suspended": "Suspendu",
|
||||
"suspendedBadge": "SUSPENDU",
|
||||
"deleteTenant": "Supprimer",
|
||||
"deleteTitle": "Supprimer le locataire",
|
||||
"deleteWarning": "Cela supprimera définitivement le locataire, son namespace, ses secrets et toutes les données associées. Cette action est irréversible.",
|
||||
"confirmDelete": "Supprimer définitivement",
|
||||
"loadingTenants": "Chargement des locataires…"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,49 +88,6 @@
|
||||
"noInstanceDescription": "Configura la tua istanza di assistente IA per iniziare con PieCed IT.",
|
||||
"manage": "Gestisci istanza e pacchetti"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Admin piattaforma",
|
||||
"allTenants": "Tenant",
|
||||
"noTenants": "Nessun tenant ancora attivato.",
|
||||
"noAccess": "Permessi insufficienti per questa vista.",
|
||||
"name": "Nome",
|
||||
"displayName": "Nome visualizzato",
|
||||
"phase": "Fase",
|
||||
"packages": "Pacchetti",
|
||||
"created": "Creato",
|
||||
"manage": "Gestisci",
|
||||
"pendingRequests": "Richieste in sospeso",
|
||||
"approve": "Approva",
|
||||
"reject": "Rifiuta",
|
||||
"company": "Azienda",
|
||||
"contact": "Contatto",
|
||||
"status": "Stato",
|
||||
"submitted": "Inviato",
|
||||
"approveConfirm": "Approvare questa richiesta e avviare l'attivazione?",
|
||||
"rejectConfirm": "Rifiutare questa richiesta?",
|
||||
"subtitle": "Gestire le richieste di onboarding e il ciclo di vita dei tenant",
|
||||
"requests": "Richieste",
|
||||
"reApprove": "Ri-approva",
|
||||
"agentName": "Agente",
|
||||
"actions": "Azioni",
|
||||
"noRequests": "Nessuna richiesta trovata.",
|
||||
"loadingRequests": "Caricamento richieste…",
|
||||
"rejectTitle": "Rifiuta richiesta",
|
||||
"adminNotesLabel": "Note (opzionale)",
|
||||
"adminNotesPlaceholder": "Motivo del rifiuto…",
|
||||
"cancelAction": "Annulla",
|
||||
"confirmReject": "Rifiuta",
|
||||
"viewTenant": "Vedi",
|
||||
"filter_all": "Tutti",
|
||||
"filter_pending": "In attesa",
|
||||
"filter_provisioning": "Provisioning",
|
||||
"filter_approved": "Approvato",
|
||||
"filter_rejected": "Rifiutato",
|
||||
"totalTenants": "Totale",
|
||||
"running": "Attivo",
|
||||
"provisioning": "Provisioning",
|
||||
"errors": "Errori"
|
||||
},
|
||||
"tenantDetail": {
|
||||
"agent": "Agente",
|
||||
"packages": "Pacchetti",
|
||||
@@ -200,5 +157,57 @@
|
||||
"documentProcessing": {
|
||||
"description": "Attiva analisi, riepilogo ed estrazione di documenti."
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"title": "Admin piattaforma",
|
||||
"subtitle": "Gestire le richieste di onboarding e il ciclo di vita dei tenant",
|
||||
"allTenants": "Tenant",
|
||||
"noTenants": "Nessun tenant provisionato.",
|
||||
"noAccess": "Permessi insufficienti per questa vista.",
|
||||
"name": "Nome",
|
||||
"displayName": "Nome visualizzato",
|
||||
"phase": "Fase",
|
||||
"packages": "Pacchetti",
|
||||
"created": "Creato",
|
||||
"manage": "Gestisci",
|
||||
"requests": "Richieste",
|
||||
"pendingRequests": "Richieste in attesa",
|
||||
"approve": "Approva",
|
||||
"reject": "Rifiuta",
|
||||
"reApprove": "Ri-approva",
|
||||
"company": "Azienda",
|
||||
"contact": "Contatto",
|
||||
"agentName": "Agente",
|
||||
"status": "Stato",
|
||||
"submitted": "Inviato",
|
||||
"actions": "Azioni",
|
||||
"noRequests": "Nessuna richiesta trovata.",
|
||||
"loadingRequests": "Caricamento richieste…",
|
||||
"approveConfirm": "Approvare questa richiesta e avviare il provisioning?",
|
||||
"rejectConfirm": "Rifiutare questa richiesta?",
|
||||
"rejectTitle": "Rifiuta richiesta",
|
||||
"adminNotesLabel": "Note (opzionale)",
|
||||
"adminNotesPlaceholder": "Motivo del rifiuto…",
|
||||
"cancelAction": "Annulla",
|
||||
"confirmReject": "Rifiuta",
|
||||
"viewTenant": "Vedi",
|
||||
"filter_all": "Tutti",
|
||||
"filter_pending": "In attenta",
|
||||
"filter_provisioning": "Provisioning",
|
||||
"filter_approved": "Approvato",
|
||||
"filter_rejected": "Rifiutato",
|
||||
"totalTenants": "Totale",
|
||||
"running": "Attivo",
|
||||
"provisioning": "Provisioning",
|
||||
"errors": "Errori",
|
||||
"suspend": "Sospendi",
|
||||
"resume": "Riprendi",
|
||||
"suspended": "Sospeso",
|
||||
"suspendedBadge": "SOSPESO",
|
||||
"deleteTenant": "Elimina",
|
||||
"deleteTitle": "Elimina tenant",
|
||||
"deleteWarning": "Questo eliminerà permanentemente il tenant, il suo namespace, i secrets e tutti i dati associati. Questa azione non può essere annullata.",
|
||||
"confirmDelete": "Elimina definitivamente",
|
||||
"loadingTenants": "Caricamento tenant…"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,16 @@ export interface PiecedTenantSpec {
|
||||
plan?: string;
|
||||
packages?: string[];
|
||||
workspaceFiles?: Record<string, string>;
|
||||
suspend?: boolean;
|
||||
}
|
||||
|
||||
export interface PiecedTenantStatus {
|
||||
phase: "Pending" | "Provisioning" | "Running" | "Error" | "Deleting";
|
||||
phase: "Pending" | "Provisioning" | "Running" | "Ready" | "Error" | "Deleting";
|
||||
message?: string;
|
||||
observedGeneration?: number;
|
||||
litellmTeamId?: string;
|
||||
tenantNamespace?: string;
|
||||
enabledPackages?: string[];
|
||||
conditions?: Array<{
|
||||
type: string;
|
||||
status: string;
|
||||
@@ -87,11 +90,11 @@ export interface BillingAddress {
|
||||
}
|
||||
|
||||
export type TenantRequestStatus =
|
||||
| "pending" // Submitted, awaiting admin approval
|
||||
| "approved" // Admin approved, provisioning will start
|
||||
| "provisioning" // PiecedTenant CR created, operator reconciling
|
||||
| "active" // Tenant running
|
||||
| "rejected"; // Admin rejected
|
||||
| "pending" // Submitted, awaiting admin approval
|
||||
| "approved" // Admin approved, provisioning will start
|
||||
| "provisioning" // PiecedTenant CR created, operator reconciling
|
||||
| "active" // Tenant running
|
||||
| "rejected"; // Admin rejected
|
||||
|
||||
export interface TenantRequest {
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user