@burdenoff/microfe-admin 2026.518.2 → 2026.520.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/AdminRoot.d.ts.map +1 -1
- package/dist/admin/AdminRoot.js +54 -46
- package/dist/admin/AdminRoot.js.map +1 -1
- package/dist/admin/AdminRoutes.d.ts.map +1 -1
- package/dist/admin/AdminRoutes.js +22 -18
- package/dist/admin/AdminRoutes.js.map +1 -1
- package/dist/admin/components/ConfirmDialog.js +81 -4
- package/dist/admin/components/ConfirmDialog.js.map +1 -0
- package/dist/admin/components/TenantSsoPanel.d.ts +7 -0
- package/dist/admin/components/TenantSsoPanel.d.ts.map +1 -0
- package/dist/admin/components/TenantSsoPanel.js +286 -0
- package/dist/admin/components/TenantSsoPanel.js.map +1 -0
- package/dist/admin/constants/permissions.d.ts +2 -0
- package/dist/admin/constants/permissions.d.ts.map +1 -1
- package/dist/admin/constants/permissions.js +2 -0
- package/dist/admin/constants/permissions.js.map +1 -1
- package/dist/admin/hooks/useAdminPermissions.d.ts +2 -0
- package/dist/admin/hooks/useAdminPermissions.d.ts.map +1 -1
- package/dist/admin/hooks/useAdminPermissions.js +2 -0
- package/dist/admin/hooks/useAdminPermissions.js.map +1 -1
- package/dist/admin/pages/SyncFailuresPage.d.ts +14 -0
- package/dist/admin/pages/SyncFailuresPage.d.ts.map +1 -0
- package/dist/admin/pages/SyncFailuresPage.js +297 -0
- package/dist/admin/pages/SyncFailuresPage.js.map +1 -0
- package/dist/admin/pages/TenantDetailPage.d.ts.map +1 -1
- package/dist/admin/pages/TenantDetailPage.js +28 -23
- package/dist/admin/pages/TenantDetailPage.js.map +1 -1
- package/dist/admin/pages/index.d.ts +1 -0
- package/dist/admin/pages/index.d.ts.map +1 -1
- package/dist/admin/pages/index.js +1 -0
- package/dist/generated/global-operations.d.ts +252 -0
- package/dist/generated/global-operations.d.ts.map +1 -1
- package/dist/generated/global-operations.js +156 -34
- package/dist/generated/global-operations.js.map +1 -1
- package/dist/generated/global-types.d.ts +566 -1393
- package/dist/generated/global-types.d.ts.map +1 -1
- package/dist/generated/global-types.js +4 -2
- package/dist/generated/global-types.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { useToast as e } from "../components/Toast.js";
|
|
2
|
+
import { useAdminPermissions as t } from "../hooks/useAdminPermissions.js";
|
|
3
|
+
import { Badge as n, Button as r, Card as i, CardContent as a, EmptyState as o } from "../components/ui.js";
|
|
4
|
+
import { PageLayout as s } from "../components/PageLayout.js";
|
|
5
|
+
import { formatDateTime as c, formatRelativeTime as l } from "../utils/formatters.js";
|
|
6
|
+
import { MetricCard as u, MetricsGrid as d } from "../components/MetricCard.js";
|
|
7
|
+
import { DataTable as f, Pagination as ee } from "../components/DataTable.js";
|
|
8
|
+
import { ConfirmDialog as te } from "../components/ConfirmDialog.js";
|
|
9
|
+
import { PermissionGuard as p } from "../components/PermissionGuard.js";
|
|
10
|
+
import "../components/index.js";
|
|
11
|
+
import { useListSyncOutboxEntriesQuery as m, useRetrySyncOutboxEntryMutation as h, useSyncOutboxStatsQuery as g } from "../../generated/global-operations.js";
|
|
12
|
+
import { SyncOutboxStatus as _ } from "../../generated/global-types.js";
|
|
13
|
+
import { useCallback as v, useMemo as y, useState as b } from "react";
|
|
14
|
+
import { AlertOctagon as ne, CheckCircle2 as x, Clock as re, RefreshCw as S, RotateCw as C } from "lucide-react";
|
|
15
|
+
import { Fragment as w, jsx as T, jsxs as E } from "react/jsx-runtime";
|
|
16
|
+
//#region src/admin/pages/SyncFailuresPage.tsx
|
|
17
|
+
var D = 25, O = [
|
|
18
|
+
"PENDING",
|
|
19
|
+
"RETRYING",
|
|
20
|
+
"SUCCESS",
|
|
21
|
+
"PERMANENTLY_FAILED"
|
|
22
|
+
], k = {
|
|
23
|
+
PENDING: _.Pending,
|
|
24
|
+
RETRYING: _.Retrying,
|
|
25
|
+
SUCCESS: _.Success,
|
|
26
|
+
PERMANENTLY_FAILED: _.PermanentlyFailed
|
|
27
|
+
};
|
|
28
|
+
function A(e) {
|
|
29
|
+
let t = String(e);
|
|
30
|
+
return O.includes(t) ? t : "PERMANENTLY_FAILED";
|
|
31
|
+
}
|
|
32
|
+
var j = {
|
|
33
|
+
PENDING: "bg-warning/10 text-warning border-warning/30",
|
|
34
|
+
RETRYING: "bg-warning/10 text-warning border-warning/30",
|
|
35
|
+
SUCCESS: "bg-success/10 text-success border-success/30",
|
|
36
|
+
PERMANENTLY_FAILED: "bg-destructive/10 text-destructive border-destructive/30"
|
|
37
|
+
}, M = {
|
|
38
|
+
PENDING: "Pending",
|
|
39
|
+
RETRYING: "Retrying",
|
|
40
|
+
SUCCESS: "Success",
|
|
41
|
+
PERMANENTLY_FAILED: "Permanently failed"
|
|
42
|
+
};
|
|
43
|
+
function ie({ status: e }) {
|
|
44
|
+
let t = A(e);
|
|
45
|
+
return /* @__PURE__ */ T(n, {
|
|
46
|
+
variant: "outline",
|
|
47
|
+
className: j[t],
|
|
48
|
+
children: M[t]
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function N() {
|
|
52
|
+
let { canViewSyncFailures: n, canManageSyncFailures: _, isLoading: O } = t(), j = e(), [M, N] = b(1), [P, F] = b("PERMANENTLY_FAILED"), [I, L] = b(null), [R, z] = b(null), B = y(() => {
|
|
53
|
+
if (P !== "all") return { statuses: [k[P]] };
|
|
54
|
+
}, [P]), V = O || !n, { data: H, loading: U, error: W, refetch: G } = m({
|
|
55
|
+
variables: {
|
|
56
|
+
filter: B,
|
|
57
|
+
limit: D,
|
|
58
|
+
offset: (M - 1) * D
|
|
59
|
+
},
|
|
60
|
+
fetchPolicy: "cache-and-network",
|
|
61
|
+
skip: V
|
|
62
|
+
}), { data: K, loading: q, refetch: J } = g({
|
|
63
|
+
fetchPolicy: "cache-and-network",
|
|
64
|
+
skip: V
|
|
65
|
+
}), [Y, { loading: X }] = h({
|
|
66
|
+
onCompleted: (e) => {
|
|
67
|
+
let t = e.retrySyncOutboxEntry;
|
|
68
|
+
t.success ? (j.success("Requeued for retry", t.message ?? "The reconciler will pick it up on the next tick."), G(), J()) : j.error("Retry failed", t.message ?? "Could not requeue this entry.");
|
|
69
|
+
},
|
|
70
|
+
onError: (e) => {
|
|
71
|
+
j.error("Retry failed", e.message);
|
|
72
|
+
}
|
|
73
|
+
}), ae = v(() => {
|
|
74
|
+
G(), J();
|
|
75
|
+
}, [G, J]), oe = v(async () => {
|
|
76
|
+
if (I) try {
|
|
77
|
+
await Y({ variables: { id: I } });
|
|
78
|
+
} finally {
|
|
79
|
+
L(null);
|
|
80
|
+
}
|
|
81
|
+
}, [I, Y]), Z = H?.listSyncOutboxEntries.items ?? [], Q = H?.listSyncOutboxEntries.total ?? 0, se = Math.max(1, Math.ceil(Q / D)), $ = K?.syncOutboxStats, ce = y(() => [
|
|
82
|
+
{
|
|
83
|
+
key: "operation",
|
|
84
|
+
header: "Operation",
|
|
85
|
+
width: "180px",
|
|
86
|
+
render: (e) => /* @__PURE__ */ T("code", {
|
|
87
|
+
className: "text-xs bg-muted px-1.5 py-0.5 rounded",
|
|
88
|
+
children: e.operation
|
|
89
|
+
})
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: "workspaceId",
|
|
93
|
+
header: "Workspace",
|
|
94
|
+
render: (e) => /* @__PURE__ */ T("span", {
|
|
95
|
+
className: "font-mono text-xs text-muted-foreground",
|
|
96
|
+
children: e.workspaceId
|
|
97
|
+
})
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
key: "status",
|
|
101
|
+
header: "Status",
|
|
102
|
+
width: "170px",
|
|
103
|
+
render: (e) => /* @__PURE__ */ T(ie, { status: e.status })
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
key: "attempts",
|
|
107
|
+
header: "Attempts",
|
|
108
|
+
width: "100px",
|
|
109
|
+
align: "right",
|
|
110
|
+
render: (e) => /* @__PURE__ */ E("span", {
|
|
111
|
+
className: "text-sm",
|
|
112
|
+
children: [e.attempts, /* @__PURE__ */ E("span", {
|
|
113
|
+
className: "text-muted-foreground",
|
|
114
|
+
children: ["/", e.maxAttempts]
|
|
115
|
+
})]
|
|
116
|
+
})
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
key: "nextRetryAt",
|
|
120
|
+
header: "Next retry",
|
|
121
|
+
width: "160px",
|
|
122
|
+
render: (e) => e.nextRetryAt ? /* @__PURE__ */ T("span", {
|
|
123
|
+
className: "text-sm text-muted-foreground",
|
|
124
|
+
children: l(e.nextRetryAt)
|
|
125
|
+
}) : /* @__PURE__ */ T("span", {
|
|
126
|
+
className: "text-xs text-muted-foreground",
|
|
127
|
+
children: "—"
|
|
128
|
+
})
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
key: "createdAt",
|
|
132
|
+
header: "Created",
|
|
133
|
+
width: "160px",
|
|
134
|
+
render: (e) => /* @__PURE__ */ T("span", {
|
|
135
|
+
className: "text-sm text-muted-foreground",
|
|
136
|
+
children: l(e.createdAt)
|
|
137
|
+
})
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
key: "actions",
|
|
141
|
+
header: "",
|
|
142
|
+
width: "120px",
|
|
143
|
+
align: "right",
|
|
144
|
+
render: (e) => {
|
|
145
|
+
let t = A(e.status) === "PERMANENTLY_FAILED";
|
|
146
|
+
return !_ || !t ? null : /* @__PURE__ */ E(r, {
|
|
147
|
+
variant: "outline",
|
|
148
|
+
size: "sm",
|
|
149
|
+
onClick: (t) => {
|
|
150
|
+
t.stopPropagation(), L(e.id);
|
|
151
|
+
},
|
|
152
|
+
disabled: X,
|
|
153
|
+
children: [/* @__PURE__ */ T(C, { className: "mr-1.5 h-3.5 w-3.5" }), "Retry"]
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
], [_, X]);
|
|
158
|
+
return /* @__PURE__ */ T(p, {
|
|
159
|
+
allowed: n,
|
|
160
|
+
isLoading: O,
|
|
161
|
+
pageTitle: "Sync Failures",
|
|
162
|
+
denyMessage: "Only super-admins can view cross-layer sync failures.",
|
|
163
|
+
children: /* @__PURE__ */ E(s, {
|
|
164
|
+
title: "Sync Failures",
|
|
165
|
+
description: "Cross-layer sync operations (global → workspace) that did not complete. Automatic retries run in the background; use the manual retry once the underlying issue is fixed.",
|
|
166
|
+
actions: /* @__PURE__ */ E(r, {
|
|
167
|
+
variant: "outline",
|
|
168
|
+
onClick: ae,
|
|
169
|
+
disabled: U || q,
|
|
170
|
+
children: [/* @__PURE__ */ T(S, { className: `mr-2 h-4 w-4 ${U ? "animate-spin" : ""}` }), "Refresh"]
|
|
171
|
+
}),
|
|
172
|
+
children: [/* @__PURE__ */ E("div", {
|
|
173
|
+
className: "space-y-6",
|
|
174
|
+
children: [
|
|
175
|
+
/* @__PURE__ */ E(d, { children: [
|
|
176
|
+
/* @__PURE__ */ T(u, {
|
|
177
|
+
title: "Pending",
|
|
178
|
+
value: $?.pending ?? 0,
|
|
179
|
+
icon: /* @__PURE__ */ T(re, { className: "h-5 w-5" }),
|
|
180
|
+
subtitle: "Waiting for first retry",
|
|
181
|
+
loading: q
|
|
182
|
+
}),
|
|
183
|
+
/* @__PURE__ */ T(u, {
|
|
184
|
+
title: "Retrying",
|
|
185
|
+
value: $?.retrying ?? 0,
|
|
186
|
+
icon: /* @__PURE__ */ T(C, { className: "h-5 w-5" }),
|
|
187
|
+
subtitle: "Automatic backoff in progress",
|
|
188
|
+
loading: q
|
|
189
|
+
}),
|
|
190
|
+
/* @__PURE__ */ T(u, {
|
|
191
|
+
title: "Permanently failed",
|
|
192
|
+
value: $?.permanentlyFailed ?? 0,
|
|
193
|
+
icon: /* @__PURE__ */ T(ne, { className: "h-5 w-5" }),
|
|
194
|
+
subtitle: "Exhausted automatic retries",
|
|
195
|
+
priority: $?.permanentlyFailed ? "critical" : "normal",
|
|
196
|
+
loading: q
|
|
197
|
+
}),
|
|
198
|
+
/* @__PURE__ */ T(u, {
|
|
199
|
+
title: "Successful",
|
|
200
|
+
value: $?.successful ?? 0,
|
|
201
|
+
icon: /* @__PURE__ */ T(x, { className: "h-5 w-5" }),
|
|
202
|
+
subtitle: "Synced (auto-purged after 7 days)",
|
|
203
|
+
loading: q
|
|
204
|
+
})
|
|
205
|
+
] }),
|
|
206
|
+
/* @__PURE__ */ T("div", {
|
|
207
|
+
className: "flex flex-wrap items-center gap-2",
|
|
208
|
+
children: [
|
|
209
|
+
["PERMANENTLY_FAILED", "Permanently failed"],
|
|
210
|
+
["RETRYING", "Retrying"],
|
|
211
|
+
["PENDING", "Pending"],
|
|
212
|
+
["SUCCESS", "Successful"],
|
|
213
|
+
["all", "All"]
|
|
214
|
+
].map(([e, t]) => /* @__PURE__ */ T(r, {
|
|
215
|
+
variant: P === e ? "default" : "outline",
|
|
216
|
+
size: "sm",
|
|
217
|
+
onClick: () => {
|
|
218
|
+
F(e), N(1), z(null);
|
|
219
|
+
},
|
|
220
|
+
children: t
|
|
221
|
+
}, e))
|
|
222
|
+
}),
|
|
223
|
+
W && /* @__PURE__ */ T(i, { children: /* @__PURE__ */ E(a, {
|
|
224
|
+
className: "py-4 text-sm text-destructive",
|
|
225
|
+
children: ["Failed to load sync outbox entries: ", W.message]
|
|
226
|
+
}) }),
|
|
227
|
+
/* @__PURE__ */ T(f, {
|
|
228
|
+
columns: ce,
|
|
229
|
+
data: Z,
|
|
230
|
+
keyExtractor: (e) => e.id,
|
|
231
|
+
loading: U,
|
|
232
|
+
onRowClick: (e) => z((t) => t === e.id ? null : e.id),
|
|
233
|
+
emptyState: /* @__PURE__ */ T(o, {
|
|
234
|
+
icon: /* @__PURE__ */ T(x, { className: "h-8 w-8" }),
|
|
235
|
+
title: "No sync failures",
|
|
236
|
+
description: P === "PERMANENTLY_FAILED" ? "Nothing has exhausted automatic retries." : "No outbox entries match the current filter."
|
|
237
|
+
})
|
|
238
|
+
}),
|
|
239
|
+
R && Z.filter((e) => e.id === R).map((e) => /* @__PURE__ */ T(i, { children: /* @__PURE__ */ E(a, {
|
|
240
|
+
className: "space-y-2 py-4 text-sm",
|
|
241
|
+
children: [
|
|
242
|
+
/* @__PURE__ */ E("div", {
|
|
243
|
+
className: "flex items-center justify-between",
|
|
244
|
+
children: [/* @__PURE__ */ T("span", {
|
|
245
|
+
className: "font-medium",
|
|
246
|
+
children: "Last error"
|
|
247
|
+
}), /* @__PURE__ */ T("span", {
|
|
248
|
+
className: "text-xs text-muted-foreground",
|
|
249
|
+
children: e.lastAttemptAt ? `Last attempt: ${c(e.lastAttemptAt)}` : "Never attempted"
|
|
250
|
+
})]
|
|
251
|
+
}),
|
|
252
|
+
/* @__PURE__ */ T("pre", {
|
|
253
|
+
className: "whitespace-pre-wrap break-words rounded bg-muted px-3 py-2 text-xs text-muted-foreground",
|
|
254
|
+
children: e.lastError ?? "No error recorded"
|
|
255
|
+
}),
|
|
256
|
+
/* @__PURE__ */ E("div", {
|
|
257
|
+
className: "text-xs text-muted-foreground",
|
|
258
|
+
children: ["Entry id: ", /* @__PURE__ */ T("code", {
|
|
259
|
+
className: "font-mono",
|
|
260
|
+
children: e.id
|
|
261
|
+
})]
|
|
262
|
+
})
|
|
263
|
+
]
|
|
264
|
+
}) }, e.id)),
|
|
265
|
+
Q > D && /* @__PURE__ */ T(ee, {
|
|
266
|
+
page: M,
|
|
267
|
+
totalPages: se,
|
|
268
|
+
totalItems: Q,
|
|
269
|
+
pageSize: D,
|
|
270
|
+
onPageChange: (e) => {
|
|
271
|
+
N(e), z(null);
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
]
|
|
275
|
+
}), /* @__PURE__ */ T(te, {
|
|
276
|
+
open: !!I,
|
|
277
|
+
onClose: () => L(null),
|
|
278
|
+
onConfirm: oe,
|
|
279
|
+
title: "Retry this sync?",
|
|
280
|
+
description: /* @__PURE__ */ E(w, { children: [
|
|
281
|
+
"The entry will be flipped to ",
|
|
282
|
+
/* @__PURE__ */ T("strong", { children: "RETRYING" }),
|
|
283
|
+
" and the background reconciler will pick it up on its next tick. Attempts will reset to 0 so the entry gets a fresh retry budget.",
|
|
284
|
+
/* @__PURE__ */ T("br", {}),
|
|
285
|
+
/* @__PURE__ */ T("br", {}),
|
|
286
|
+
"Make sure the underlying issue (e.g. a downstream service that was down) is actually resolved — otherwise it will fail again."
|
|
287
|
+
] }),
|
|
288
|
+
confirmLabel: X ? "Requeueing…" : "Retry",
|
|
289
|
+
loading: X
|
|
290
|
+
})]
|
|
291
|
+
})
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
//#endregion
|
|
295
|
+
export { N as SyncFailuresPage };
|
|
296
|
+
|
|
297
|
+
//# sourceMappingURL=SyncFailuresPage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SyncFailuresPage.js","names":[],"sources":["../../../src/admin/pages/SyncFailuresPage.tsx"],"sourcesContent":["import { useCallback, useMemo, useState } from 'react';\nimport { RefreshCw, AlertOctagon, Clock, CheckCircle2, RotateCw } from 'lucide-react';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n MetricCard,\n MetricsGrid,\n Card,\n CardContent,\n Badge,\n Button,\n DataTable,\n Pagination,\n EmptyState,\n PermissionGuard,\n ConfirmDialog,\n useToast,\n} from '../components';\nimport { formatDateTime, formatRelativeTime } from '../utils/formatters';\nimport { SyncOutboxStatus } from '../../generated/global-types';\nimport {\n useListSyncOutboxEntriesQuery,\n useSyncOutboxStatsQuery,\n useRetrySyncOutboxEntryMutation,\n type ListSyncOutboxEntriesQuery,\n} from '../../generated/global-operations';\n\nconst PAGE_SIZE = 25;\n\n// Single source of truth for the underlying string values. Authored as a\n// const-tuple so TypeScript derives both the union type and a runtime guard.\n// The mapping below is type-checked: if codegen ever drops or renames a\n// SyncOutboxStatus enum member, the build fails at STATUS_TO_GQL, not at\n// runtime.\nconst STATUS_VALUES = ['PENDING', 'RETRYING', 'SUCCESS', 'PERMANENTLY_FAILED'] as const;\ntype StatusValue = (typeof STATUS_VALUES)[number];\ntype StatusFilter = 'all' | StatusValue;\n\ntype OutboxEntry = ListSyncOutboxEntriesQuery['listSyncOutboxEntries']['items'][number];\n\n// Typed bridge between our boundary string union and the codegen-generated\n// enum. Explicit mapping makes the codegen casing dependency a compile-time\n// constraint instead of a runtime cast.\nconst STATUS_TO_GQL: Record<StatusValue, SyncOutboxStatus> = {\n PENDING: SyncOutboxStatus.Pending,\n RETRYING: SyncOutboxStatus.Retrying,\n SUCCESS: SyncOutboxStatus.Success,\n PERMANENTLY_FAILED: SyncOutboxStatus.PermanentlyFailed,\n};\n\n// Reverse map: codegen enum value → boundary string. Falls back to\n// PERMANENTLY_FAILED if codegen ever emits an unknown value — a loud-but-safe\n// default that surfaces the drift without breaking the page.\nfunction asStatusValue(s: SyncOutboxStatus): StatusValue {\n const raw = String(s) as StatusValue;\n return (STATUS_VALUES as readonly string[]).includes(raw) ? raw : 'PERMANENTLY_FAILED';\n}\n\nconst STATUS_VARIANTS: Record<StatusValue, string> = {\n PENDING: 'bg-warning/10 text-warning border-warning/30',\n RETRYING: 'bg-warning/10 text-warning border-warning/30',\n SUCCESS: 'bg-success/10 text-success border-success/30',\n PERMANENTLY_FAILED: 'bg-destructive/10 text-destructive border-destructive/30',\n};\n\nconst STATUS_LABELS: Record<StatusValue, string> = {\n PENDING: 'Pending',\n RETRYING: 'Retrying',\n SUCCESS: 'Success',\n PERMANENTLY_FAILED: 'Permanently failed',\n};\n\nfunction StatusBadge({ status }: { status: SyncOutboxStatus }) {\n const key = asStatusValue(status);\n return (\n <Badge variant=\"outline\" className={STATUS_VARIANTS[key]}>\n {STATUS_LABELS[key]}\n </Badge>\n );\n}\n\n/**\n * Sync Failures admin page.\n *\n * Shows the state of the cross-layer sync outbox so a super-admin can spot\n * users/workspaces that didn't get fully provisioned (e.g. signup happened\n * while a downstream service was down) and manually requeue them once the\n * underlying problem is fixed.\n *\n * Manual retry only flips the row to RETRYING; the background reconciler in\n * global-tenant-svc actually performs the sync on its next tick. That keeps\n * the existing single-writer retry semantics intact.\n */\nexport function SyncFailuresPage() {\n const {\n canViewSyncFailures,\n canManageSyncFailures,\n isLoading: permissionsLoading,\n } = useAdminPermissions();\n const toast = useToast();\n\n const [page, setPage] = useState(1);\n const [statusFilter, setStatusFilter] = useState<StatusFilter>('PERMANENTLY_FAILED');\n const [confirmRetryId, setConfirmRetryId] = useState<string | null>(null);\n const [expandedErrorId, setExpandedErrorId] = useState<string | null>(null);\n\n const filter = useMemo(() => {\n if (statusFilter === 'all') return undefined;\n // Explicit, type-checked mapping — replaces the previous `as unknown as`\n // double-cast. If codegen ever renames a SyncOutboxStatus enum member,\n // the build fails at STATUS_TO_GQL above, not silently at runtime.\n return { statuses: [STATUS_TO_GQL[statusFilter]] };\n }, [statusFilter]);\n\n const skipQueries = permissionsLoading || !canViewSyncFailures;\n\n const {\n data: listData,\n loading: listLoading,\n error: listError,\n refetch: refetchList,\n } = useListSyncOutboxEntriesQuery({\n variables: {\n filter,\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const {\n data: statsData,\n loading: statsLoading,\n refetch: refetchStats,\n } = useSyncOutboxStatsQuery({\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const [retryEntry, { loading: retrying }] = useRetrySyncOutboxEntryMutation({\n onCompleted: (data) => {\n const result = data.retrySyncOutboxEntry;\n if (result.success) {\n toast.success(\n 'Requeued for retry',\n result.message ?? 'The reconciler will pick it up on the next tick.'\n );\n void refetchList();\n void refetchStats();\n } else {\n toast.error('Retry failed', result.message ?? 'Could not requeue this entry.');\n }\n },\n onError: (err) => {\n toast.error('Retry failed', err.message);\n },\n });\n\n const handleRefresh = useCallback(() => {\n void refetchList();\n void refetchStats();\n }, [refetchList, refetchStats]);\n\n const handleConfirmRetry = useCallback(async () => {\n if (!confirmRetryId) return;\n // Apollo's mutate promise rejects on network/GraphQL error; without the\n // finally the dialog would stay open and ConfirmDialog's awaited\n // onConfirm would see an unhandled rejection. The onError toast still\n // surfaces the failure to the user.\n try {\n await retryEntry({ variables: { id: confirmRetryId } });\n } finally {\n setConfirmRetryId(null);\n }\n }, [confirmRetryId, retryEntry]);\n\n const entries = listData?.listSyncOutboxEntries.items ?? [];\n const total = listData?.listSyncOutboxEntries.total ?? 0;\n const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));\n const stats = statsData?.syncOutboxStats;\n\n const columns = useMemo(\n () => [\n {\n key: 'operation',\n header: 'Operation',\n width: '180px',\n render: (entry: OutboxEntry) => (\n <code className=\"text-xs bg-muted px-1.5 py-0.5 rounded\">{entry.operation}</code>\n ),\n },\n {\n key: 'workspaceId',\n header: 'Workspace',\n render: (entry: OutboxEntry) => (\n <span className=\"font-mono text-xs text-muted-foreground\">{entry.workspaceId}</span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n width: '170px',\n render: (entry: OutboxEntry) => <StatusBadge status={entry.status} />,\n },\n {\n key: 'attempts',\n header: 'Attempts',\n width: '100px',\n align: 'right' as const,\n render: (entry: OutboxEntry) => (\n <span className=\"text-sm\">\n {entry.attempts}\n <span className=\"text-muted-foreground\">/{entry.maxAttempts}</span>\n </span>\n ),\n },\n {\n key: 'nextRetryAt',\n header: 'Next retry',\n width: '160px',\n render: (entry: OutboxEntry) =>\n entry.nextRetryAt ? (\n <span className=\"text-sm text-muted-foreground\">\n {formatRelativeTime(entry.nextRetryAt)}\n </span>\n ) : (\n <span className=\"text-xs text-muted-foreground\">—</span>\n ),\n },\n {\n key: 'createdAt',\n header: 'Created',\n width: '160px',\n render: (entry: OutboxEntry) => (\n <span className=\"text-sm text-muted-foreground\">\n {formatRelativeTime(entry.createdAt)}\n </span>\n ),\n },\n {\n key: 'actions',\n header: '',\n width: '120px',\n align: 'right' as const,\n render: (entry: OutboxEntry) => {\n // Retry is only meaningful for entries that have exhausted automatic\n // retries. PENDING/RETRYING entries already get picked up by the\n // background reconciler. SUCCESS entries don't need retrying.\n const isRetryable = asStatusValue(entry.status) === 'PERMANENTLY_FAILED';\n if (!canManageSyncFailures || !isRetryable) {\n return null;\n }\n return (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={(e) => {\n e.stopPropagation();\n setConfirmRetryId(entry.id);\n }}\n disabled={retrying}\n >\n <RotateCw className=\"mr-1.5 h-3.5 w-3.5\" />\n Retry\n </Button>\n );\n },\n },\n ],\n [canManageSyncFailures, retrying]\n );\n\n return (\n <PermissionGuard\n allowed={canViewSyncFailures}\n isLoading={permissionsLoading}\n pageTitle=\"Sync Failures\"\n denyMessage=\"Only super-admins can view cross-layer sync failures.\"\n >\n <PageLayout\n title=\"Sync Failures\"\n description=\"Cross-layer sync operations (global → workspace) that did not complete. Automatic retries run in the background; use the manual retry once the underlying issue is fixed.\"\n actions={\n <Button variant=\"outline\" onClick={handleRefresh} disabled={listLoading || statsLoading}>\n <RefreshCw className={`mr-2 h-4 w-4 ${listLoading ? 'animate-spin' : ''}`} />\n Refresh\n </Button>\n }\n >\n <div className=\"space-y-6\">\n {/* Stats */}\n <MetricsGrid>\n <MetricCard\n title=\"Pending\"\n value={stats?.pending ?? 0}\n icon={<Clock className=\"h-5 w-5\" />}\n subtitle=\"Waiting for first retry\"\n loading={statsLoading}\n />\n <MetricCard\n title=\"Retrying\"\n value={stats?.retrying ?? 0}\n icon={<RotateCw className=\"h-5 w-5\" />}\n subtitle=\"Automatic backoff in progress\"\n loading={statsLoading}\n />\n <MetricCard\n title=\"Permanently failed\"\n value={stats?.permanentlyFailed ?? 0}\n icon={<AlertOctagon className=\"h-5 w-5\" />}\n subtitle=\"Exhausted automatic retries\"\n priority={stats?.permanentlyFailed ? 'critical' : 'normal'}\n loading={statsLoading}\n />\n <MetricCard\n title=\"Successful\"\n value={stats?.successful ?? 0}\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n subtitle=\"Synced (auto-purged after 7 days)\"\n loading={statsLoading}\n />\n </MetricsGrid>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-2\">\n {(\n [\n ['PERMANENTLY_FAILED', 'Permanently failed'],\n ['RETRYING', 'Retrying'],\n ['PENDING', 'Pending'],\n ['SUCCESS', 'Successful'],\n ['all', 'All'],\n ] as Array<[StatusFilter, string]>\n ).map(([value, label]) => (\n <Button\n key={value}\n variant={statusFilter === value ? 'default' : 'outline'}\n size=\"sm\"\n onClick={() => {\n setStatusFilter(value);\n setPage(1);\n // Clear any expanded error detail card — the row it referenced\n // may not be in the new result set.\n setExpandedErrorId(null);\n }}\n >\n {label}\n </Button>\n ))}\n </div>\n\n {/* Error banner */}\n {listError && (\n <Card>\n <CardContent className=\"py-4 text-sm text-destructive\">\n Failed to load sync outbox entries: {listError.message}\n </CardContent>\n </Card>\n )}\n\n {/* Table */}\n <DataTable\n columns={columns}\n data={entries}\n keyExtractor={(entry) => entry.id}\n loading={listLoading}\n onRowClick={(entry: OutboxEntry) =>\n setExpandedErrorId((current) => (current === entry.id ? null : entry.id))\n }\n emptyState={\n <EmptyState\n icon={<CheckCircle2 className=\"h-8 w-8\" />}\n title=\"No sync failures\"\n description={\n statusFilter === 'PERMANENTLY_FAILED'\n ? 'Nothing has exhausted automatic retries.'\n : 'No outbox entries match the current filter.'\n }\n />\n }\n />\n\n {/* Expanded error detail */}\n {expandedErrorId &&\n entries\n .filter((e: OutboxEntry) => e.id === expandedErrorId)\n .map((entry: OutboxEntry) => (\n <Card key={entry.id}>\n <CardContent className=\"space-y-2 py-4 text-sm\">\n <div className=\"flex items-center justify-between\">\n <span className=\"font-medium\">Last error</span>\n <span className=\"text-xs text-muted-foreground\">\n {entry.lastAttemptAt\n ? `Last attempt: ${formatDateTime(entry.lastAttemptAt)}`\n : 'Never attempted'}\n </span>\n </div>\n <pre className=\"whitespace-pre-wrap break-words rounded bg-muted px-3 py-2 text-xs text-muted-foreground\">\n {entry.lastError ?? 'No error recorded'}\n </pre>\n <div className=\"text-xs text-muted-foreground\">\n Entry id: <code className=\"font-mono\">{entry.id}</code>\n </div>\n </CardContent>\n </Card>\n ))}\n\n {/* Pagination */}\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={(p) => {\n setPage(p);\n // Same rationale as the filter buttons — the previously\n // expanded row may not be in the new page's result set.\n setExpandedErrorId(null);\n }}\n />\n )}\n </div>\n\n {/* Retry confirm */}\n <ConfirmDialog\n open={!!confirmRetryId}\n onClose={() => setConfirmRetryId(null)}\n onConfirm={handleConfirmRetry}\n title=\"Retry this sync?\"\n description={\n <>\n The entry will be flipped to <strong>RETRYING</strong> and the background reconciler\n will pick it up on its next tick. Attempts will reset to 0 so the entry gets a fresh\n retry budget.\n <br />\n <br />\n Make sure the underlying issue (e.g. a downstream service that was down) is actually\n resolved — otherwise it will fail again.\n </>\n }\n confirmLabel={retrying ? 'Requeueing…' : 'Retry'}\n loading={retrying}\n />\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TenantDetailPage.d.ts","sourceRoot":"","sources":["../../../src/admin/pages/TenantDetailPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"TenantDetailPage.d.ts","sourceRoot":"","sources":["../../../src/admin/pages/TenantDetailPage.tsx"],"names":[],"mappings":"AA2CA;;GAEG;AACH,wBAAgB,gBAAgB,4CA0X/B"}
|
|
@@ -9,25 +9,26 @@ import { tWithFallback as p } from "../utils/i18n.js";
|
|
|
9
9
|
import { PermissionGuard as m } from "../components/PermissionGuard.js";
|
|
10
10
|
import "../components/index.js";
|
|
11
11
|
import { joinPath as h } from "../utils/navigation.js";
|
|
12
|
-
import { mapTenantToUi as
|
|
13
|
-
import { useActivateTenantMutation as
|
|
14
|
-
import { emitTenantLifecycle as
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
12
|
+
import { mapTenantToUi as ee } from "../utils/mappers.js";
|
|
13
|
+
import { useActivateTenantMutation as g, useGetTenantQuery as _, useSuspendTenantMutation as v } from "../../generated/global-operations.js";
|
|
14
|
+
import { emitTenantLifecycle as y } from "../utils/tenantEvents.js";
|
|
15
|
+
import { TenantSsoPanel as b } from "../components/TenantSsoPanel.js";
|
|
16
|
+
import { ArrowLeft as x, Ban as te, Building2 as S, Calendar as C, CheckCircle as w, Globe as T, Loader2 as E, Mail as D, Settings as O, Users as k } from "lucide-react";
|
|
17
|
+
import { useNavigate as ne, useParams as A } from "react-router-dom";
|
|
17
18
|
import { jsx as j, jsxs as M } from "react/jsx-runtime";
|
|
18
19
|
import { useI18n as N } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
19
20
|
import { useEventBus as P } from "@burdenoff/fe-libs/shared/events";
|
|
20
21
|
//#region src/admin/pages/TenantDetailPage.tsx
|
|
21
22
|
function F() {
|
|
22
|
-
let { canViewTenants: F, canManageTenants: I, isLoading: L } = n(), { tenantId: R } =
|
|
23
|
+
let { canViewTenants: F, canManageTenants: I, isLoading: L } = n(), { tenantId: R } = A(), z = ne(), { basePath: B = "" } = t(), V = e(), { t: H } = N(), U = (e, t, n) => p(H, e, t, n), { data: W, loading: G, error: K } = _({
|
|
23
24
|
variables: { id: R },
|
|
24
25
|
skip: !R || L || !F,
|
|
25
26
|
fetchPolicy: "cache-and-network"
|
|
26
|
-
}), [q, { loading: J }] =
|
|
27
|
+
}), [q, { loading: J }] = v(), [Y, { loading: X }] = g(), Z = P(), Q = W?.tenant ? ee(W.tenant) : null, re = async () => {
|
|
27
28
|
if (!R) return;
|
|
28
29
|
let e = Q?.slug;
|
|
29
30
|
try {
|
|
30
|
-
await q({ variables: { id: R } }),
|
|
31
|
+
await q({ variables: { id: R } }), y(Z, "tenant.tenant.suspended", {
|
|
31
32
|
tenantId: R,
|
|
32
33
|
tenantSlug: e,
|
|
33
34
|
status: "SUSPENDED"
|
|
@@ -35,11 +36,11 @@ function F() {
|
|
|
35
36
|
} catch (e) {
|
|
36
37
|
V.error(U("admin.tenantDetail.toast.errorTitle", "Error"), e instanceof Error ? e.message : U("admin.tenantDetail.toast.suspendFailed", "Failed to suspend tenant."));
|
|
37
38
|
}
|
|
38
|
-
},
|
|
39
|
+
}, ie = async () => {
|
|
39
40
|
if (!R) return;
|
|
40
41
|
let e = Q?.slug;
|
|
41
42
|
try {
|
|
42
|
-
await Y({ variables: { id: R } }),
|
|
43
|
+
await Y({ variables: { id: R } }), y(Z, "tenant.tenant.activated", {
|
|
43
44
|
tenantId: R,
|
|
44
45
|
tenantSlug: e,
|
|
45
46
|
status: "ACTIVE"
|
|
@@ -57,7 +58,7 @@ function F() {
|
|
|
57
58
|
title: U("admin.tenantDetail.loadingTitle", "Loading..."),
|
|
58
59
|
children: /* @__PURE__ */ j("div", {
|
|
59
60
|
className: "flex h-64 items-center justify-center",
|
|
60
|
-
children: /* @__PURE__ */ j(
|
|
61
|
+
children: /* @__PURE__ */ j(E, { className: "h-8 w-8 animate-spin text-muted-foreground" })
|
|
61
62
|
})
|
|
62
63
|
}) : K || !Q ? /* @__PURE__ */ M(u, {
|
|
63
64
|
title: U("admin.tenantDetail.notFoundTitle", "Tenant Not Found"),
|
|
@@ -84,20 +85,20 @@ function F() {
|
|
|
84
85
|
I && /* @__PURE__ */ M(i, {
|
|
85
86
|
variant: "outline",
|
|
86
87
|
onClick: () => z(h(B, `tenants/${Q.id}/edit`)),
|
|
87
|
-
children: [/* @__PURE__ */ j(
|
|
88
|
+
children: [/* @__PURE__ */ j(O, { className: "mr-2 h-4 w-4" }), U("admin.tenantDetail.edit", "Edit")]
|
|
88
89
|
}),
|
|
89
90
|
I && (Q.status === "active" ? /* @__PURE__ */ M(i, {
|
|
90
91
|
variant: "outline",
|
|
91
92
|
className: "text-warning",
|
|
92
|
-
onClick:
|
|
93
|
+
onClick: re,
|
|
93
94
|
disabled: J,
|
|
94
|
-
children: [J ? /* @__PURE__ */ j(
|
|
95
|
+
children: [J ? /* @__PURE__ */ j(E, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ j(te, { className: "mr-2 h-4 w-4" }), U("admin.tenantDetail.suspend", "Suspend")]
|
|
95
96
|
}) : /* @__PURE__ */ M(i, {
|
|
96
97
|
variant: "outline",
|
|
97
98
|
className: "text-success",
|
|
98
|
-
onClick:
|
|
99
|
+
onClick: ie,
|
|
99
100
|
disabled: X,
|
|
100
|
-
children: [X ? /* @__PURE__ */ j(
|
|
101
|
+
children: [X ? /* @__PURE__ */ j(E, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ j(w, { className: "mr-2 h-4 w-4" }), U("admin.tenantDetail.activate", "Activate")]
|
|
101
102
|
}))
|
|
102
103
|
]
|
|
103
104
|
}),
|
|
@@ -138,7 +139,7 @@ function F() {
|
|
|
138
139
|
children: [
|
|
139
140
|
Q.ownerEmail && /* @__PURE__ */ M("div", {
|
|
140
141
|
className: "flex items-center gap-3",
|
|
141
|
-
children: [/* @__PURE__ */ j(
|
|
142
|
+
children: [/* @__PURE__ */ j(D, { className: "h-4 w-4 text-muted-foreground" }), /* @__PURE__ */ M("div", { children: [/* @__PURE__ */ j("p", {
|
|
142
143
|
className: "text-xs text-muted-foreground",
|
|
143
144
|
children: U("admin.tenantDetail.overview.ownerEmail", "Owner Email")
|
|
144
145
|
}), /* @__PURE__ */ j("p", {
|
|
@@ -148,7 +149,7 @@ function F() {
|
|
|
148
149
|
}),
|
|
149
150
|
Q.domain && /* @__PURE__ */ M("div", {
|
|
150
151
|
className: "flex items-center gap-3",
|
|
151
|
-
children: [/* @__PURE__ */ j(
|
|
152
|
+
children: [/* @__PURE__ */ j(T, { className: "h-4 w-4 text-muted-foreground" }), /* @__PURE__ */ M("div", { children: [/* @__PURE__ */ j("p", {
|
|
152
153
|
className: "text-xs text-muted-foreground",
|
|
153
154
|
children: U("admin.tenantDetail.overview.domain", "Domain")
|
|
154
155
|
}), /* @__PURE__ */ j("p", {
|
|
@@ -158,7 +159,7 @@ function F() {
|
|
|
158
159
|
}),
|
|
159
160
|
/* @__PURE__ */ M("div", {
|
|
160
161
|
className: "flex items-center gap-3",
|
|
161
|
-
children: [/* @__PURE__ */ j(
|
|
162
|
+
children: [/* @__PURE__ */ j(C, { className: "h-4 w-4 text-muted-foreground" }), /* @__PURE__ */ M("div", { children: [/* @__PURE__ */ j("p", {
|
|
162
163
|
className: "text-xs text-muted-foreground",
|
|
163
164
|
children: U("admin.tenantDetail.overview.created", "Created")
|
|
164
165
|
}), /* @__PURE__ */ j("p", {
|
|
@@ -168,7 +169,7 @@ function F() {
|
|
|
168
169
|
}),
|
|
169
170
|
/* @__PURE__ */ M("div", {
|
|
170
171
|
className: "flex items-center gap-3",
|
|
171
|
-
children: [/* @__PURE__ */ j(
|
|
172
|
+
children: [/* @__PURE__ */ j(C, { className: "h-4 w-4 text-muted-foreground" }), /* @__PURE__ */ M("div", { children: [/* @__PURE__ */ j("p", {
|
|
172
173
|
className: "text-xs text-muted-foreground",
|
|
173
174
|
children: U("admin.tenantDetail.overview.lastUpdated", "Last Updated")
|
|
174
175
|
}), /* @__PURE__ */ j("p", {
|
|
@@ -242,7 +243,11 @@ function F() {
|
|
|
242
243
|
})]
|
|
243
244
|
})
|
|
244
245
|
]
|
|
245
|
-
}) })] })
|
|
246
|
+
}) })] }),
|
|
247
|
+
/* @__PURE__ */ j(b, {
|
|
248
|
+
tenantId: Q.id,
|
|
249
|
+
canManage: I
|
|
250
|
+
})
|
|
246
251
|
]
|
|
247
252
|
}), /* @__PURE__ */ j("div", {
|
|
248
253
|
className: "space-y-6",
|
|
@@ -255,12 +260,12 @@ function F() {
|
|
|
255
260
|
variant: "outline",
|
|
256
261
|
className: "w-full justify-start",
|
|
257
262
|
onClick: () => z(h(B, `users?tenantId=${Q.id}`)),
|
|
258
|
-
children: [/* @__PURE__ */ j(
|
|
263
|
+
children: [/* @__PURE__ */ j(k, { className: "mr-2 h-4 w-4" }), U("admin.tenantDetail.actions.viewUsers", "View Users")]
|
|
259
264
|
}), /* @__PURE__ */ M(i, {
|
|
260
265
|
variant: "outline",
|
|
261
266
|
className: "w-full justify-start",
|
|
262
267
|
onClick: () => z(h(B, `billing/subscriptions?tenantId=${Q.id}`)),
|
|
263
|
-
children: [/* @__PURE__ */ j(
|
|
268
|
+
children: [/* @__PURE__ */ j(S, { className: "mr-2 h-4 w-4" }), U("admin.tenantDetail.actions.viewBilling", "View Billing")]
|
|
264
269
|
})]
|
|
265
270
|
})] })
|
|
266
271
|
})]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TenantDetailPage.js","names":[],"sources":["../../../src/admin/pages/TenantDetailPage.tsx"],"sourcesContent":["import { useParams, useNavigate } from 'react-router-dom';\nimport {\n ArrowLeft,\n Building2,\n Users,\n Mail,\n Calendar,\n Globe,\n Settings,\n Ban,\n CheckCircle,\n Loader2,\n} from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Button,\n Card,\n CardHeader,\n CardTitle,\n CardContent,\n Avatar,\n TenantStatusBadge,\n Separator,\n useToast,\n PermissionGuard,\n} from '../components';\nimport { joinPath } from '../utils/navigation';\nimport { formatDate } from '../utils/formatters';\nimport { mapTenantToUi } from '../utils/mappers';\nimport type { Tenant } from '../types';\nimport {\n useGetTenantQuery,\n useSuspendTenantMutation,\n useActivateTenantMutation,\n} from '../../generated/global-operations';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { tWithFallback } from '../utils/i18n';\nimport { emitTenantLifecycle } from '../utils/tenantEvents';\n\n/**\n * Tenant detail page\n */\nexport function TenantDetailPage() {\n const { canViewTenants, canManageTenants, isLoading: permissionsLoading } = useAdminPermissions();\n\n const { tenantId } = useParams<{ tenantId: string }>();\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const toast = useToast();\n const { t } = useI18n();\n const tr = (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params);\n\n const { data, loading, error } = useGetTenantQuery({\n variables: { id: tenantId! },\n skip: !tenantId || permissionsLoading || !canViewTenants,\n fetchPolicy: 'cache-and-network',\n });\n\n const [suspendTenant, { loading: suspending }] = useSuspendTenantMutation();\n const [activateTenant, { loading: activating }] = useActivateTenantMutation();\n const events = useEventBus();\n\n const tenant: Tenant | null = data?.tenant ? mapTenantToUi(data.tenant) : null;\n\n const handleSuspend = async () => {\n if (!tenantId) return;\n // Capture before the await: a refetch could null `tenant` mid-mutation,\n // and we want consumers of `tenant.tenant.suspended` to see the slug as\n // it was when the user clicked suspend.\n const slugAtClick = tenant?.slug;\n try {\n await suspendTenant({ variables: { id: tenantId } });\n emitTenantLifecycle(events, 'tenant.tenant.suspended', {\n tenantId,\n tenantSlug: slugAtClick,\n status: 'SUSPENDED',\n });\n toast.success(\n tr('admin.tenantDetail.toast.suspendedTitle', 'Tenant suspended'),\n tr('admin.tenantDetail.toast.suspendedDescription', '{{name}} has been suspended.', {\n name: tenant?.name ?? '',\n })\n );\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.toast.suspendFailed', 'Failed to suspend tenant.')\n );\n }\n };\n\n const handleActivate = async () => {\n if (!tenantId) return;\n const slugAtClick = tenant?.slug;\n try {\n await activateTenant({ variables: { id: tenantId } });\n emitTenantLifecycle(events, 'tenant.tenant.activated', {\n tenantId,\n tenantSlug: slugAtClick,\n status: 'ACTIVE',\n });\n toast.success(\n tr('admin.tenantDetail.toast.activatedTitle', 'Tenant activated'),\n tr('admin.tenantDetail.toast.activatedDescription', '{{name}} has been activated.', {\n name: tenant?.name ?? '',\n })\n );\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.toast.activateFailed', 'Failed to activate tenant.')\n );\n }\n };\n\n const workspaces = data?.tenant?.workspaces ?? [];\n\n return (\n <PermissionGuard\n allowed={canViewTenants}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.tenantDetail.pageTitle', 'Tenant Details')}\n denyMessage={tr(\n 'admin.tenantDetail.denyMessage',\n \"You don't have permission to view tenant details.\"\n )}\n >\n {loading ? (\n <PageLayout title={tr('admin.tenantDetail.loadingTitle', 'Loading...')}>\n <div className=\"flex h-64 items-center justify-center\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n </PageLayout>\n ) : error || !tenant ? (\n <PageLayout\n title={tr('admin.tenantDetail.notFoundTitle', 'Tenant Not Found')}\n description={tr(\n 'admin.tenantDetail.notFoundDescription',\n 'The requested tenant could not be found'\n )}\n >\n {error && (\n <div className=\"mb-4 rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive\">\n {error.message}\n </div>\n )}\n <Button variant=\"outline\" onClick={() => navigate(joinPath(basePath, 'tenants'))}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.backToTenants', 'Back to Tenants')}\n </Button>\n </PageLayout>\n ) : (\n <PageLayout\n title={tenant.name}\n description={tr('admin.tenantDetail.tenantId', 'Tenant ID: {{id}}', { id: tenant.id })}\n actions={\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" onClick={() => navigate(joinPath(basePath, 'tenants'))}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.back', 'Back')}\n </Button>\n {canManageTenants && (\n <Button\n variant=\"outline\"\n onClick={() => navigate(joinPath(basePath, `tenants/${tenant.id}/edit`))}\n >\n <Settings className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.edit', 'Edit')}\n </Button>\n )}\n {canManageTenants &&\n (tenant.status === 'active' ? (\n <Button\n variant=\"outline\"\n className=\"text-warning\"\n onClick={handleSuspend}\n disabled={suspending}\n >\n {suspending ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <Ban className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantDetail.suspend', 'Suspend')}\n </Button>\n ) : (\n <Button\n variant=\"outline\"\n className=\"text-success\"\n onClick={handleActivate}\n disabled={activating}\n >\n {activating ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantDetail.activate', 'Activate')}\n </Button>\n ))}\n </div>\n }\n >\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Main Info */}\n <div className=\"space-y-6 lg:col-span-2\">\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.overview.title', 'Overview')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-start gap-4\">\n <Avatar fallback={tenant.name.slice(0, 2).toUpperCase()} size=\"lg\" />\n <div className=\"flex-1 space-y-4\">\n <div>\n <h2 className=\"text-xl font-semibold text-foreground\">{tenant.name}</h2>\n <p className=\"text-sm text-muted-foreground\">@{tenant.slug}</p>\n </div>\n <div className=\"flex flex-wrap items-center gap-4\">\n <TenantStatusBadge status={tenant.status} />\n {tenant.plan && (\n <span className=\"rounded-md bg-primary/10 px-2 py-1 text-xs font-medium text-primary\">\n {tenant.plan}\n </span>\n )}\n </div>\n </div>\n </div>\n\n <Separator className=\"my-6\" />\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {tenant.ownerEmail && (\n <div className=\"flex items-center gap-3\">\n <Mail className=\"h-4 w-4 text-muted-foreground\" />\n <div>\n <p className=\"text-xs text-muted-foreground\">\n {tr('admin.tenantDetail.overview.ownerEmail', 'Owner Email')}\n </p>\n <p className=\"text-sm font-medium\">{tenant.ownerEmail}</p>\n </div>\n </div>\n )}\n {tenant.domain && (\n <div className=\"flex items-center gap-3\">\n <Globe className=\"h-4 w-4 text-muted-foreground\" />\n <div>\n <p className=\"text-xs text-muted-foreground\">\n {tr('admin.tenantDetail.overview.domain', 'Domain')}\n </p>\n <p className=\"text-sm font-medium\">{tenant.domain}</p>\n </div>\n </div>\n )}\n <div className=\"flex items-center gap-3\">\n <Calendar className=\"h-4 w-4 text-muted-foreground\" />\n <div>\n <p className=\"text-xs text-muted-foreground\">\n {tr('admin.tenantDetail.overview.created', 'Created')}\n </p>\n <p className=\"text-sm font-medium\">{formatDate(tenant.createdAt)}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <Calendar className=\"h-4 w-4 text-muted-foreground\" />\n <div>\n <p className=\"text-xs text-muted-foreground\">\n {tr('admin.tenantDetail.overview.lastUpdated', 'Last Updated')}\n </p>\n <p className=\"text-sm font-medium\">{formatDate(tenant.updatedAt)}</p>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n\n {/* Workspaces */}\n {workspaces.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.workspaces.title', 'Workspaces ({{count}})', {\n count: workspaces.length,\n })}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n {workspaces.map(\n (ws: { id: string; name: string; status?: string | null }) => (\n <div\n key={ws.id}\n className=\"flex items-center justify-between rounded-lg border border-border p-3\"\n >\n <span className=\"text-sm font-medium\">{ws.name}</span>\n <span className=\"text-xs text-muted-foreground capitalize\">\n {ws.status?.toLowerCase()}\n </span>\n </div>\n )\n )}\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Config */}\n {tenant.settings && (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.settings.title', 'Settings')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"flex items-center justify-between rounded-lg border border-border p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.allowSignup', 'Allow Signup')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.allowSignup ? 'text-success' : 'text-muted-foreground'\n }`}\n >\n {tenant.settings?.allowSignup\n ? tr('admin.tenantDetail.settings.enabled', 'Enabled')\n : tr('admin.tenantDetail.settings.disabled', 'Disabled')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.sso', 'SSO')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.ssoEnabled ? 'text-success' : 'text-muted-foreground'\n }`}\n >\n {tenant.settings?.ssoEnabled\n ? tr('admin.tenantDetail.settings.enabled', 'Enabled')\n : tr('admin.tenantDetail.settings.disabled', 'Disabled')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.mfaRequired', 'MFA Required')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.mfaRequired ? 'text-success' : 'text-muted-foreground'\n }`}\n >\n {tenant.settings?.mfaRequired\n ? tr('admin.tenantDetail.settings.yes', 'Yes')\n : tr('admin.tenantDetail.settings.no', 'No')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.dataRetention', 'Data Retention')}\n </span>\n <span className=\"text-sm font-medium\">\n {tr('admin.tenantDetail.settings.dataRetentionDays', '{{days}} days', {\n days: tenant.settings?.dataRetentionDays ?? 0,\n })}\n </span>\n </div>\n </div>\n </CardContent>\n </Card>\n )}\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* Actions */}\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.actions.title', 'Actions')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n <Button\n variant=\"outline\"\n className=\"w-full justify-start\"\n onClick={() => navigate(joinPath(basePath, `users?tenantId=${tenant.id}`))}\n >\n <Users className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.actions.viewUsers', 'View Users')}\n </Button>\n <Button\n variant=\"outline\"\n className=\"w-full justify-start\"\n onClick={() =>\n navigate(joinPath(basePath, `billing/subscriptions?tenantId=${tenant.id}`))\n }\n >\n <Building2 className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.actions.viewBilling', 'View Billing')}\n </Button>\n </CardContent>\n </Card>\n </div>\n </div>\n </PageLayout>\n )}\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TenantDetailPage.js","names":[],"sources":["../../../src/admin/pages/TenantDetailPage.tsx"],"sourcesContent":["import { useParams, useNavigate } from 'react-router-dom';\nimport {\n ArrowLeft,\n Building2,\n Users,\n Mail,\n Calendar,\n Globe,\n Settings,\n Ban,\n CheckCircle,\n Loader2,\n} from 'lucide-react';\nimport { useAdmin } from '../providers/AdminProvider';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Button,\n Card,\n CardHeader,\n CardTitle,\n CardContent,\n Avatar,\n TenantStatusBadge,\n Separator,\n useToast,\n PermissionGuard,\n} from '../components';\nimport { TenantSsoPanel } from '../components/TenantSsoPanel';\nimport { joinPath } from '../utils/navigation';\nimport { formatDate } from '../utils/formatters';\nimport { mapTenantToUi } from '../utils/mappers';\nimport type { Tenant } from '../types';\nimport {\n useGetTenantQuery,\n useSuspendTenantMutation,\n useActivateTenantMutation,\n} from '../../generated/global-operations';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { tWithFallback } from '../utils/i18n';\nimport { emitTenantLifecycle } from '../utils/tenantEvents';\n\n/**\n * Tenant detail page\n */\nexport function TenantDetailPage() {\n const { canViewTenants, canManageTenants, isLoading: permissionsLoading } = useAdminPermissions();\n\n const { tenantId } = useParams<{ tenantId: string }>();\n const navigate = useNavigate();\n const { basePath = '' } = useAdmin();\n const toast = useToast();\n const { t } = useI18n();\n const tr = (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params);\n\n const { data, loading, error } = useGetTenantQuery({\n variables: { id: tenantId! },\n skip: !tenantId || permissionsLoading || !canViewTenants,\n fetchPolicy: 'cache-and-network',\n });\n\n const [suspendTenant, { loading: suspending }] = useSuspendTenantMutation();\n const [activateTenant, { loading: activating }] = useActivateTenantMutation();\n const events = useEventBus();\n\n const tenant: Tenant | null = data?.tenant ? mapTenantToUi(data.tenant) : null;\n\n const handleSuspend = async () => {\n if (!tenantId) return;\n // Capture before the await: a refetch could null `tenant` mid-mutation,\n // and we want consumers of `tenant.tenant.suspended` to see the slug as\n // it was when the user clicked suspend.\n const slugAtClick = tenant?.slug;\n try {\n await suspendTenant({ variables: { id: tenantId } });\n emitTenantLifecycle(events, 'tenant.tenant.suspended', {\n tenantId,\n tenantSlug: slugAtClick,\n status: 'SUSPENDED',\n });\n toast.success(\n tr('admin.tenantDetail.toast.suspendedTitle', 'Tenant suspended'),\n tr('admin.tenantDetail.toast.suspendedDescription', '{{name}} has been suspended.', {\n name: tenant?.name ?? '',\n })\n );\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.toast.suspendFailed', 'Failed to suspend tenant.')\n );\n }\n };\n\n const handleActivate = async () => {\n if (!tenantId) return;\n const slugAtClick = tenant?.slug;\n try {\n await activateTenant({ variables: { id: tenantId } });\n emitTenantLifecycle(events, 'tenant.tenant.activated', {\n tenantId,\n tenantSlug: slugAtClick,\n status: 'ACTIVE',\n });\n toast.success(\n tr('admin.tenantDetail.toast.activatedTitle', 'Tenant activated'),\n tr('admin.tenantDetail.toast.activatedDescription', '{{name}} has been activated.', {\n name: tenant?.name ?? '',\n })\n );\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.toast.activateFailed', 'Failed to activate tenant.')\n );\n }\n };\n\n const workspaces = data?.tenant?.workspaces ?? [];\n\n return (\n <PermissionGuard\n allowed={canViewTenants}\n isLoading={permissionsLoading}\n pageTitle={tr('admin.tenantDetail.pageTitle', 'Tenant Details')}\n denyMessage={tr(\n 'admin.tenantDetail.denyMessage',\n \"You don't have permission to view tenant details.\"\n )}\n >\n {loading ? (\n <PageLayout title={tr('admin.tenantDetail.loadingTitle', 'Loading...')}>\n <div className=\"flex h-64 items-center justify-center\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n </PageLayout>\n ) : error || !tenant ? (\n <PageLayout\n title={tr('admin.tenantDetail.notFoundTitle', 'Tenant Not Found')}\n description={tr(\n 'admin.tenantDetail.notFoundDescription',\n 'The requested tenant could not be found'\n )}\n >\n {error && (\n <div className=\"mb-4 rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive\">\n {error.message}\n </div>\n )}\n <Button variant=\"outline\" onClick={() => navigate(joinPath(basePath, 'tenants'))}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.backToTenants', 'Back to Tenants')}\n </Button>\n </PageLayout>\n ) : (\n <PageLayout\n title={tenant.name}\n description={tr('admin.tenantDetail.tenantId', 'Tenant ID: {{id}}', { id: tenant.id })}\n actions={\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" onClick={() => navigate(joinPath(basePath, 'tenants'))}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.back', 'Back')}\n </Button>\n {canManageTenants && (\n <Button\n variant=\"outline\"\n onClick={() => navigate(joinPath(basePath, `tenants/${tenant.id}/edit`))}\n >\n <Settings className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.edit', 'Edit')}\n </Button>\n )}\n {canManageTenants &&\n (tenant.status === 'active' ? (\n <Button\n variant=\"outline\"\n className=\"text-warning\"\n onClick={handleSuspend}\n disabled={suspending}\n >\n {suspending ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <Ban className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantDetail.suspend', 'Suspend')}\n </Button>\n ) : (\n <Button\n variant=\"outline\"\n className=\"text-success\"\n onClick={handleActivate}\n disabled={activating}\n >\n {activating ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantDetail.activate', 'Activate')}\n </Button>\n ))}\n </div>\n }\n >\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Main Info */}\n <div className=\"space-y-6 lg:col-span-2\">\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.overview.title', 'Overview')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-start gap-4\">\n <Avatar fallback={tenant.name.slice(0, 2).toUpperCase()} size=\"lg\" />\n <div className=\"flex-1 space-y-4\">\n <div>\n <h2 className=\"text-xl font-semibold text-foreground\">{tenant.name}</h2>\n <p className=\"text-sm text-muted-foreground\">@{tenant.slug}</p>\n </div>\n <div className=\"flex flex-wrap items-center gap-4\">\n <TenantStatusBadge status={tenant.status} />\n {tenant.plan && (\n <span className=\"rounded-md bg-primary/10 px-2 py-1 text-xs font-medium text-primary\">\n {tenant.plan}\n </span>\n )}\n </div>\n </div>\n </div>\n\n <Separator className=\"my-6\" />\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {tenant.ownerEmail && (\n <div className=\"flex items-center gap-3\">\n <Mail className=\"h-4 w-4 text-muted-foreground\" />\n <div>\n <p className=\"text-xs text-muted-foreground\">\n {tr('admin.tenantDetail.overview.ownerEmail', 'Owner Email')}\n </p>\n <p className=\"text-sm font-medium\">{tenant.ownerEmail}</p>\n </div>\n </div>\n )}\n {tenant.domain && (\n <div className=\"flex items-center gap-3\">\n <Globe className=\"h-4 w-4 text-muted-foreground\" />\n <div>\n <p className=\"text-xs text-muted-foreground\">\n {tr('admin.tenantDetail.overview.domain', 'Domain')}\n </p>\n <p className=\"text-sm font-medium\">{tenant.domain}</p>\n </div>\n </div>\n )}\n <div className=\"flex items-center gap-3\">\n <Calendar className=\"h-4 w-4 text-muted-foreground\" />\n <div>\n <p className=\"text-xs text-muted-foreground\">\n {tr('admin.tenantDetail.overview.created', 'Created')}\n </p>\n <p className=\"text-sm font-medium\">{formatDate(tenant.createdAt)}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <Calendar className=\"h-4 w-4 text-muted-foreground\" />\n <div>\n <p className=\"text-xs text-muted-foreground\">\n {tr('admin.tenantDetail.overview.lastUpdated', 'Last Updated')}\n </p>\n <p className=\"text-sm font-medium\">{formatDate(tenant.updatedAt)}</p>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n\n {/* Workspaces */}\n {workspaces.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.workspaces.title', 'Workspaces ({{count}})', {\n count: workspaces.length,\n })}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n {workspaces.map(\n (ws: { id: string; name: string; status?: string | null }) => (\n <div\n key={ws.id}\n className=\"flex items-center justify-between rounded-lg border border-border p-3\"\n >\n <span className=\"text-sm font-medium\">{ws.name}</span>\n <span className=\"text-xs text-muted-foreground capitalize\">\n {ws.status?.toLowerCase()}\n </span>\n </div>\n )\n )}\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Config */}\n {tenant.settings && (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.settings.title', 'Settings')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"flex items-center justify-between rounded-lg border border-border p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.allowSignup', 'Allow Signup')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.allowSignup ? 'text-success' : 'text-muted-foreground'\n }`}\n >\n {tenant.settings?.allowSignup\n ? tr('admin.tenantDetail.settings.enabled', 'Enabled')\n : tr('admin.tenantDetail.settings.disabled', 'Disabled')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.sso', 'SSO')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.ssoEnabled ? 'text-success' : 'text-muted-foreground'\n }`}\n >\n {tenant.settings?.ssoEnabled\n ? tr('admin.tenantDetail.settings.enabled', 'Enabled')\n : tr('admin.tenantDetail.settings.disabled', 'Disabled')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.mfaRequired', 'MFA Required')}\n </span>\n <span\n className={`text-sm font-medium ${\n tenant.settings?.mfaRequired ? 'text-success' : 'text-muted-foreground'\n }`}\n >\n {tenant.settings?.mfaRequired\n ? tr('admin.tenantDetail.settings.yes', 'Yes')\n : tr('admin.tenantDetail.settings.no', 'No')}\n </span>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border p-3\">\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.settings.dataRetention', 'Data Retention')}\n </span>\n <span className=\"text-sm font-medium\">\n {tr('admin.tenantDetail.settings.dataRetentionDays', '{{days}} days', {\n days: tenant.settings?.dataRetentionDays ?? 0,\n })}\n </span>\n </div>\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* SSO Configuration */}\n <TenantSsoPanel tenantId={tenant.id} canManage={canManageTenants} />\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* Actions */}\n <Card>\n <CardHeader>\n <CardTitle className=\"text-lg\">\n {tr('admin.tenantDetail.actions.title', 'Actions')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n <Button\n variant=\"outline\"\n className=\"w-full justify-start\"\n onClick={() => navigate(joinPath(basePath, `users?tenantId=${tenant.id}`))}\n >\n <Users className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.actions.viewUsers', 'View Users')}\n </Button>\n <Button\n variant=\"outline\"\n className=\"w-full justify-start\"\n onClick={() =>\n navigate(joinPath(basePath, `billing/subscriptions?tenantId=${tenant.id}`))\n }\n >\n <Building2 className=\"mr-2 h-4 w-4\" />\n {tr('admin.tenantDetail.actions.viewBilling', 'View Billing')}\n </Button>\n </CardContent>\n </Card>\n </div>\n </div>\n </PageLayout>\n )}\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
@@ -8,6 +8,7 @@ export { SettingsPage } from './SettingsPage';
|
|
|
8
8
|
export { ProductsPage } from './ProductsPage';
|
|
9
9
|
export { CommunicationsPage } from './CommunicationsPage';
|
|
10
10
|
export { SREPage } from './SREPage';
|
|
11
|
+
export { SyncFailuresPage } from './SyncFailuresPage';
|
|
11
12
|
export { AuditLogsPage } from './AuditLogsPage';
|
|
12
13
|
export { SandboxSettingsPage } from './SandboxSettingsPage';
|
|
13
14
|
export { OrganizationsListPage } from './OrganizationsListPage';
|