Projektstart
This commit is contained in:
@@ -24,6 +24,10 @@ type Account = {
|
||||
email: string;
|
||||
provider: string;
|
||||
isActive: boolean;
|
||||
hasOauth?: boolean;
|
||||
oauthExpiresAt?: string | null;
|
||||
oauthLastCheckedAt?: string | null;
|
||||
oauthLastErrorCode?: string | null;
|
||||
tenant?: { id: string; name: string } | null;
|
||||
};
|
||||
|
||||
@@ -52,7 +56,8 @@ export default function AdminPanel({ token, onImpersonate }: Props) {
|
||||
const [exportTenantId, setExportTenantId] = useState<string | null>(null);
|
||||
const [exportStatus, setExportStatus] = useState<"idle" | "loading" | "done" | "failed">("idle");
|
||||
const [exportJobId, setExportJobId] = useState<string | null>(null);
|
||||
const [exportHistory, setExportHistory] = useState<{ id: string; status: string; expiresAt?: string | null; createdAt?: string }[]>([]);
|
||||
const [exportHistory, setExportHistory] = useState<{ id: string; status: string; expiresAt?: string | null; createdAt?: string; progress?: number }[]>([]);
|
||||
const [exportFilter, setExportFilter] = useState<"all" | "active" | "done" | "failed" | "expired">("all");
|
||||
const [exportScope, setExportScope] = useState<"all" | "users" | "accounts" | "jobs" | "rules">("all");
|
||||
const [exportFormat, setExportFormat] = useState<"json" | "csv" | "zip">("json");
|
||||
const [tenantSort, setTenantSort] = useState<"recent" | "oldest" | "name">("recent");
|
||||
@@ -72,8 +77,8 @@ export default function AdminPanel({ token, onImpersonate }: Props) {
|
||||
|
||||
const jobsData = await apiFetch("/admin/jobs", {}, token);
|
||||
setJobs(jobsData.jobs ?? []);
|
||||
const exportsData = await apiFetch("/admin/exports", {}, token);
|
||||
setExportHistory(exportsData.exports ?? []);
|
||||
const exportsData = await apiFetch("/admin/exports", {}, token);
|
||||
setExportHistory(exportsData.exports ?? []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -107,7 +112,7 @@ export default function AdminPanel({ token, onImpersonate }: Props) {
|
||||
source.onmessage = async (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
setExportHistory((prev) =>
|
||||
prev.map((item) => (item.id === data.id ? { ...item, status: data.status, expiresAt: data.expiresAt } : item))
|
||||
prev.map((item) => (item.id === data.id ? { ...item, status: data.status, expiresAt: data.expiresAt, progress: data.progress } : item))
|
||||
);
|
||||
if (data.status === "DONE") {
|
||||
const response = await downloadExport(token, result.jobId);
|
||||
@@ -213,6 +218,14 @@ export default function AdminPanel({ token, onImpersonate }: Props) {
|
||||
const usersSorted = sortBy(users, userSort, (u) => u.email);
|
||||
const accountsSorted = sortBy(accounts, accountSort, (a) => a.email);
|
||||
const jobsSorted = sortBy(jobs, jobSort, (j) => j.status);
|
||||
const exportsFiltered = exportHistory.filter((item) => {
|
||||
const expired = item.expiresAt ? new Date(item.expiresAt) < new Date() : false;
|
||||
if (exportFilter === "expired") return expired;
|
||||
if (exportFilter === "active") return item.status === "QUEUED" || item.status === "RUNNING";
|
||||
if (exportFilter === "done") return item.status === "DONE";
|
||||
if (exportFilter === "failed") return item.status === "FAILED";
|
||||
return true;
|
||||
});
|
||||
const mapJobStatus = (status: string) => {
|
||||
switch (status) {
|
||||
case "RUNNING":
|
||||
@@ -317,7 +330,28 @@ export default function AdminPanel({ token, onImpersonate }: Props) {
|
||||
{exportHistory.length > 0 && (
|
||||
<div className="export-history">
|
||||
<h4>{t("exportHistory")}</h4>
|
||||
{exportHistory.map((item) => (
|
||||
<div className="filter-row">
|
||||
<label>
|
||||
{t("adminSortLabel")}
|
||||
<select value={exportFilter} onChange={(event) => setExportFilter(event.target.value as typeof exportFilter)}>
|
||||
<option value="all">{t("adminExportAll")}</option>
|
||||
<option value="active">{t("exportStatusRunning")}</option>
|
||||
<option value="done">{t("exportStatusDone")}</option>
|
||||
<option value="failed">{t("exportStatusFailed")}</option>
|
||||
<option value="expired">{t("exportStatusExpired")}</option>
|
||||
</select>
|
||||
</label>
|
||||
<button
|
||||
className="ghost"
|
||||
onClick={async () => {
|
||||
await apiFetch("/admin/exports/purge", { method: "POST" }, token);
|
||||
loadAll().catch(() => undefined);
|
||||
}}
|
||||
>
|
||||
{t("adminExportPurge")}
|
||||
</button>
|
||||
</div>
|
||||
{exportsFiltered.map((item) => (
|
||||
<div key={item.id} className="list-item">
|
||||
<div>
|
||||
<strong>{item.id.slice(0, 6)}</strong>
|
||||
@@ -332,6 +366,9 @@ export default function AdminPanel({ token, onImpersonate }: Props) {
|
||||
? t("exportStatusDone")
|
||||
: t("exportStatusFailed")}
|
||||
</p>
|
||||
{item.progress !== undefined && (
|
||||
<p>{t("exportProgress", { progress: item.progress })}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="inline-actions">
|
||||
<span>
|
||||
@@ -351,6 +388,15 @@ export default function AdminPanel({ token, onImpersonate }: Props) {
|
||||
>
|
||||
{t("exportDownload")}
|
||||
</button>
|
||||
<button
|
||||
className="ghost"
|
||||
onClick={async () => {
|
||||
await apiFetch(`/admin/exports/${item.id}`, { method: "DELETE" }, token);
|
||||
loadAll().catch(() => undefined);
|
||||
}}
|
||||
>
|
||||
{t("delete")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -442,6 +488,22 @@ export default function AdminPanel({ token, onImpersonate }: Props) {
|
||||
</span>
|
||||
</div>
|
||||
<p>{account.provider} · {account.tenant?.name ?? "-"}</p>
|
||||
{account.hasOauth && (
|
||||
<p className="status-note">
|
||||
{t("statusLabel")}: {account.oauthLastErrorCode ? t("badgeUnhealthy") : t("badgeHealthy")} ·{" "}
|
||||
{t("adminExpiresAt")}: {account.oauthExpiresAt ? new Date(account.oauthExpiresAt).toLocaleString() : t("oauthStatusUnknown")} ·{" "}
|
||||
{t("adminLastConnected")}: {account.oauthLastCheckedAt ? new Date(account.oauthLastCheckedAt).toLocaleString() : t("oauthStatusUnknown")}
|
||||
</p>
|
||||
)}
|
||||
{account.oauthLastErrorCode && (
|
||||
<p className="status-note">
|
||||
{account.oauthLastErrorCode === "invalid_grant"
|
||||
? t("oauthErrorInvalidGrant")
|
||||
: account.oauthLastErrorCode === "token_expired"
|
||||
? t("oauthErrorExpired")
|
||||
: t("oauthErrorUnknown")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button className="ghost" onClick={() => toggleAccount(account)}>
|
||||
{account.isActive ? t("adminDisable") : t("adminEnable")}
|
||||
|
||||
@@ -151,5 +151,7 @@
|
||||
"exportStatusRunning": "Läuft",
|
||||
"exportStatusDone": "Fertig",
|
||||
"exportStatusFailed": "Fehlgeschlagen",
|
||||
"exportStatusExpired": "Abgelaufen"
|
||||
"exportStatusExpired": "Abgelaufen",
|
||||
"adminExportPurge": "Abgelaufene löschen",
|
||||
"exportProgress": "Fortschritt {{progress}}%"
|
||||
}
|
||||
|
||||
@@ -151,5 +151,7 @@
|
||||
"exportStatusRunning": "Running",
|
||||
"exportStatusDone": "Done",
|
||||
"exportStatusFailed": "Failed",
|
||||
"exportStatusExpired": "Expired"
|
||||
"exportStatusExpired": "Expired",
|
||||
"adminExportPurge": "Purge expired",
|
||||
"exportProgress": "Progress {{progress}}%"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user