@dreamtree-org/landing-kit 0.9.0 → 0.10.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/MIGRATION.md +16 -0
- package/PLATFORM-API.md +17 -13
- package/README.md +41 -1
- package/ai-skills/landing-kit.md +7 -6
- package/auth.css +57 -0
- package/auth.d.ts +2 -2
- package/dist/auth.cjs +143 -38
- package/dist/auth.js +139 -34
- package/dist/index.cjs +605 -540
- package/dist/index.js +573 -508
- package/dist/platform.cjs +1208 -408
- package/dist/platform.js +1209 -409
- package/index.d.ts +25 -0
- package/package.json +3 -2
- package/platform.d.ts +37 -3
- package/styles.css +57 -0
package/dist/platform.cjs
CHANGED
|
@@ -32,6 +32,7 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
Card: () => Card,
|
|
34
34
|
CopyButton: () => CopyButton,
|
|
35
|
+
DomainsTab: () => DomainsTab,
|
|
35
36
|
Facts: () => Facts,
|
|
36
37
|
HealthDot: () => HealthDot,
|
|
37
38
|
InspectResult: () => InspectResult,
|
|
@@ -40,19 +41,26 @@ __export(index_exports, {
|
|
|
40
41
|
PLATFORM_API_BASE: () => PLATFORM_API_BASE,
|
|
41
42
|
PLATFORM_FEATURES: () => PLATFORM_FEATURES,
|
|
42
43
|
PLATFORM_LOGIN_PATH: () => PLATFORM_LOGIN_PATH,
|
|
44
|
+
PlansView: () => PlansView,
|
|
43
45
|
PlatformApiError: () => PlatformApiError,
|
|
44
46
|
PlatformConsole: () => PlatformConsole,
|
|
45
47
|
PlatformLogin: () => PlatformLogin,
|
|
46
48
|
PlatformShell: () => PlatformShell,
|
|
47
49
|
SHIPPED_FEATURES: () => SHIPPED_FEATURES,
|
|
48
50
|
STEP_UP_REQUIRED: () => STEP_UP_REQUIRED,
|
|
51
|
+
SettingsTab: () => SettingsTab,
|
|
49
52
|
StatusPill: () => StatusPill,
|
|
50
53
|
StepUpProvider: () => StepUpProvider,
|
|
54
|
+
SubscriptionTab: () => SubscriptionTab,
|
|
55
|
+
SupportAccessView: () => SupportAccessView,
|
|
56
|
+
SupportGrants: () => SupportGrants,
|
|
57
|
+
SupportTab: () => SupportTab,
|
|
51
58
|
createPlatformClient: () => createPlatformClient,
|
|
52
59
|
createStepUp: () => createStepUp,
|
|
53
60
|
describeAction: () => describeAction,
|
|
54
61
|
enabledFeatures: () => enabledFeatures,
|
|
55
62
|
formatDate: () => formatDate,
|
|
63
|
+
formatPrice: () => formatPrice,
|
|
56
64
|
isStepUpRequired: () => isStepUpRequired,
|
|
57
65
|
matchConsoleRoute: () => matchConsoleRoute,
|
|
58
66
|
readPlatformCsrf: () => readPlatformCsrf,
|
|
@@ -67,7 +75,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
67
75
|
var PLATFORM_API_BASE = "/api/platform";
|
|
68
76
|
var PLATFORM_LOGIN_PATH = "/platform/login";
|
|
69
77
|
var STEP_UP_REQUIRED = "STEP_UP_REQUIRED";
|
|
70
|
-
var PLATFORM_FEATURES = ["overview", "companies", "jobs", "plans", "audit", "support", "settings", "admins"];
|
|
78
|
+
var PLATFORM_FEATURES = ["overview", "companies", "jobs", "plans", "audit", "support", "settings", "domains", "admins"];
|
|
71
79
|
var SHIPPED_FEATURES = ["overview", "companies", "jobs", "admins"];
|
|
72
80
|
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
73
81
|
var CSRF_HEADER = "x-csrf-token";
|
|
@@ -99,7 +107,7 @@ function readPlatformCsrf(cookieString, name) {
|
|
|
99
107
|
return null;
|
|
100
108
|
}
|
|
101
109
|
function joinUrl(basePath, path, query) {
|
|
102
|
-
const
|
|
110
|
+
const base3 = basePath.replace(/\/+$/, "");
|
|
103
111
|
const rel = path ? `/${String(path).replace(/^\/+/, "")}` : "";
|
|
104
112
|
const params = new URLSearchParams();
|
|
105
113
|
for (const [k, v] of Object.entries(query != null ? query : {})) {
|
|
@@ -107,7 +115,7 @@ function joinUrl(basePath, path, query) {
|
|
|
107
115
|
params.append(k, String(v));
|
|
108
116
|
}
|
|
109
117
|
const qs = params.toString();
|
|
110
|
-
return `${
|
|
118
|
+
return `${base3}${rel}${qs ? `?${qs}` : ""}`;
|
|
111
119
|
}
|
|
112
120
|
function createPlatformClient(opts = {}) {
|
|
113
121
|
var _a, _b, _c, _d, _e;
|
|
@@ -210,8 +218,8 @@ function timeAgo(value, now = Date.now()) {
|
|
|
210
218
|
}
|
|
211
219
|
|
|
212
220
|
// src/platform/PlatformConsole.jsx
|
|
213
|
-
var
|
|
214
|
-
var
|
|
221
|
+
var import_react16 = __toESM(require("react"), 1);
|
|
222
|
+
var import_twreact_ui15 = require("@dreamtree-org/twreact-ui");
|
|
215
223
|
|
|
216
224
|
// src/platform/context.jsx
|
|
217
225
|
var import_react = __toESM(require("react"), 1);
|
|
@@ -463,6 +471,23 @@ var IconArchive = make(/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_
|
|
|
463
471
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8M10 12h4" })
|
|
464
472
|
] }));
|
|
465
473
|
var IconDownload = make(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" }));
|
|
474
|
+
var IconTag = make(/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
475
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12.6 2.6A2 2 0 0 0 11.2 2H4a2 2 0 0 0-2 2v7.2a2 2 0 0 0 .6 1.4l8.7 8.7a2.4 2.4 0 0 0 3.4 0l6.6-6.6a2.4 2.4 0 0 0 0-3.4Z" }),
|
|
476
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "7.5", cy: "7.5", r: "1.5" })
|
|
477
|
+
] }));
|
|
478
|
+
var IconLifebuoy = make(/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
479
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
|
|
480
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "12", cy: "12", r: "4" }),
|
|
481
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "m4.93 4.93 4.24 4.24M14.83 14.83l4.24 4.24M14.83 9.17l4.24-4.24M4.93 19.07l4.24-4.24" })
|
|
482
|
+
] }));
|
|
483
|
+
var IconGlobe = make(/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
484
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
|
|
485
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" })
|
|
486
|
+
] }));
|
|
487
|
+
var IconSettings = make(/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
488
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" }),
|
|
489
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "12", cy: "12", r: "3" })
|
|
490
|
+
] }));
|
|
466
491
|
|
|
467
492
|
// src/platform/PlatformShell.jsx
|
|
468
493
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
@@ -536,7 +561,13 @@ var STATUS_MAP = {
|
|
|
536
561
|
running: { label: "Running", tone: "info" },
|
|
537
562
|
pending: { label: "Pending", tone: "neutral" },
|
|
538
563
|
queued: { label: "Queued", tone: "neutral" },
|
|
539
|
-
cancelled: { label: "Cancelled", tone: "muted" }
|
|
564
|
+
cancelled: { label: "Cancelled", tone: "muted" },
|
|
565
|
+
trial: { label: "Trial", tone: "info" },
|
|
566
|
+
past_due: { label: "Past due", tone: "warning" },
|
|
567
|
+
expired: { label: "Expired", tone: "muted" },
|
|
568
|
+
requested: { label: "Awaiting approval", tone: "warning" },
|
|
569
|
+
revoked: { label: "Revoked", tone: "muted" },
|
|
570
|
+
verified: { label: "Verified", tone: "success" }
|
|
540
571
|
};
|
|
541
572
|
function StatusPill({ status }) {
|
|
542
573
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_twreact_ui3.StatusPill, { status: String(status != null ? status : ""), map: STATUS_MAP });
|
|
@@ -693,8 +724,31 @@ var ACTION_LABEL = {
|
|
|
693
724
|
"platform.admin.roles": "changed platform admin roles",
|
|
694
725
|
"platform.admin.disable": "disabled platform admin",
|
|
695
726
|
"platform.admin.enable": "enabled platform admin",
|
|
696
|
-
"audit.export": "exported the audit log"
|
|
727
|
+
"audit.export": "exported the audit log",
|
|
728
|
+
"plan.create": "created plan",
|
|
729
|
+
"plan.update": "updated plan",
|
|
730
|
+
"subscription.update": "changed subscription",
|
|
731
|
+
"settings.update": "changed settings",
|
|
732
|
+
"support.grant.request": "requested support access",
|
|
733
|
+
"support.grant.approve": "approved support access",
|
|
734
|
+
"support.grant.revoke": "revoked support access",
|
|
735
|
+
"domain.add": "added domain",
|
|
736
|
+
"domain.verify": "verified domain",
|
|
737
|
+
"domain.primary": "set primary domain",
|
|
738
|
+
"domain.remove": "removed domain"
|
|
697
739
|
};
|
|
740
|
+
function formatPrice(priceMinor, currency = "INR", interval) {
|
|
741
|
+
var _a;
|
|
742
|
+
const amount = Number(priceMinor != null ? priceMinor : 0) / 100;
|
|
743
|
+
let text;
|
|
744
|
+
try {
|
|
745
|
+
text = new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
|
|
746
|
+
} catch {
|
|
747
|
+
text = `${currency} ${amount.toFixed(2)}`;
|
|
748
|
+
}
|
|
749
|
+
const per = (_a = { month: " / month", year: " / year", "one-time": " once" }[interval]) != null ? _a : "";
|
|
750
|
+
return `${text}${per}`;
|
|
751
|
+
}
|
|
698
752
|
function describeAction(action) {
|
|
699
753
|
var _a;
|
|
700
754
|
return (_a = ACTION_LABEL[action]) != null ? _a : String(action != null ? action : "").replace(/[._]/g, " ");
|
|
@@ -1171,19 +1225,503 @@ function NewCompanyView() {
|
|
|
1171
1225
|
}
|
|
1172
1226
|
|
|
1173
1227
|
// src/platform/views/CompanyDetail.jsx
|
|
1228
|
+
var import_react11 = __toESM(require("react"), 1);
|
|
1229
|
+
var import_twreact_ui10 = require("@dreamtree-org/twreact-ui");
|
|
1230
|
+
|
|
1231
|
+
// src/platform/views/CompanyFeatures.jsx
|
|
1232
|
+
var import_react10 = __toESM(require("react"), 1);
|
|
1233
|
+
var import_twreact_ui9 = require("@dreamtree-org/twreact-ui");
|
|
1234
|
+
|
|
1235
|
+
// src/platform/views/SupportAccess.jsx
|
|
1174
1236
|
var import_react9 = __toESM(require("react"), 1);
|
|
1175
1237
|
var import_twreact_ui8 = require("@dreamtree-org/twreact-ui");
|
|
1176
1238
|
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
1239
|
+
var FILTERS2 = [
|
|
1240
|
+
{ value: "", label: "All" },
|
|
1241
|
+
{ value: "requested", label: "Awaiting approval" },
|
|
1242
|
+
{ value: "active", label: "Active" },
|
|
1243
|
+
{ value: "expired", label: "Expired" },
|
|
1244
|
+
{ value: "revoked", label: "Revoked" }
|
|
1245
|
+
];
|
|
1246
|
+
var DURATIONS = [
|
|
1247
|
+
{ value: "30", label: "30 minutes" },
|
|
1248
|
+
{ value: "60", label: "1 hour" },
|
|
1249
|
+
{ value: "120", label: "2 hours" },
|
|
1250
|
+
{ value: "240", label: "4 hours" },
|
|
1251
|
+
{ value: "480", label: "8 hours" }
|
|
1252
|
+
];
|
|
1253
|
+
function until(value) {
|
|
1254
|
+
const ms = new Date(value).getTime() - Date.now();
|
|
1255
|
+
if (!Number.isFinite(ms) || ms <= 0) return "now";
|
|
1256
|
+
const m = Math.round(ms / 6e4);
|
|
1257
|
+
return m < 60 ? `in ${m} min` : `in ${Math.floor(m / 60)} h ${m % 60} min`;
|
|
1258
|
+
}
|
|
1259
|
+
function SupportGrants({ companyId, companyName }) {
|
|
1260
|
+
var _a, _b;
|
|
1261
|
+
const { client, can, me } = usePlatform();
|
|
1262
|
+
const sensitive = useSensitive();
|
|
1263
|
+
const [status, setStatus] = (0, import_react9.useState)(companyId ? "" : "requested");
|
|
1264
|
+
const { data, error, loading, reload } = useLoad("support-access", { company: companyId || void 0, status: status || void 0 });
|
|
1265
|
+
const [flash, setFlash] = (0, import_react9.useState)(null);
|
|
1266
|
+
const [requesting, setRequesting] = (0, import_react9.useState)(false);
|
|
1267
|
+
const canGrant = can("support.grant");
|
|
1268
|
+
const myId = (_a = me == null ? void 0 : me.admin) == null ? void 0 : _a.id;
|
|
1269
|
+
const grants = (_b = data == null ? void 0 : data.grants) != null ? _b : [];
|
|
1270
|
+
const act = async (g, action) => {
|
|
1271
|
+
var _a2;
|
|
1272
|
+
setFlash(null);
|
|
1273
|
+
try {
|
|
1274
|
+
await sensitive(() => client.post(`support-access/${encodeURIComponent(g.id)}`, { action }));
|
|
1275
|
+
setFlash({ kind: "success", msg: action === "approve" ? `Approved access to ${(_a2 = g.companyName) != null ? _a2 : "the company"}` : "Access revoked" });
|
|
1276
|
+
await reload();
|
|
1277
|
+
} catch (e) {
|
|
1278
|
+
const msg = errorText(e);
|
|
1279
|
+
if (msg) setFlash({ kind: "error", msg });
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-4", children: [
|
|
1283
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
|
|
1284
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "overflow-x-auto", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_twreact_ui8.SegmentedControl, { "aria-label": "Filter by status", options: FILTERS2, value: status, onChange: (v) => setStatus(String(v)) }) }),
|
|
1285
|
+
can("support.request") && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_twreact_ui8.Button, { variant: companyId ? "outline" : "primary", leftIcon: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(IconPlus, {}), onClick: () => setRequesting(true), children: "Request access" })
|
|
1286
|
+
] }),
|
|
1287
|
+
flash && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_twreact_ui8.Alert, { variant: flash.kind, dismissible: true, onDismiss: () => setFlash(null), children: flash.msg }),
|
|
1288
|
+
error && !data ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_twreact_ui8.ErrorState, { description: error.message, onRetry: reload }) : loading && !data ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_twreact_ui8.LoadingState, { variant: "table", rows: 3 }) : grants.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
1289
|
+
import_twreact_ui8.EmptyState,
|
|
1290
|
+
{
|
|
1291
|
+
compact: true,
|
|
1292
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(IconLifebuoy, { size: 24 }),
|
|
1293
|
+
title: status ? "No grants with this status" : "No support access yet",
|
|
1294
|
+
description: "Support access lets a platform admin work in one company for a limited time, after another admin approves it."
|
|
1295
|
+
}
|
|
1296
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { className: "divide-y divide-line rounded-xl border border-line bg-surface", children: grants.map((g) => {
|
|
1297
|
+
var _a2, _b2;
|
|
1298
|
+
const mine = myId && g.requestedById === myId;
|
|
1299
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("li", { className: "flex flex-wrap items-start gap-3 px-4 py-3", children: [
|
|
1300
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
1301
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "text-sm font-medium text-foreground", children: [
|
|
1302
|
+
g.requestedBy,
|
|
1303
|
+
!companyId && g.companyName ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "text-muted", children: [
|
|
1304
|
+
" \u2192 ",
|
|
1305
|
+
g.companyName
|
|
1306
|
+
] }) : null
|
|
1307
|
+
] }),
|
|
1308
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "mt-0.5 break-words text-sm text-foreground", children: g.reason }),
|
|
1309
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "mt-0.5 text-xs text-muted", children: [
|
|
1310
|
+
g.status === "requested" && `${(_a2 = g.minutes) != null ? _a2 : "\u2014"} min once approved \xB7 requested ${timeAgo(g.createdAt)}`,
|
|
1311
|
+
g.status === "active" && `Approved by ${(_b2 = g.approvedBy) != null ? _b2 : "\u2014"} \xB7 ends ${until(g.expiresAt)}`,
|
|
1312
|
+
g.status === "expired" && `Ended ${formatDate(g.expiresAt)}`,
|
|
1313
|
+
g.status === "revoked" && `Revoked ${formatDate(g.revokedAt)}`
|
|
1314
|
+
] })
|
|
1315
|
+
] }),
|
|
1316
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StatusPill, { status: g.status }),
|
|
1317
|
+
canGrant && (g.status === "requested" || g.status === "active") && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex gap-1", children: [
|
|
1318
|
+
g.status === "requested" && !mine && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_twreact_ui8.Button, { size: "sm", onClick: () => act(g, "approve"), children: "Approve" }),
|
|
1319
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_twreact_ui8.Button, { size: "sm", variant: "ghost", onClick: () => act(g, "revoke"), children: g.status === "requested" ? "Decline" : "Revoke" })
|
|
1320
|
+
] })
|
|
1321
|
+
] }, g.id);
|
|
1322
|
+
}) }),
|
|
1323
|
+
requesting && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
1324
|
+
RequestDialog,
|
|
1325
|
+
{
|
|
1326
|
+
companyId,
|
|
1327
|
+
companyName,
|
|
1328
|
+
onClose: () => setRequesting(false),
|
|
1329
|
+
onDone: async (g) => {
|
|
1330
|
+
var _a2;
|
|
1331
|
+
setRequesting(false);
|
|
1332
|
+
setFlash({ kind: "success", msg: `Requested access to ${(_a2 = g.companyName) != null ? _a2 : "the company"}. Another admin has to approve it.` });
|
|
1333
|
+
await reload();
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
)
|
|
1337
|
+
] });
|
|
1338
|
+
}
|
|
1339
|
+
function RequestDialog({ companyId, companyName, onClose, onDone }) {
|
|
1340
|
+
var _a, _b;
|
|
1341
|
+
const { client } = usePlatform();
|
|
1342
|
+
const companies = useLoad(companyId ? null : "companies");
|
|
1343
|
+
const [company, setCompany] = (0, import_react9.useState)(companyId != null ? companyId : "");
|
|
1344
|
+
const [reason, setReason] = (0, import_react9.useState)("");
|
|
1345
|
+
const [minutes, setMinutes] = (0, import_react9.useState)("60");
|
|
1346
|
+
const [busy, setBusy] = (0, import_react9.useState)(false);
|
|
1347
|
+
const [err, setErr] = (0, import_react9.useState)(null);
|
|
1348
|
+
const options = ((_b = (_a = companies.data) == null ? void 0 : _a.companies) != null ? _b : []).map((c) => {
|
|
1349
|
+
var _a2;
|
|
1350
|
+
return { value: c.id, label: (_a2 = c.displayName) != null ? _a2 : c.slug };
|
|
1351
|
+
});
|
|
1352
|
+
const submit = async () => {
|
|
1353
|
+
setBusy(true);
|
|
1354
|
+
setErr(null);
|
|
1355
|
+
try {
|
|
1356
|
+
const r = await client.post("support-access", { companyId: company, reason: reason.trim(), minutes: Number(minutes) });
|
|
1357
|
+
await onDone(r.grant);
|
|
1358
|
+
} catch (e) {
|
|
1359
|
+
setErr(errorText(e));
|
|
1360
|
+
} finally {
|
|
1361
|
+
setBusy(false);
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
1365
|
+
import_twreact_ui8.Dialog,
|
|
1366
|
+
{
|
|
1367
|
+
isOpen: true,
|
|
1368
|
+
onClose,
|
|
1369
|
+
title: "Request support access",
|
|
1370
|
+
size: "sm",
|
|
1371
|
+
footer: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex justify-end gap-2", children: [
|
|
1372
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_twreact_ui8.Button, { variant: "outline", onClick: onClose, children: "Cancel" }),
|
|
1373
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_twreact_ui8.Button, { loading: busy, disabled: !company || reason.trim().length < 8, onClick: submit, children: "Request" })
|
|
1374
|
+
] }),
|
|
1375
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-4", children: [
|
|
1376
|
+
err && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_twreact_ui8.Alert, { variant: "error", children: err }),
|
|
1377
|
+
companyId ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_twreact_ui8.Input, { label: "Company", value: companyName != null ? companyName : companyId, disabled: true }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_twreact_ui8.Select, { label: "Company", searchable: true, options, loading: companies.loading, value: company, onChange: (v) => setCompany(String(v)), placeholder: "Pick a company" }),
|
|
1378
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_twreact_ui8.Textarea, { label: "Reason", rows: 3, value: reason, onChange: (e) => setReason(e.target.value), helperText: "At least 8 characters, e.g. the ticket number. The company's audit trail shows it." }),
|
|
1379
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_twreact_ui8.Select, { label: "For", options: DURATIONS, value: minutes, onChange: (v) => setMinutes(String(v)), helperText: "Counted from approval" })
|
|
1380
|
+
] })
|
|
1381
|
+
}
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
function SupportAccessView() {
|
|
1385
|
+
const { Link } = usePlatform();
|
|
1386
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "mx-auto max-w-5xl space-y-6 px-4 py-6 sm:px-6", children: [
|
|
1387
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_twreact_ui8.PageHeader, { title: "Support access", subtitle: "Time-boxed access to one company, approved by a second platform admin", linkAs: Link }),
|
|
1388
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SupportGrants, {})
|
|
1389
|
+
] });
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// src/platform/views/CompanyFeatures.jsx
|
|
1393
|
+
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
1394
|
+
var SUB_STATUSES = [
|
|
1395
|
+
{ value: "active", label: "Active" },
|
|
1396
|
+
{ value: "trial", label: "Trial" },
|
|
1397
|
+
{ value: "past_due", label: "Past due" },
|
|
1398
|
+
{ value: "cancelled", label: "Cancelled" },
|
|
1399
|
+
{ value: "expired", label: "Expired" }
|
|
1400
|
+
];
|
|
1401
|
+
var day = (v) => v ? String(v).slice(0, 10) : "";
|
|
1402
|
+
function featureTabs({ features, can }) {
|
|
1403
|
+
return [
|
|
1404
|
+
features.includes("plans") && can("plans.read") && { id: "subscription", label: "Subscription", Component: SubscriptionTab },
|
|
1405
|
+
features.includes("domains") && can("domains.read") && { id: "domains", label: "Domains", Component: DomainsTab },
|
|
1406
|
+
features.includes("settings") && can("settings.read") && { id: "settings", label: "Settings", Component: SettingsTab },
|
|
1407
|
+
features.includes("support") && can("support.request") && { id: "support", label: "Support", Component: SupportTab }
|
|
1408
|
+
].filter(Boolean);
|
|
1409
|
+
}
|
|
1410
|
+
function SubscriptionTab({ company, setFlash }) {
|
|
1411
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1412
|
+
const { client, can } = usePlatform();
|
|
1413
|
+
const sensitive = useSensitive();
|
|
1414
|
+
const id = encodeURIComponent(company.id);
|
|
1415
|
+
const { data, error, loading, reload } = useLoad(`companies/${id}/subscription`);
|
|
1416
|
+
const plans = useLoad(can("plans.write") ? "plans" : null);
|
|
1417
|
+
const [editing, setEditing] = (0, import_react10.useState)(null);
|
|
1418
|
+
const [busy, setBusy] = (0, import_react10.useState)(false);
|
|
1419
|
+
const current = data == null ? void 0 : data.subscription;
|
|
1420
|
+
const history = (_a = data == null ? void 0 : data.history) != null ? _a : [];
|
|
1421
|
+
const planOptions = ((_c = (_b = plans.data) == null ? void 0 : _b.plans) != null ? _c : []).filter((p) => p.status === "active" || p.id === (current == null ? void 0 : current.planId)).map((p) => ({ value: p.id, label: `${p.name} \xB7 ${formatPrice(p.priceMinor, p.currency, p.interval)}` }));
|
|
1422
|
+
const save = async () => {
|
|
1423
|
+
setBusy(true);
|
|
1424
|
+
try {
|
|
1425
|
+
await sensitive(
|
|
1426
|
+
() => client.put(`companies/${id}/subscription`, {
|
|
1427
|
+
planId: editing.planId,
|
|
1428
|
+
status: editing.status,
|
|
1429
|
+
startsAt: editing.startsAt || void 0,
|
|
1430
|
+
endsAt: editing.endsAt || null,
|
|
1431
|
+
seats: editing.seats === "" ? null : Number(editing.seats),
|
|
1432
|
+
notes: editing.notes || null
|
|
1433
|
+
})
|
|
1434
|
+
);
|
|
1435
|
+
setEditing(null);
|
|
1436
|
+
setFlash({ kind: "success", msg: "Subscription updated" });
|
|
1437
|
+
await reload();
|
|
1438
|
+
} catch (e) {
|
|
1439
|
+
const msg = errorText(e);
|
|
1440
|
+
if (msg) setFlash({ kind: "error", msg });
|
|
1441
|
+
} finally {
|
|
1442
|
+
setBusy(false);
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
if (error && !data) return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.ErrorState, { description: error.message, onRetry: reload });
|
|
1446
|
+
if (loading && !data) return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.LoadingState, { variant: "detail" });
|
|
1447
|
+
const open = () => {
|
|
1448
|
+
var _a2, _b2;
|
|
1449
|
+
return setEditing({
|
|
1450
|
+
planId: (_a2 = current == null ? void 0 : current.planId) != null ? _a2 : "",
|
|
1451
|
+
status: (_b2 = current == null ? void 0 : current.status) != null ? _b2 : "active",
|
|
1452
|
+
startsAt: "",
|
|
1453
|
+
endsAt: day(current == null ? void 0 : current.endsAt),
|
|
1454
|
+
seats: (current == null ? void 0 : current.seats) == null ? "" : String(current.seats),
|
|
1455
|
+
notes: ""
|
|
1456
|
+
});
|
|
1457
|
+
};
|
|
1458
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "space-y-4", children: [
|
|
1459
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1460
|
+
Card,
|
|
1461
|
+
{
|
|
1462
|
+
title: "Current plan",
|
|
1463
|
+
actions: can("plans.write") && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Button, { size: "sm", variant: "outline", onClick: open, children: current ? "Change" : "Assign a plan" }),
|
|
1464
|
+
children: current ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1465
|
+
Facts,
|
|
1466
|
+
{
|
|
1467
|
+
rows: [
|
|
1468
|
+
["Plan", (_e = (_d = current.planName) != null ? _d : current.planCode) != null ? _e : current.planId],
|
|
1469
|
+
["Status", /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(StatusPill, { status: current.status }, "s")],
|
|
1470
|
+
["Started", formatDate(current.startsAt)],
|
|
1471
|
+
["Ends", current.endsAt ? formatDate(current.endsAt) : "No end date"],
|
|
1472
|
+
["Seats", (_f = current.seats) != null ? _f : "Plan default"],
|
|
1473
|
+
...current.notes ? [["Notes", current.notes]] : []
|
|
1474
|
+
]
|
|
1475
|
+
}
|
|
1476
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.EmptyState, { compact: true, icon: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(IconTag, { size: 24 }), title: "No plan yet", description: "This company is not on a plan." })
|
|
1477
|
+
}
|
|
1478
|
+
),
|
|
1479
|
+
history.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Card, { title: "History", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("ul", { className: "divide-y divide-line", children: history.map((s) => {
|
|
1480
|
+
var _a2, _b2;
|
|
1481
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("li", { className: "flex flex-wrap items-center gap-3 py-2 text-sm", children: [
|
|
1482
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "min-w-0 flex-1 text-foreground", children: (_b2 = (_a2 = s.planName) != null ? _a2 : s.planCode) != null ? _b2 : s.planId }),
|
|
1483
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(StatusPill, { status: s.status }),
|
|
1484
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "text-xs text-muted", children: [
|
|
1485
|
+
"from ",
|
|
1486
|
+
formatDate(s.startsAt)
|
|
1487
|
+
] })
|
|
1488
|
+
] }, s.id);
|
|
1489
|
+
}) }) }),
|
|
1490
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1491
|
+
import_twreact_ui9.Dialog,
|
|
1492
|
+
{
|
|
1493
|
+
isOpen: editing !== null,
|
|
1494
|
+
onClose: () => setEditing(null),
|
|
1495
|
+
title: current ? "Change subscription" : "Assign a plan",
|
|
1496
|
+
size: "sm",
|
|
1497
|
+
footer: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex justify-end gap-2", children: [
|
|
1498
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Button, { variant: "outline", onClick: () => setEditing(null), children: "Cancel" }),
|
|
1499
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Button, { loading: busy, disabled: !(editing == null ? void 0 : editing.planId), onClick: save, children: "Save" })
|
|
1500
|
+
] }),
|
|
1501
|
+
children: editing && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "space-y-4", children: [
|
|
1502
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Select, { label: "Plan", options: planOptions, loading: plans.loading, value: editing.planId, onChange: (v) => setEditing((s) => ({ ...s, planId: String(v) })), placeholder: "Pick a plan" }),
|
|
1503
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Select, { label: "Status", options: SUB_STATUSES, value: editing.status, onChange: (v) => setEditing((s) => ({ ...s, status: String(v) })) }),
|
|
1504
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "grid gap-4 sm:grid-cols-2", children: [
|
|
1505
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Input, { label: "Starts", type: "date", value: editing.startsAt, onChange: (e) => setEditing((s) => ({ ...s, startsAt: e.target.value })), helperText: "Empty: today" }),
|
|
1506
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Input, { label: "Ends", type: "date", value: editing.endsAt, onChange: (e) => setEditing((s) => ({ ...s, endsAt: e.target.value })), helperText: "Empty: no end date" })
|
|
1507
|
+
] }),
|
|
1508
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Input, { label: "Seats", type: "number", min: "1", value: editing.seats, onChange: (e) => setEditing((s) => ({ ...s, seats: e.target.value })), helperText: "Empty: the plan's default" }),
|
|
1509
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Textarea, { label: "Notes", rows: 2, value: editing.notes, onChange: (e) => setEditing((s) => ({ ...s, notes: e.target.value })) })
|
|
1510
|
+
] })
|
|
1511
|
+
}
|
|
1512
|
+
)
|
|
1513
|
+
] });
|
|
1514
|
+
}
|
|
1515
|
+
function DomainsTab({ company, setFlash }) {
|
|
1516
|
+
var _a;
|
|
1517
|
+
const { client, can } = usePlatform();
|
|
1518
|
+
const sensitive = useSensitive();
|
|
1519
|
+
const id = encodeURIComponent(company.id);
|
|
1520
|
+
const { data, error, loading, reload } = useLoad(`companies/${id}/domains`);
|
|
1521
|
+
const [host, setHost] = (0, import_react10.useState)("");
|
|
1522
|
+
const [adding, setAdding] = (0, import_react10.useState)(false);
|
|
1523
|
+
const [busy, setBusy] = (0, import_react10.useState)(null);
|
|
1524
|
+
const [removing, setRemoving] = (0, import_react10.useState)(null);
|
|
1525
|
+
const writable = can("domains.write");
|
|
1526
|
+
const domains = (_a = data == null ? void 0 : data.domains) != null ? _a : [];
|
|
1527
|
+
const act = async (d, action) => {
|
|
1528
|
+
setBusy(`${d.id}:${action}`);
|
|
1529
|
+
setFlash(null);
|
|
1530
|
+
try {
|
|
1531
|
+
const call = () => client.post(`companies/${id}/domains/${encodeURIComponent(d.id)}`, { action });
|
|
1532
|
+
const r = await (action === "remove" ? sensitive(call) : call());
|
|
1533
|
+
if (action === "verify") {
|
|
1534
|
+
setFlash(r.verified ? { kind: "success", msg: `${d.host} is verified` } : { kind: "warning", msg: `The TXT record for ${d.host} was not found yet. DNS changes can take a while to appear.` });
|
|
1535
|
+
} else setFlash({ kind: "success", msg: action === "remove" ? `${d.host} removed` : `${d.host} is now the primary domain` });
|
|
1536
|
+
await reload();
|
|
1537
|
+
} catch (e) {
|
|
1538
|
+
const msg = errorText(e);
|
|
1539
|
+
if (msg) setFlash({ kind: "error", msg });
|
|
1540
|
+
} finally {
|
|
1541
|
+
setBusy(null);
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
const add = async () => {
|
|
1545
|
+
setBusy("add");
|
|
1546
|
+
setFlash(null);
|
|
1547
|
+
try {
|
|
1548
|
+
await client.post(`companies/${id}/domains`, { host: host.trim() });
|
|
1549
|
+
setHost("");
|
|
1550
|
+
setAdding(false);
|
|
1551
|
+
setFlash({ kind: "success", msg: "Domain added. Publish the TXT record below, then verify." });
|
|
1552
|
+
await reload();
|
|
1553
|
+
} catch (e) {
|
|
1554
|
+
const msg = errorText(e);
|
|
1555
|
+
if (msg) setFlash({ kind: "error", msg });
|
|
1556
|
+
} finally {
|
|
1557
|
+
setBusy(null);
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
if (error && !data) return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.ErrorState, { description: error.message, onRetry: reload });
|
|
1561
|
+
if (loading && !data) return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.LoadingState, { variant: "table", rows: 2 });
|
|
1562
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "space-y-4", children: [
|
|
1563
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
|
|
1564
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-sm text-muted", children: "Custom host names for this company. A domain routes only after its TXT record is verified." }),
|
|
1565
|
+
writable && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Button, { size: "sm", variant: "outline", leftIcon: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(IconPlus, {}), onClick: () => setAdding(true), children: "Add domain" })
|
|
1566
|
+
] }),
|
|
1567
|
+
domains.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.EmptyState, { compact: true, icon: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(IconGlobe, { size: 24 }), title: "No custom domains", description: "The company signs in on the platform's own address." }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("ul", { className: "space-y-3", children: domains.map((d) => {
|
|
1568
|
+
var _a2;
|
|
1569
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("li", { className: "rounded-xl border border-line bg-surface p-4", children: [
|
|
1570
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1571
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "w-full break-all font-mono text-sm text-foreground sm:w-auto sm:min-w-0 sm:flex-1", children: d.host }),
|
|
1572
|
+
d.primary && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Badge, { variant: "primary", size: "sm", children: "Primary" }),
|
|
1573
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(StatusPill, { status: (_a2 = d.status) != null ? _a2 : d.verifiedAt ? "verified" : "pending" }),
|
|
1574
|
+
writable && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex gap-1", children: [
|
|
1575
|
+
!d.verifiedAt && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Button, { size: "sm", loading: busy === `${d.id}:verify`, onClick: () => act(d, "verify"), children: "Verify" }),
|
|
1576
|
+
d.verifiedAt && !d.primary && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Button, { size: "sm", variant: "ghost", loading: busy === `${d.id}:make-primary`, onClick: () => act(d, "make-primary"), children: "Make primary" }),
|
|
1577
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Button, { size: "sm", variant: "ghost", onClick: () => setRemoving(d), children: "Remove" })
|
|
1578
|
+
] })
|
|
1579
|
+
] }),
|
|
1580
|
+
d.verifiedAt && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("p", { className: "mt-1 text-xs text-muted", children: [
|
|
1581
|
+
"Verified ",
|
|
1582
|
+
formatDate(d.verifiedAt)
|
|
1583
|
+
] }),
|
|
1584
|
+
!d.verifiedAt && d.verification && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "mt-3 space-y-2 rounded-lg bg-background p-3", children: [
|
|
1585
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-xs text-muted", children: "Add this TXT record at the domain's DNS provider:" }),
|
|
1586
|
+
[
|
|
1587
|
+
["Name", d.verification.name],
|
|
1588
|
+
["Value", d.verification.value]
|
|
1589
|
+
].map(([k, v]) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
|
|
1590
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "text-xs text-muted", children: k }),
|
|
1591
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "flex min-w-0 items-center gap-2", children: [
|
|
1592
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "break-all font-mono text-xs text-foreground", children: v }),
|
|
1593
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(CopyButton, { value: v })
|
|
1594
|
+
] })
|
|
1595
|
+
] }, k))
|
|
1596
|
+
] })
|
|
1597
|
+
] }, d.id);
|
|
1598
|
+
}) }),
|
|
1599
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1600
|
+
import_twreact_ui9.Dialog,
|
|
1601
|
+
{
|
|
1602
|
+
isOpen: adding,
|
|
1603
|
+
onClose: () => setAdding(false),
|
|
1604
|
+
title: "Add domain",
|
|
1605
|
+
size: "sm",
|
|
1606
|
+
footer: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex justify-end gap-2", children: [
|
|
1607
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Button, { variant: "outline", onClick: () => setAdding(false), children: "Cancel" }),
|
|
1608
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Button, { loading: busy === "add", disabled: !host.trim().includes("."), onClick: add, children: "Add domain" })
|
|
1609
|
+
] }),
|
|
1610
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Input, { label: "Host name", autoFocus: true, placeholder: "app.example.com", value: host, onChange: (e) => setHost(e.target.value), helperText: "The company points this host at the platform and proves it with a TXT record." })
|
|
1611
|
+
}
|
|
1612
|
+
),
|
|
1613
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1614
|
+
import_twreact_ui9.ConfirmDialog,
|
|
1615
|
+
{
|
|
1616
|
+
isOpen: removing !== null,
|
|
1617
|
+
title: `Remove ${removing == null ? void 0 : removing.host}?`,
|
|
1618
|
+
description: "The company can no longer sign in on this host.",
|
|
1619
|
+
confirmLabel: "Remove",
|
|
1620
|
+
destructive: true,
|
|
1621
|
+
onCancel: () => setRemoving(null),
|
|
1622
|
+
onConfirm: () => {
|
|
1623
|
+
const d = removing;
|
|
1624
|
+
setRemoving(null);
|
|
1625
|
+
void act(d, "remove");
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
)
|
|
1629
|
+
] });
|
|
1630
|
+
}
|
|
1631
|
+
function SettingsTab({ company, setFlash }) {
|
|
1632
|
+
var _a;
|
|
1633
|
+
const { can } = usePlatform();
|
|
1634
|
+
const { data, error, loading, reload } = useLoad("settings", { company: company.id });
|
|
1635
|
+
if (error && !data) return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.ErrorState, { description: error.message, onRetry: reload });
|
|
1636
|
+
if (loading && !data) return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.LoadingState, { variant: "detail" });
|
|
1637
|
+
const families = (_a = data == null ? void 0 : data.families) != null ? _a : [];
|
|
1638
|
+
if (!families.length) return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.EmptyState, { compact: true, icon: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(IconSettings, { size: 24 }), title: "No settings", description: "This product defines no company settings." });
|
|
1639
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "space-y-4", children: families.map((f) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SettingsFamily, { family: f, companyId: company.id, version: data.version, writable: can("settings.write"), setFlash, onSaved: reload }, f.family)) });
|
|
1640
|
+
}
|
|
1641
|
+
var initialValues = (family) => Object.fromEntries(family.fields.map((f) => {
|
|
1642
|
+
var _a;
|
|
1643
|
+
return [f.key, f.type === "secret" ? "" : (_a = f.value) != null ? _a : f.type === "boolean" ? false : ""];
|
|
1644
|
+
}));
|
|
1645
|
+
function SettingsFamily({ family, companyId, version, writable, setFlash, onSaved }) {
|
|
1646
|
+
const { client } = usePlatform();
|
|
1647
|
+
const sensitive = useSensitive();
|
|
1648
|
+
const [values, setValues] = (0, import_react10.useState)(() => initialValues(family));
|
|
1649
|
+
const [busy, setBusy] = (0, import_react10.useState)(false);
|
|
1650
|
+
(0, import_react10.useEffect)(() => setValues(initialValues(family)), [family]);
|
|
1651
|
+
const changed = family.fields.filter((f) => {
|
|
1652
|
+
var _a;
|
|
1653
|
+
return f.type === "secret" ? values[f.key] !== "" : JSON.stringify(values[f.key]) !== JSON.stringify((_a = f.value) != null ? _a : f.type === "boolean" ? false : "");
|
|
1654
|
+
});
|
|
1655
|
+
const save = async () => {
|
|
1656
|
+
setBusy(true);
|
|
1657
|
+
setFlash(null);
|
|
1658
|
+
try {
|
|
1659
|
+
const body = Object.fromEntries(changed.map((f) => [f.key, f.type === "number" ? values[f.key] === "" ? null : Number(values[f.key]) : values[f.key]]));
|
|
1660
|
+
await sensitive(() => client.put(`settings/${encodeURIComponent(family.family)}`, { companyId, version, values: body }));
|
|
1661
|
+
setFlash({ kind: "success", msg: `${family.label} saved` });
|
|
1662
|
+
await onSaved();
|
|
1663
|
+
} catch (e) {
|
|
1664
|
+
const msg = errorText(e);
|
|
1665
|
+
if (msg) setFlash({ kind: "error", msg });
|
|
1666
|
+
} finally {
|
|
1667
|
+
setBusy(false);
|
|
1668
|
+
}
|
|
1669
|
+
};
|
|
1670
|
+
const set = (key) => (v) => setValues((s) => ({ ...s, [key]: v }));
|
|
1671
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1672
|
+
Card,
|
|
1673
|
+
{
|
|
1674
|
+
title: family.label,
|
|
1675
|
+
actions: writable && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Button, { size: "sm", loading: busy, disabled: !changed.length, onClick: save, children: "Save" }),
|
|
1676
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "grid gap-4 sm:grid-cols-2", children: family.fields.map((f) => {
|
|
1677
|
+
var _a, _b, _c;
|
|
1678
|
+
const help = [f.help, f.source === "default" ? "Using the default" : null].filter(Boolean).join(" \xB7 ") || void 0;
|
|
1679
|
+
if (f.type === "boolean") {
|
|
1680
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-line px-3 py-2", children: [
|
|
1681
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "min-w-0", children: [
|
|
1682
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "text-sm text-foreground", children: f.label }),
|
|
1683
|
+
help && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "text-xs text-muted", children: help })
|
|
1684
|
+
] }),
|
|
1685
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Switch, { checked: Boolean(values[f.key]), disabled: !writable, onChange: set(f.key), ariaLabel: f.label })
|
|
1686
|
+
] }, f.key);
|
|
1687
|
+
}
|
|
1688
|
+
if (f.type === "select") {
|
|
1689
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_twreact_ui9.Select, { label: f.label, options: (_a = f.options) != null ? _a : [], value: (_b = values[f.key]) != null ? _b : "", disabled: !writable, onChange: (v) => set(f.key)(String(v)), helperText: help }, f.key);
|
|
1690
|
+
}
|
|
1691
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1692
|
+
import_twreact_ui9.Input,
|
|
1693
|
+
{
|
|
1694
|
+
label: f.label,
|
|
1695
|
+
type: f.type === "secret" ? "password" : f.type === "number" ? "number" : f.type === "email" ? "email" : f.type === "url" ? "url" : "text",
|
|
1696
|
+
autoComplete: f.type === "secret" ? "new-password" : void 0,
|
|
1697
|
+
placeholder: f.type === "secret" ? f.set ? "Configured \xB7 leave blank to keep" : "Not set" : void 0,
|
|
1698
|
+
value: (_c = values[f.key]) != null ? _c : "",
|
|
1699
|
+
disabled: !writable,
|
|
1700
|
+
onChange: (e) => set(f.key)(e.target.value),
|
|
1701
|
+
helperText: help
|
|
1702
|
+
},
|
|
1703
|
+
f.key
|
|
1704
|
+
);
|
|
1705
|
+
}) })
|
|
1706
|
+
}
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
function SupportTab({ company }) {
|
|
1710
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SupportGrants, { companyId: company.id, companyName: company.displayName });
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// src/platform/views/CompanyDetail.jsx
|
|
1714
|
+
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
1177
1715
|
var SETUP_STATES = ["draft", "provisioning", "provisioning_failed"];
|
|
1178
1716
|
function CompanyDetailView({ companyId }) {
|
|
1179
1717
|
var _a;
|
|
1180
|
-
const { client, Link, href, can, companyTabs, companyLoginUrl, companyLoginPath } = usePlatform();
|
|
1718
|
+
const { client, Link, href, can, companyTabs, companyLoginUrl, companyLoginPath, features = [] } = usePlatform();
|
|
1181
1719
|
const sensitive = useSensitive();
|
|
1182
1720
|
const { data, error, reload } = useLoad(`companies/${encodeURIComponent(companyId)}`);
|
|
1183
|
-
const [tab, setTab] = (0,
|
|
1184
|
-
const [flash, setFlash] = (0,
|
|
1185
|
-
const [secret, setSecret] = (0,
|
|
1186
|
-
const run = (0,
|
|
1721
|
+
const [tab, setTab] = (0, import_react11.useState)(0);
|
|
1722
|
+
const [flash, setFlash] = (0, import_react11.useState)(null);
|
|
1723
|
+
const [secret, setSecret] = (0, import_react11.useState)(null);
|
|
1724
|
+
const run = (0, import_react11.useCallback)(
|
|
1187
1725
|
async (fn, ok, { stepUp = false } = {}) => {
|
|
1188
1726
|
setFlash(null);
|
|
1189
1727
|
try {
|
|
@@ -1199,69 +1737,70 @@ function CompanyDetailView({ companyId }) {
|
|
|
1199
1737
|
},
|
|
1200
1738
|
[reload, sensitive]
|
|
1201
1739
|
);
|
|
1202
|
-
if (error && !data) return /* @__PURE__ */ (0,
|
|
1203
|
-
if (!data) return /* @__PURE__ */ (0,
|
|
1740
|
+
if (error && !data) return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "mx-auto max-w-6xl px-4 py-6 sm:px-6", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.ErrorState, { description: error.message, onRetry: reload }) });
|
|
1741
|
+
if (!data) return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "mx-auto max-w-6xl px-4 py-6 sm:px-6", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.LoadingState, { variant: "detail" }) });
|
|
1204
1742
|
const company = data.company;
|
|
1205
1743
|
const database = (_a = company.database) != null ? _a : { configured: false };
|
|
1206
1744
|
const ctx = { company, users: data.users, run, setFlash, setSecret, client, can, reload };
|
|
1207
1745
|
const tabs = [
|
|
1208
|
-
{ id: "overview", label: "Overview", render: () => /* @__PURE__ */ (0,
|
|
1209
|
-
{ id: "branches", label: "Branches", render: () => /* @__PURE__ */ (0,
|
|
1210
|
-
{ id: "database", label: "Database", render: () => /* @__PURE__ */ (0,
|
|
1211
|
-
{ id: "admins", label: "Admins", render: () => /* @__PURE__ */ (0,
|
|
1746
|
+
{ id: "overview", label: "Overview", render: () => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(OverviewTab, { ...ctx }) },
|
|
1747
|
+
{ id: "branches", label: "Branches", render: () => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(BranchesTab, { ...ctx }) },
|
|
1748
|
+
{ id: "database", label: "Database", render: () => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(DatabaseTab, { ...ctx }) },
|
|
1749
|
+
{ id: "admins", label: "Admins", render: () => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(AdminsTab, { ...ctx }) },
|
|
1750
|
+
...featureTabs({ features, can }).map(({ id, label, Component }) => ({ id, label, render: () => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Component, { ...ctx }) })),
|
|
1212
1751
|
...(companyTabs != null ? companyTabs : []).map((t) => ({ id: t.id, label: t.label, render: () => t.render({ company, reload, client, sensitive }) })),
|
|
1213
|
-
{ id: "activity", label: "Activity", render: () => /* @__PURE__ */ (0,
|
|
1752
|
+
{ id: "activity", label: "Activity", render: () => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ActivityTab, { ...ctx }) }
|
|
1214
1753
|
];
|
|
1215
|
-
return /* @__PURE__ */ (0,
|
|
1216
|
-
/* @__PURE__ */ (0,
|
|
1217
|
-
|
|
1754
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "mx-auto max-w-6xl space-y-5 px-4 py-6 sm:px-6", children: [
|
|
1755
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
1756
|
+
import_twreact_ui10.PageHeader,
|
|
1218
1757
|
{
|
|
1219
1758
|
title: company.displayName,
|
|
1220
|
-
subtitle: /* @__PURE__ */ (0,
|
|
1221
|
-
/* @__PURE__ */ (0,
|
|
1222
|
-
/* @__PURE__ */ (0,
|
|
1223
|
-
/* @__PURE__ */ (0,
|
|
1759
|
+
subtitle: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1760
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "font-mono", children: companyLoginPath(company.slug) }),
|
|
1761
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(StatusPill, { status: company.status }),
|
|
1762
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ModePill, { mode: database.configured ? database.mode : null })
|
|
1224
1763
|
] }),
|
|
1225
1764
|
back: { href: href("companies"), label: "Companies" },
|
|
1226
1765
|
linkAs: Link
|
|
1227
1766
|
}
|
|
1228
1767
|
),
|
|
1229
|
-
flash && /* @__PURE__ */ (0,
|
|
1230
|
-
secret && /* @__PURE__ */ (0,
|
|
1231
|
-
/* @__PURE__ */ (0,
|
|
1232
|
-
/* @__PURE__ */ (0,
|
|
1233
|
-
/* @__PURE__ */ (0,
|
|
1768
|
+
flash && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Alert, { variant: flash.kind, dismissible: true, onDismiss: () => setFlash(null), children: flash.msg }),
|
|
1769
|
+
secret && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(OneTimeSecret, { title: secret.title, loginUrl: companyLoginUrl(company.slug), email: secret.email, password: secret.password, onDone: () => setSecret(null) }),
|
|
1770
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_twreact_ui10.Tabs, { index: tab, onChange: setTab, variant: "line", children: [
|
|
1771
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Tabs.List, { children: tabs.map((t) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Tabs.Tab, { children: t.label }, t.id)) }),
|
|
1772
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Tabs.Panels, { children: tabs.map((t, i) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Tabs.Panel, { children: i === tab ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "pt-4", children: t.render() }) : null }, t.id)) })
|
|
1234
1773
|
] })
|
|
1235
1774
|
] });
|
|
1236
1775
|
}
|
|
1237
1776
|
function OverviewTab({ company, users, run, setSecret, client, can }) {
|
|
1238
1777
|
var _a, _b, _c, _d, _e, _f;
|
|
1239
1778
|
const { companyLoginUrl } = usePlatform();
|
|
1240
|
-
const [confirm, setConfirm] = (0,
|
|
1241
|
-
const [editing, setEditing] = (0,
|
|
1779
|
+
const [confirm, setConfirm] = (0, import_react11.useState)(null);
|
|
1780
|
+
const [editing, setEditing] = (0, import_react11.useState)(false);
|
|
1242
1781
|
const setup = SETUP_STATES.includes(company.status);
|
|
1243
1782
|
const writable = can("companies.write");
|
|
1244
1783
|
const db = (_a = company.database) != null ? _a : {};
|
|
1245
1784
|
const url = companyLoginUrl(company.slug);
|
|
1246
1785
|
const LABEL = { active: "Activate", suspended: "Suspend", archived: "Archive" };
|
|
1247
|
-
return /* @__PURE__ */ (0,
|
|
1248
|
-
/* @__PURE__ */ (0,
|
|
1249
|
-
setup && writable && /* @__PURE__ */ (0,
|
|
1250
|
-
/* @__PURE__ */ (0,
|
|
1786
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "grid gap-5 [grid-template-columns:repeat(auto-fit,minmax(min(20rem,100%),1fr))]", children: [
|
|
1787
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "space-y-5", children: [
|
|
1788
|
+
setup && writable && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SetupPanel, { company, run, setSecret, client }),
|
|
1789
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
1251
1790
|
Card,
|
|
1252
1791
|
{
|
|
1253
1792
|
title: "Company",
|
|
1254
|
-
actions: writable && /* @__PURE__ */ (0,
|
|
1255
|
-
children: /* @__PURE__ */ (0,
|
|
1793
|
+
actions: writable && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { size: "sm", variant: "outline", onClick: () => setEditing(true), children: "Edit details" }),
|
|
1794
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
1256
1795
|
Facts,
|
|
1257
1796
|
{
|
|
1258
1797
|
rows: [
|
|
1259
1798
|
["Name", company.displayName],
|
|
1260
1799
|
[
|
|
1261
1800
|
"Sign-in address",
|
|
1262
|
-
/* @__PURE__ */ (0,
|
|
1263
|
-
/* @__PURE__ */ (0,
|
|
1264
|
-
/* @__PURE__ */ (0,
|
|
1801
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "flex items-center gap-2", children: [
|
|
1802
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "truncate font-mono text-xs", children: url }),
|
|
1803
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(CopyButton, { value: url })
|
|
1265
1804
|
] }, "l")
|
|
1266
1805
|
],
|
|
1267
1806
|
["Branches", (_c = (_b = company.branches) == null ? void 0 : _b.length) != null ? _c : "\u2014"],
|
|
@@ -1274,17 +1813,17 @@ function OverviewTab({ company, users, run, setSecret, client, can }) {
|
|
|
1274
1813
|
}
|
|
1275
1814
|
)
|
|
1276
1815
|
] }),
|
|
1277
|
-
/* @__PURE__ */ (0,
|
|
1278
|
-
/* @__PURE__ */ (0,
|
|
1279
|
-
/* @__PURE__ */ (0,
|
|
1280
|
-
writable && company.status !== "active" && /* @__PURE__ */ (0,
|
|
1281
|
-
writable && company.status === "active" && /* @__PURE__ */ (0,
|
|
1282
|
-
writable && company.status !== "archived" && /* @__PURE__ */ (0,
|
|
1816
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Card, { title: "Lifecycle", children: setup ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-sm text-muted", children: "Activation, suspension and archiving become available once the company's database is set up." }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
|
|
1817
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "mb-3 text-sm text-muted", children: "Suspending immediately blocks every sign-in and request for this company. Data is kept." }),
|
|
1818
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex flex-col gap-2", children: [
|
|
1819
|
+
writable && company.status !== "active" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { variant: "outline", leftIcon: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(IconPlay, {}), onClick: () => setConfirm("active"), children: "Activate" }),
|
|
1820
|
+
writable && company.status === "active" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { variant: "outline", leftIcon: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(IconPause, {}), onClick: () => setConfirm("suspended"), children: "Suspend" }),
|
|
1821
|
+
writable && company.status !== "archived" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { variant: "outline", leftIcon: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(IconArchive, {}), onClick: () => setConfirm("archived"), children: "Archive" })
|
|
1283
1822
|
] })
|
|
1284
1823
|
] }) }),
|
|
1285
|
-
editing && /* @__PURE__ */ (0,
|
|
1286
|
-
/* @__PURE__ */ (0,
|
|
1287
|
-
|
|
1824
|
+
editing && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(EditDetails, { company, run, client, onClose: () => setEditing(false) }),
|
|
1825
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
1826
|
+
import_twreact_ui10.ConfirmDialog,
|
|
1288
1827
|
{
|
|
1289
1828
|
isOpen: confirm !== null,
|
|
1290
1829
|
title: `${(_e = LABEL[confirm]) != null ? _e : ""} ${company.displayName}?`,
|
|
@@ -1303,11 +1842,11 @@ function OverviewTab({ company, users, run, setSecret, client, can }) {
|
|
|
1303
1842
|
}
|
|
1304
1843
|
function EditDetails({ company, run, client, onClose }) {
|
|
1305
1844
|
const { companyLoginPath } = usePlatform();
|
|
1306
|
-
const [name, setName] = (0,
|
|
1307
|
-
const [slug, setSlug] = (0,
|
|
1308
|
-
const [slugState, setSlugState] = (0,
|
|
1309
|
-
const [busy, setBusy] = (0,
|
|
1310
|
-
(0,
|
|
1845
|
+
const [name, setName] = (0, import_react11.useState)(company.displayName);
|
|
1846
|
+
const [slug, setSlug] = (0, import_react11.useState)(company.slug);
|
|
1847
|
+
const [slugState, setSlugState] = (0, import_react11.useState)({ available: true });
|
|
1848
|
+
const [busy, setBusy] = (0, import_react11.useState)(false);
|
|
1849
|
+
(0, import_react11.useEffect)(() => {
|
|
1311
1850
|
if (slug === company.slug) {
|
|
1312
1851
|
setSlugState({ available: true });
|
|
1313
1852
|
return void 0;
|
|
@@ -1321,17 +1860,17 @@ function EditDetails({ company, run, client, onClose }) {
|
|
|
1321
1860
|
}, 350);
|
|
1322
1861
|
return () => clearTimeout(t);
|
|
1323
1862
|
}, [slug, company.slug, client]);
|
|
1324
|
-
return /* @__PURE__ */ (0,
|
|
1325
|
-
|
|
1863
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
1864
|
+
import_twreact_ui10.Dialog,
|
|
1326
1865
|
{
|
|
1327
1866
|
isOpen: true,
|
|
1328
1867
|
onClose,
|
|
1329
1868
|
title: "Edit company details",
|
|
1330
1869
|
size: "md",
|
|
1331
|
-
footer: /* @__PURE__ */ (0,
|
|
1332
|
-
/* @__PURE__ */ (0,
|
|
1333
|
-
/* @__PURE__ */ (0,
|
|
1334
|
-
|
|
1870
|
+
footer: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex justify-end gap-2", children: [
|
|
1871
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { variant: "outline", onClick: onClose, children: "Cancel" }),
|
|
1872
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
1873
|
+
import_twreact_ui10.Button,
|
|
1335
1874
|
{
|
|
1336
1875
|
loading: busy,
|
|
1337
1876
|
disabled: !name.trim() || slugState.available !== true,
|
|
@@ -1349,10 +1888,10 @@ function EditDetails({ company, run, client, onClose }) {
|
|
|
1349
1888
|
}
|
|
1350
1889
|
)
|
|
1351
1890
|
] }),
|
|
1352
|
-
children: /* @__PURE__ */ (0,
|
|
1353
|
-
/* @__PURE__ */ (0,
|
|
1354
|
-
/* @__PURE__ */ (0,
|
|
1355
|
-
|
|
1891
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "space-y-4", children: [
|
|
1892
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Input, { label: "Company name", value: name, onChange: (e) => setName(e.target.value) }),
|
|
1893
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
1894
|
+
import_twreact_ui10.Input,
|
|
1356
1895
|
{
|
|
1357
1896
|
label: "URL slug",
|
|
1358
1897
|
value: slug,
|
|
@@ -1370,10 +1909,10 @@ function SetupPanel({ company, run, setSecret, client }) {
|
|
|
1370
1909
|
const provisioner = useProvisioner();
|
|
1371
1910
|
const job = company.lastJob;
|
|
1372
1911
|
const stored = ((_a = company.database) == null ? void 0 : _a.configured) ? company.database.mode : null;
|
|
1373
|
-
const [db, setDb] = (0,
|
|
1374
|
-
const [adminName, setAdminName] = (0,
|
|
1375
|
-
const [adminEmail, setAdminEmail] = (0,
|
|
1376
|
-
const [busy, setBusy] = (0,
|
|
1912
|
+
const [db, setDb] = (0, import_react11.useState)({ mode: stored === "external" ? "external" : "managed", url: "", report: null, testedUrl: null });
|
|
1913
|
+
const [adminName, setAdminName] = (0, import_react11.useState)("");
|
|
1914
|
+
const [adminEmail, setAdminEmail] = (0, import_react11.useState)("");
|
|
1915
|
+
const [busy, setBusy] = (0, import_react11.useState)(false);
|
|
1377
1916
|
const ready = databaseReady(db, provisioner, { acceptStates: ["empty", "company"], keepStored: stored === "external" });
|
|
1378
1917
|
const adminOk = !adminName.trim() && !adminEmail.trim() || adminName.trim() && EMAIL_RE.test(adminEmail.trim());
|
|
1379
1918
|
const provision = async () => {
|
|
@@ -1394,13 +1933,13 @@ function SetupPanel({ company, run, setSecret, client }) {
|
|
|
1394
1933
|
setBusy(false);
|
|
1395
1934
|
};
|
|
1396
1935
|
const lastError = (_b = job == null ? void 0 : job.lastError) != null ? _b : job == null ? void 0 : job.last_error;
|
|
1397
|
-
return /* @__PURE__ */ (0,
|
|
1398
|
-
/* @__PURE__ */ (0,
|
|
1399
|
-
(job == null ? void 0 : job.status) === "failed" && /* @__PURE__ */ (0,
|
|
1400
|
-
/* @__PURE__ */ (0,
|
|
1401
|
-
jobErrorHint(lastError) && /* @__PURE__ */ (0,
|
|
1936
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Card, { title: "Finish setup", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "space-y-4", children: [
|
|
1937
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-sm text-muted", children: "This company's database is not ready yet, so nobody can sign in." }),
|
|
1938
|
+
(job == null ? void 0 : job.status) === "failed" && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_twreact_ui10.Alert, { variant: "error", title: `Last attempt failed${job.step ? ` at \u201C${job.step}\u201D` : ""}`, children: [
|
|
1939
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "block break-words font-mono text-xs", children: lastError }),
|
|
1940
|
+
jobErrorHint(lastError) && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "mt-1 block", children: jobErrorHint(lastError) })
|
|
1402
1941
|
] }),
|
|
1403
|
-
/* @__PURE__ */ (0,
|
|
1942
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
1404
1943
|
DatabaseChoice,
|
|
1405
1944
|
{
|
|
1406
1945
|
value: db,
|
|
@@ -1412,25 +1951,25 @@ function SetupPanel({ company, run, setSecret, client }) {
|
|
|
1412
1951
|
compact: true
|
|
1413
1952
|
}
|
|
1414
1953
|
),
|
|
1415
|
-
/* @__PURE__ */ (0,
|
|
1416
|
-
/* @__PURE__ */ (0,
|
|
1417
|
-
/* @__PURE__ */ (0,
|
|
1418
|
-
/* @__PURE__ */ (0,
|
|
1419
|
-
/* @__PURE__ */ (0,
|
|
1420
|
-
/* @__PURE__ */ (0,
|
|
1954
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { children: [
|
|
1955
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "text-sm font-medium text-foreground", children: "First admin (recommended)" }),
|
|
1956
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "mb-2 text-xs text-muted", children: "A temporary password is generated and shown once when setup finishes." }),
|
|
1957
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "grid gap-3 [grid-template-columns:repeat(auto-fit,minmax(12rem,1fr))]", children: [
|
|
1958
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Input, { "aria-label": "First admin full name", placeholder: "Full name", value: adminName, onChange: (e) => setAdminName(e.target.value) }),
|
|
1959
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Input, { "aria-label": "First admin email", type: "email", placeholder: "email@company.com", value: adminEmail, onChange: (e) => setAdminEmail(e.target.value) })
|
|
1421
1960
|
] })
|
|
1422
1961
|
] }),
|
|
1423
|
-
/* @__PURE__ */ (0,
|
|
1424
|
-
/* @__PURE__ */ (0,
|
|
1425
|
-
busy && /* @__PURE__ */ (0,
|
|
1962
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex flex-wrap items-center gap-3", children: [
|
|
1963
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { loading: busy, disabled: !ready || !adminOk, leftIcon: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(IconRefresh, {}), onClick: provision, children: (job == null ? void 0 : job.status) === "failed" ? "Retry setup" : "Provision database" }),
|
|
1964
|
+
busy && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "text-xs text-muted", children: "Applying the schema. This can take a minute\u2026" })
|
|
1426
1965
|
] })
|
|
1427
1966
|
] }) });
|
|
1428
1967
|
}
|
|
1429
1968
|
function BranchesTab({ company, run, client, can }) {
|
|
1430
1969
|
var _a;
|
|
1431
|
-
const [editing, setEditing] = (0,
|
|
1432
|
-
const [code, setCode] = (0,
|
|
1433
|
-
const [name, setName] = (0,
|
|
1970
|
+
const [editing, setEditing] = (0, import_react11.useState)(null);
|
|
1971
|
+
const [code, setCode] = (0, import_react11.useState)("");
|
|
1972
|
+
const [name, setName] = (0, import_react11.useState)("");
|
|
1434
1973
|
const writable = can("companies.write");
|
|
1435
1974
|
const branches = (_a = company.branches) != null ? _a : [];
|
|
1436
1975
|
const open = (b) => {
|
|
@@ -1443,23 +1982,23 @@ function BranchesTab({ company, run, client, can }) {
|
|
|
1443
1982
|
const ok = editing.id ? await run(() => client.patch(`companies/${company.id}/branches/${editing.id}`, { name: name.trim() }), "Branch renamed") : await run(() => client.post(`companies/${company.id}/branches`, { code: code.trim().toUpperCase(), name: name.trim() }), "Branch added");
|
|
1444
1983
|
if (ok) setEditing(null);
|
|
1445
1984
|
};
|
|
1446
|
-
return /* @__PURE__ */ (0,
|
|
1985
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
|
|
1447
1986
|
Card,
|
|
1448
1987
|
{
|
|
1449
1988
|
title: "Branches",
|
|
1450
|
-
actions: writable && /* @__PURE__ */ (0,
|
|
1989
|
+
actions: writable && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { size: "sm", leftIcon: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(IconPlus, {}), onClick: () => open(null), children: "Add branch" }),
|
|
1451
1990
|
children: [
|
|
1452
|
-
branches.length === 0 ? /* @__PURE__ */ (0,
|
|
1453
|
-
/* @__PURE__ */ (0,
|
|
1454
|
-
/* @__PURE__ */ (0,
|
|
1991
|
+
branches.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.EmptyState, { title: "No branches", compact: true }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ul", { className: "divide-y divide-line", children: branches.map((b) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("li", { className: "flex flex-wrap items-center gap-3 py-2.5", children: [
|
|
1992
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "w-20 shrink-0 font-mono text-xs text-muted", children: b.code }),
|
|
1993
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "min-w-0 flex-1 text-sm text-foreground", children: [
|
|
1455
1994
|
b.name,
|
|
1456
|
-
b.id === company.defaultBranchId && /* @__PURE__ */ (0,
|
|
1995
|
+
b.id === company.defaultBranchId && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "ml-2 text-xs text-muted", children: "default" })
|
|
1457
1996
|
] }),
|
|
1458
|
-
/* @__PURE__ */ (0,
|
|
1459
|
-
writable && /* @__PURE__ */ (0,
|
|
1460
|
-
/* @__PURE__ */ (0,
|
|
1461
|
-
b.id !== company.defaultBranchId && /* @__PURE__ */ (0,
|
|
1462
|
-
|
|
1997
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(StatusPill, { status: b.status }),
|
|
1998
|
+
writable && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex gap-1", children: [
|
|
1999
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { size: "sm", variant: "ghost", onClick: () => open(b), children: "Rename" }),
|
|
2000
|
+
b.id !== company.defaultBranchId && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2001
|
+
import_twreact_ui10.Button,
|
|
1463
2002
|
{
|
|
1464
2003
|
size: "sm",
|
|
1465
2004
|
variant: "ghost",
|
|
@@ -1472,20 +2011,20 @@ function BranchesTab({ company, run, client, can }) {
|
|
|
1472
2011
|
)
|
|
1473
2012
|
] })
|
|
1474
2013
|
] }, b.id)) }),
|
|
1475
|
-
/* @__PURE__ */ (0,
|
|
1476
|
-
|
|
2014
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2015
|
+
import_twreact_ui10.Dialog,
|
|
1477
2016
|
{
|
|
1478
2017
|
isOpen: editing !== null,
|
|
1479
2018
|
onClose: () => setEditing(null),
|
|
1480
2019
|
title: (editing == null ? void 0 : editing.id) ? `Rename ${editing.code}` : "Add branch",
|
|
1481
2020
|
size: "sm",
|
|
1482
|
-
footer: /* @__PURE__ */ (0,
|
|
1483
|
-
/* @__PURE__ */ (0,
|
|
1484
|
-
/* @__PURE__ */ (0,
|
|
2021
|
+
footer: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex justify-end gap-2", children: [
|
|
2022
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { variant: "outline", onClick: () => setEditing(null), children: "Cancel" }),
|
|
2023
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { disabled: !name.trim() || !(editing == null ? void 0 : editing.id) && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/.test(code), onClick: save, children: "Save" })
|
|
1485
2024
|
] }),
|
|
1486
|
-
children: /* @__PURE__ */ (0,
|
|
1487
|
-
!(editing == null ? void 0 : editing.id) && /* @__PURE__ */ (0,
|
|
1488
|
-
/* @__PURE__ */ (0,
|
|
2025
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "space-y-4", children: [
|
|
2026
|
+
!(editing == null ? void 0 : editing.id) && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Input, { label: "Code", value: code, onChange: (e) => setCode(e.target.value.toUpperCase()), helperText: "Letters, digits, - and _. Cannot be changed later." }),
|
|
2027
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Input, { label: "Name", value: name, onChange: (e) => setName(e.target.value) })
|
|
1489
2028
|
] })
|
|
1490
2029
|
}
|
|
1491
2030
|
)
|
|
@@ -1496,29 +2035,29 @@ function BranchesTab({ company, run, client, can }) {
|
|
|
1496
2035
|
function DatabaseTab({ company, run, setFlash, client, can }) {
|
|
1497
2036
|
var _a, _b, _c, _d, _e;
|
|
1498
2037
|
const db = (_a = company.database) != null ? _a : { configured: false };
|
|
1499
|
-
const [busy, setBusy] = (0,
|
|
1500
|
-
const [steps, setSteps] = (0,
|
|
1501
|
-
const [replacing, setReplacing] = (0,
|
|
1502
|
-
const [newUrl, setNewUrl] = (0,
|
|
1503
|
-
const [report, setReport] = (0,
|
|
1504
|
-
const [revealing, setRevealing] = (0,
|
|
1505
|
-
const [reason, setReason] = (0,
|
|
1506
|
-
const [revealed, setRevealed] = (0,
|
|
1507
|
-
if (!db.configured) return /* @__PURE__ */ (0,
|
|
2038
|
+
const [busy, setBusy] = (0, import_react11.useState)(null);
|
|
2039
|
+
const [steps, setSteps] = (0, import_react11.useState)(null);
|
|
2040
|
+
const [replacing, setReplacing] = (0, import_react11.useState)(false);
|
|
2041
|
+
const [newUrl, setNewUrl] = (0, import_react11.useState)("");
|
|
2042
|
+
const [report, setReport] = (0, import_react11.useState)(null);
|
|
2043
|
+
const [revealing, setRevealing] = (0, import_react11.useState)(false);
|
|
2044
|
+
const [reason, setReason] = (0, import_react11.useState)("");
|
|
2045
|
+
const [revealed, setRevealed] = (0, import_react11.useState)(null);
|
|
2046
|
+
if (!db.configured) return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Card, { title: "Database", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-sm text-muted", children: "No database yet. Provision it from the Overview tab." }) });
|
|
1508
2047
|
const writable = can("db.write");
|
|
1509
|
-
return /* @__PURE__ */ (0,
|
|
1510
|
-
/* @__PURE__ */ (0,
|
|
1511
|
-
/* @__PURE__ */ (0,
|
|
2048
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "grid gap-5 [grid-template-columns:repeat(auto-fit,minmax(min(20rem,100%),1fr))]", children: [
|
|
2049
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(Card, { title: "Connection", children: [
|
|
2050
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
1512
2051
|
Facts,
|
|
1513
2052
|
{
|
|
1514
2053
|
rows: [
|
|
1515
|
-
["Type", /* @__PURE__ */ (0,
|
|
1516
|
-
["Target", /* @__PURE__ */ (0,
|
|
1517
|
-
["Status", /* @__PURE__ */ (0,
|
|
1518
|
-
["Health", /* @__PURE__ */ (0,
|
|
1519
|
-
/* @__PURE__ */ (0,
|
|
2054
|
+
["Type", /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ModePill, { mode: db.mode }, "m")],
|
|
2055
|
+
["Target", /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "font-mono text-xs", children: (_b = db.target) != null ? _b : "\u2014" }, "t")],
|
|
2056
|
+
["Status", /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(StatusPill, { status: db.status }, "s")],
|
|
2057
|
+
["Health", /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
|
|
2058
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(HealthDot, { ok: db.lastCheckOk }),
|
|
1520
2059
|
" ",
|
|
1521
|
-
db.lastCheckAt && /* @__PURE__ */ (0,
|
|
2060
|
+
db.lastCheckAt && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "text-xs text-muted", children: [
|
|
1522
2061
|
"\xB7 ",
|
|
1523
2062
|
timeAgo(db.lastCheckAt)
|
|
1524
2063
|
] })
|
|
@@ -1530,23 +2069,23 @@ function DatabaseTab({ company, run, setFlash, client, can }) {
|
|
|
1530
2069
|
]
|
|
1531
2070
|
}
|
|
1532
2071
|
),
|
|
1533
|
-
db.lastCheckOk === false && db.lastCheckError && /* @__PURE__ */ (0,
|
|
1534
|
-
steps && /* @__PURE__ */ (0,
|
|
2072
|
+
db.lastCheckOk === false && db.lastCheckError && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "mt-4 rounded-lg bg-error-50 p-3 font-mono text-xs text-error-800", children: db.lastCheckError }),
|
|
2073
|
+
steps && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ul", { className: "mt-4 space-y-1 rounded-lg bg-background p-3 text-xs text-muted", children: steps.map((s) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("li", { children: [
|
|
1535
2074
|
"\u2713 ",
|
|
1536
2075
|
s
|
|
1537
2076
|
] }, s)) }),
|
|
1538
|
-
revealed && /* @__PURE__ */ (0,
|
|
1539
|
-
/* @__PURE__ */ (0,
|
|
1540
|
-
/* @__PURE__ */ (0,
|
|
1541
|
-
/* @__PURE__ */ (0,
|
|
1542
|
-
/* @__PURE__ */ (0,
|
|
2077
|
+
revealed && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "mt-4 rounded-lg border border-warning-200 bg-warning-50 p-3", children: [
|
|
2078
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "mb-1 text-xs font-medium text-warning-800", children: "Database URL (audited, shown once)" }),
|
|
2079
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
2080
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("code", { className: "min-w-0 flex-1 break-all text-xs text-foreground", children: revealed }),
|
|
2081
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(CopyButton, { value: revealed })
|
|
1543
2082
|
] }),
|
|
1544
|
-
/* @__PURE__ */ (0,
|
|
2083
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: "mt-2 text-xs text-muted underline", onClick: () => setRevealed(null), children: "Hide" })
|
|
1545
2084
|
] })
|
|
1546
2085
|
] }),
|
|
1547
|
-
writable && /* @__PURE__ */ (0,
|
|
1548
|
-
/* @__PURE__ */ (0,
|
|
1549
|
-
|
|
2086
|
+
writable && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Card, { title: "Actions", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex flex-col gap-2", children: [
|
|
2087
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2088
|
+
import_twreact_ui10.Button,
|
|
1550
2089
|
{
|
|
1551
2090
|
variant: "outline",
|
|
1552
2091
|
loading: busy === "test",
|
|
@@ -1563,8 +2102,8 @@ function DatabaseTab({ company, run, setFlash, client, can }) {
|
|
|
1563
2102
|
children: "Test connection"
|
|
1564
2103
|
}
|
|
1565
2104
|
),
|
|
1566
|
-
/* @__PURE__ */ (0,
|
|
1567
|
-
|
|
2105
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2106
|
+
import_twreact_ui10.Button,
|
|
1568
2107
|
{
|
|
1569
2108
|
variant: "outline",
|
|
1570
2109
|
loading: busy === "sync",
|
|
@@ -1577,12 +2116,12 @@ function DatabaseTab({ company, run, setFlash, client, can }) {
|
|
|
1577
2116
|
children: "Sync schema"
|
|
1578
2117
|
}
|
|
1579
2118
|
),
|
|
1580
|
-
db.mode === "external" && /* @__PURE__ */ (0,
|
|
1581
|
-
can("db.reveal") && /* @__PURE__ */ (0,
|
|
1582
|
-
/* @__PURE__ */ (0,
|
|
2119
|
+
db.mode === "external" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { variant: "outline", onClick: () => setReplacing(true), children: "Replace URL" }),
|
|
2120
|
+
can("db.reveal") && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { variant: "ghost", onClick: () => setRevealing(true), children: "Reveal URL\u2026" }),
|
|
2121
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "mt-1 text-xs text-muted", children: "Sync adds missing tables, columns and indexes. It never drops or renames anything." })
|
|
1583
2122
|
] }) }),
|
|
1584
|
-
/* @__PURE__ */ (0,
|
|
1585
|
-
|
|
2123
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
|
|
2124
|
+
import_twreact_ui10.Dialog,
|
|
1586
2125
|
{
|
|
1587
2126
|
isOpen: replacing,
|
|
1588
2127
|
onClose: () => {
|
|
@@ -1592,10 +2131,10 @@ function DatabaseTab({ company, run, setFlash, client, can }) {
|
|
|
1592
2131
|
},
|
|
1593
2132
|
title: "Replace database URL",
|
|
1594
2133
|
size: "md",
|
|
1595
|
-
footer: /* @__PURE__ */ (0,
|
|
1596
|
-
/* @__PURE__ */ (0,
|
|
1597
|
-
/* @__PURE__ */ (0,
|
|
1598
|
-
|
|
2134
|
+
footer: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex justify-end gap-2", children: [
|
|
2135
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { variant: "outline", onClick: () => setReplacing(false), children: "Cancel" }),
|
|
2136
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2137
|
+
import_twreact_ui10.Button,
|
|
1599
2138
|
{
|
|
1600
2139
|
disabled: !(report == null ? void 0 : report.ok) || report.state !== "company",
|
|
1601
2140
|
loading: busy === "replace",
|
|
@@ -1614,11 +2153,11 @@ function DatabaseTab({ company, run, setFlash, client, can }) {
|
|
|
1614
2153
|
)
|
|
1615
2154
|
] }),
|
|
1616
2155
|
children: [
|
|
1617
|
-
/* @__PURE__ */ (0,
|
|
1618
|
-
/* @__PURE__ */ (0,
|
|
1619
|
-
/* @__PURE__ */ (0,
|
|
1620
|
-
/* @__PURE__ */ (0,
|
|
1621
|
-
|
|
2156
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "mb-3 text-sm text-muted", children: "Use this after moving the database or rotating its password. The new URL must point at this company's existing database." }),
|
|
2157
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-end gap-2", children: [
|
|
2158
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Input, { label: "New database URL", type: "password", autoComplete: "off", value: newUrl, onChange: (e) => (setNewUrl(e.target.value), setReport(null)) }) }),
|
|
2159
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2160
|
+
import_twreact_ui10.Button,
|
|
1622
2161
|
{
|
|
1623
2162
|
variant: "outline",
|
|
1624
2163
|
disabled: !newUrl.trim(),
|
|
@@ -1633,21 +2172,21 @@ function DatabaseTab({ company, run, setFlash, client, can }) {
|
|
|
1633
2172
|
}
|
|
1634
2173
|
)
|
|
1635
2174
|
] }),
|
|
1636
|
-
report && /* @__PURE__ */ (0,
|
|
2175
|
+
report && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(InspectResult, { report })
|
|
1637
2176
|
]
|
|
1638
2177
|
}
|
|
1639
2178
|
),
|
|
1640
|
-
/* @__PURE__ */ (0,
|
|
1641
|
-
|
|
2179
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
|
|
2180
|
+
import_twreact_ui10.Dialog,
|
|
1642
2181
|
{
|
|
1643
2182
|
isOpen: revealing,
|
|
1644
2183
|
onClose: () => setRevealing(false),
|
|
1645
2184
|
title: "Reveal database URL",
|
|
1646
2185
|
size: "md",
|
|
1647
|
-
footer: /* @__PURE__ */ (0,
|
|
1648
|
-
/* @__PURE__ */ (0,
|
|
1649
|
-
/* @__PURE__ */ (0,
|
|
1650
|
-
|
|
2186
|
+
footer: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex justify-end gap-2", children: [
|
|
2187
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { variant: "outline", onClick: () => setRevealing(false), children: "Cancel" }),
|
|
2188
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2189
|
+
import_twreact_ui10.Button,
|
|
1651
2190
|
{
|
|
1652
2191
|
variant: "destructive",
|
|
1653
2192
|
disabled: reason.trim().length < 8,
|
|
@@ -1664,8 +2203,8 @@ function DatabaseTab({ company, run, setFlash, client, can }) {
|
|
|
1664
2203
|
)
|
|
1665
2204
|
] }),
|
|
1666
2205
|
children: [
|
|
1667
|
-
/* @__PURE__ */ (0,
|
|
1668
|
-
/* @__PURE__ */ (0,
|
|
2206
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "mb-3 text-sm text-muted", children: "Only credential custodians can do this. The reveal is recorded in the audit log with your reason before the URL is read." }),
|
|
2207
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Textarea, { label: "Reason", rows: 3, value: reason, onChange: (e) => setReason(e.target.value), helperText: "At least 8 characters, e.g. the incident or ticket number." })
|
|
1669
2208
|
]
|
|
1670
2209
|
}
|
|
1671
2210
|
)
|
|
@@ -1676,13 +2215,13 @@ function AdminsTab({ company, setSecret, setFlash, client, can }) {
|
|
|
1676
2215
|
const sensitive = useSensitive();
|
|
1677
2216
|
const ready = ((_a = company.database) == null ? void 0 : _a.configured) && ((_b = company.database) == null ? void 0 : _b.status) === "active";
|
|
1678
2217
|
const { data, error, reload } = useLoad(ready ? `companies/${company.id}/admins` : null);
|
|
1679
|
-
const [adding, setAdding] = (0,
|
|
1680
|
-
const [fullName, setFullName] = (0,
|
|
1681
|
-
const [email, setEmail] = (0,
|
|
1682
|
-
const [phone, setPhone] = (0,
|
|
1683
|
-
const [busy, setBusy] = (0,
|
|
2218
|
+
const [adding, setAdding] = (0, import_react11.useState)(false);
|
|
2219
|
+
const [fullName, setFullName] = (0, import_react11.useState)("");
|
|
2220
|
+
const [email, setEmail] = (0, import_react11.useState)("");
|
|
2221
|
+
const [phone, setPhone] = (0, import_react11.useState)("");
|
|
2222
|
+
const [busy, setBusy] = (0, import_react11.useState)(false);
|
|
1684
2223
|
const writable = can("companies.write");
|
|
1685
|
-
if (!ready) return /* @__PURE__ */ (0,
|
|
2224
|
+
if (!ready) return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Card, { title: "Company admins", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-sm text-muted", children: "Admins can be added once the company's database is provisioned." }) });
|
|
1686
2225
|
const act = async (a, action) => {
|
|
1687
2226
|
var _a2;
|
|
1688
2227
|
try {
|
|
@@ -1711,38 +2250,38 @@ function AdminsTab({ company, setSecret, setFlash, client, can }) {
|
|
|
1711
2250
|
setBusy(false);
|
|
1712
2251
|
}
|
|
1713
2252
|
};
|
|
1714
|
-
return /* @__PURE__ */ (0,
|
|
2253
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
|
|
1715
2254
|
Card,
|
|
1716
2255
|
{
|
|
1717
2256
|
title: "Company admins",
|
|
1718
|
-
actions: writable && /* @__PURE__ */ (0,
|
|
2257
|
+
actions: writable && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { size: "sm", leftIcon: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(IconPlus, {}), onClick: () => setAdding(true), children: "Add admin" }),
|
|
1719
2258
|
children: [
|
|
1720
|
-
error && !data ? /* @__PURE__ */ (0,
|
|
1721
|
-
/* @__PURE__ */ (0,
|
|
1722
|
-
/* @__PURE__ */ (0,
|
|
1723
|
-
/* @__PURE__ */ (0,
|
|
2259
|
+
error && !data ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.ErrorState, { description: error.message, onRetry: reload }) : !data ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.LoadingState, { variant: "table", rows: 2 }) : !((_c = data.admins) == null ? void 0 : _c.length) ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.EmptyState, { title: "No administrators yet", compact: true }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ul", { className: "divide-y divide-line", children: data.admins.map((a) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("li", { className: "flex flex-wrap items-center gap-3 py-2.5", children: [
|
|
2260
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
2261
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "truncate text-sm font-medium text-foreground", children: a.fullName }),
|
|
2262
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "truncate text-xs text-muted", children: a.email })
|
|
1724
2263
|
] }),
|
|
1725
|
-
/* @__PURE__ */ (0,
|
|
1726
|
-
writable && /* @__PURE__ */ (0,
|
|
1727
|
-
/* @__PURE__ */ (0,
|
|
1728
|
-
/* @__PURE__ */ (0,
|
|
2264
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(StatusPill, { status: a.status }),
|
|
2265
|
+
writable && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex gap-1", children: [
|
|
2266
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { size: "sm", variant: "ghost", onClick: () => act(a, "reset-password"), children: "Reset password" }),
|
|
2267
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { size: "sm", variant: "ghost", onClick: () => act(a, a.status === "active" ? "disable" : "enable"), children: a.status === "active" ? "Disable" : "Enable" })
|
|
1729
2268
|
] })
|
|
1730
2269
|
] }, a.id)) }),
|
|
1731
|
-
/* @__PURE__ */ (0,
|
|
1732
|
-
|
|
2270
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2271
|
+
import_twreact_ui10.Dialog,
|
|
1733
2272
|
{
|
|
1734
2273
|
isOpen: adding,
|
|
1735
2274
|
onClose: () => setAdding(false),
|
|
1736
2275
|
title: "Add company admin",
|
|
1737
2276
|
size: "sm",
|
|
1738
|
-
footer: /* @__PURE__ */ (0,
|
|
1739
|
-
/* @__PURE__ */ (0,
|
|
1740
|
-
/* @__PURE__ */ (0,
|
|
2277
|
+
footer: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex justify-end gap-2", children: [
|
|
2278
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { variant: "outline", onClick: () => setAdding(false), children: "Cancel" }),
|
|
2279
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Button, { loading: busy, disabled: !fullName.trim() || !EMAIL_RE.test(email.trim()), onClick: add, children: "Add admin" })
|
|
1741
2280
|
] }),
|
|
1742
|
-
children: /* @__PURE__ */ (0,
|
|
1743
|
-
/* @__PURE__ */ (0,
|
|
1744
|
-
/* @__PURE__ */ (0,
|
|
1745
|
-
/* @__PURE__ */ (0,
|
|
2281
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "space-y-4", children: [
|
|
2282
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Input, { label: "Full name", autoFocus: true, value: fullName, onChange: (e) => setFullName(e.target.value) }),
|
|
2283
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Input, { label: "Email", type: "email", value: email, onChange: (e) => setEmail(e.target.value) }),
|
|
2284
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Input, { label: "Phone (optional)", type: "tel", value: phone, onChange: (e) => setPhone(e.target.value) })
|
|
1746
2285
|
] })
|
|
1747
2286
|
}
|
|
1748
2287
|
)
|
|
@@ -1754,7 +2293,7 @@ function ActivityTab({ company, client, can }) {
|
|
|
1754
2293
|
var _a, _b;
|
|
1755
2294
|
const sensitive = useSensitive();
|
|
1756
2295
|
const { data, error, reload } = useLoad(`companies/${company.id}/activity`);
|
|
1757
|
-
const [retryError, setRetryError] = (0,
|
|
2296
|
+
const [retryError, setRetryError] = (0, import_react11.useState)(null);
|
|
1758
2297
|
const modeOf = (payload) => {
|
|
1759
2298
|
try {
|
|
1760
2299
|
const p = typeof payload === "string" ? JSON.parse(payload) : payload;
|
|
@@ -1763,27 +2302,27 @@ function ActivityTab({ company, client, can }) {
|
|
|
1763
2302
|
return "managed";
|
|
1764
2303
|
}
|
|
1765
2304
|
};
|
|
1766
|
-
if (error && !data) return /* @__PURE__ */ (0,
|
|
1767
|
-
if (!data) return /* @__PURE__ */ (0,
|
|
1768
|
-
return /* @__PURE__ */ (0,
|
|
1769
|
-
/* @__PURE__ */ (0,
|
|
1770
|
-
retryError && /* @__PURE__ */ (0,
|
|
1771
|
-
!((_a = data.jobs) == null ? void 0 : _a.length) ? /* @__PURE__ */ (0,
|
|
2305
|
+
if (error && !data) return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.ErrorState, { description: error.message, onRetry: reload });
|
|
2306
|
+
if (!data) return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.LoadingState, { variant: "table", rows: 4 });
|
|
2307
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "grid gap-5 [grid-template-columns:repeat(auto-fit,minmax(min(20rem,100%),1fr))]", children: [
|
|
2308
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(Card, { title: "Provisioning jobs", children: [
|
|
2309
|
+
retryError && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_twreact_ui10.Alert, { variant: "error", children: retryError }),
|
|
2310
|
+
!((_a = data.jobs) == null ? void 0 : _a.length) ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-sm text-muted", children: "No provisioning jobs." }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ul", { className: "divide-y divide-line", children: data.jobs.map((j) => {
|
|
1772
2311
|
var _a2, _b2;
|
|
1773
2312
|
const lastError = (_a2 = j.lastError) != null ? _a2 : j.last_error;
|
|
1774
|
-
return /* @__PURE__ */ (0,
|
|
1775
|
-
/* @__PURE__ */ (0,
|
|
1776
|
-
/* @__PURE__ */ (0,
|
|
1777
|
-
/* @__PURE__ */ (0,
|
|
2313
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("li", { className: "py-2.5", children: [
|
|
2314
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
2315
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(StatusPill, { status: j.status }),
|
|
2316
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "text-xs text-muted", children: [
|
|
1778
2317
|
modeOf(j.payload),
|
|
1779
2318
|
" \xB7 step ",
|
|
1780
|
-
/* @__PURE__ */ (0,
|
|
2319
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "font-mono", children: j.step }),
|
|
1781
2320
|
" \xB7 ",
|
|
1782
2321
|
j.attempts,
|
|
1783
2322
|
" attempt(s)"
|
|
1784
2323
|
] }),
|
|
1785
|
-
j.status === "failed" && can("companies.write") && /* @__PURE__ */ (0,
|
|
1786
|
-
|
|
2324
|
+
j.status === "failed" && can("companies.write") && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2325
|
+
import_twreact_ui10.Button,
|
|
1787
2326
|
{
|
|
1788
2327
|
size: "sm",
|
|
1789
2328
|
variant: "ghost",
|
|
@@ -1801,33 +2340,33 @@ function ActivityTab({ company, client, can }) {
|
|
|
1801
2340
|
}
|
|
1802
2341
|
)
|
|
1803
2342
|
] }),
|
|
1804
|
-
lastError && j.status !== "succeeded" && /* @__PURE__ */ (0,
|
|
1805
|
-
/* @__PURE__ */ (0,
|
|
2343
|
+
lastError && j.status !== "succeeded" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "mt-1 break-words font-mono text-xs text-error-700", children: lastError }),
|
|
2344
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "mt-0.5 text-xs text-muted", children: formatDate((_b2 = j.updatedAt) != null ? _b2 : j.updated_at) })
|
|
1806
2345
|
] }, j.id);
|
|
1807
2346
|
}) })
|
|
1808
2347
|
] }),
|
|
1809
|
-
/* @__PURE__ */ (0,
|
|
2348
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Card, { title: "Audit trail", children: !((_b = data.events) == null ? void 0 : _b.length) ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-sm text-muted", children: "No events." }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ol", { className: "divide-y divide-line", children: data.events.map(normalizeEvent).map((e, i) => {
|
|
1810
2349
|
var _a2;
|
|
1811
|
-
return /* @__PURE__ */ (0,
|
|
1812
|
-
/* @__PURE__ */ (0,
|
|
1813
|
-
/* @__PURE__ */ (0,
|
|
2350
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("li", { className: "py-2.5", children: [
|
|
2351
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "text-sm text-foreground", children: [
|
|
2352
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "font-medium", children: e.actor }),
|
|
1814
2353
|
" ",
|
|
1815
2354
|
describeAction(e.action),
|
|
1816
|
-
e.target && /* @__PURE__ */ (0,
|
|
2355
|
+
e.target && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "text-muted", children: [
|
|
1817
2356
|
" \xB7 ",
|
|
1818
2357
|
e.target
|
|
1819
2358
|
] })
|
|
1820
2359
|
] }),
|
|
1821
|
-
/* @__PURE__ */ (0,
|
|
2360
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "text-xs text-muted", children: formatDate(e.at) })
|
|
1822
2361
|
] }, (_a2 = e.id) != null ? _a2 : i);
|
|
1823
2362
|
}) }) })
|
|
1824
2363
|
] });
|
|
1825
2364
|
}
|
|
1826
2365
|
|
|
1827
2366
|
// src/platform/views/Admins.jsx
|
|
1828
|
-
var
|
|
1829
|
-
var
|
|
1830
|
-
var
|
|
2367
|
+
var import_react12 = __toESM(require("react"), 1);
|
|
2368
|
+
var import_twreact_ui11 = require("@dreamtree-org/twreact-ui");
|
|
2369
|
+
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
1831
2370
|
var ROLE_OPTIONS = [
|
|
1832
2371
|
{ value: "superadmin", label: "Superadmin: everything" },
|
|
1833
2372
|
{ value: "support", label: "Support: read-only" }
|
|
@@ -1837,13 +2376,13 @@ function AdminsView() {
|
|
|
1837
2376
|
const { client, can, loginUrl, Link } = usePlatform();
|
|
1838
2377
|
const sensitive = useSensitive();
|
|
1839
2378
|
const { data, error, loading, reload } = useLoad("admins");
|
|
1840
|
-
const [flash, setFlash] = (0,
|
|
1841
|
-
const [secret, setSecret] = (0,
|
|
1842
|
-
const [adding, setAdding] = (0,
|
|
1843
|
-
const [email, setEmail] = (0,
|
|
1844
|
-
const [role, setRole] = (0,
|
|
1845
|
-
const [busy, setBusy] = (0,
|
|
1846
|
-
const [confirm, setConfirm] = (0,
|
|
2379
|
+
const [flash, setFlash] = (0, import_react12.useState)(null);
|
|
2380
|
+
const [secret, setSecret] = (0, import_react12.useState)(null);
|
|
2381
|
+
const [adding, setAdding] = (0, import_react12.useState)(false);
|
|
2382
|
+
const [email, setEmail] = (0, import_react12.useState)("");
|
|
2383
|
+
const [role, setRole] = (0, import_react12.useState)("superadmin");
|
|
2384
|
+
const [busy, setBusy] = (0, import_react12.useState)(false);
|
|
2385
|
+
const [confirm, setConfirm] = (0, import_react12.useState)(null);
|
|
1847
2386
|
const writable = can("admins.write");
|
|
1848
2387
|
const act = async (a, action, extra = {}) => {
|
|
1849
2388
|
var _a2;
|
|
@@ -1874,63 +2413,63 @@ function AdminsView() {
|
|
|
1874
2413
|
setBusy(false);
|
|
1875
2414
|
}
|
|
1876
2415
|
};
|
|
1877
|
-
return /* @__PURE__ */ (0,
|
|
1878
|
-
/* @__PURE__ */ (0,
|
|
1879
|
-
|
|
2416
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "mx-auto max-w-5xl space-y-6 px-4 py-6 sm:px-6", children: [
|
|
2417
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2418
|
+
import_twreact_ui11.PageHeader,
|
|
1880
2419
|
{
|
|
1881
2420
|
title: "Platform admins",
|
|
1882
2421
|
subtitle: "People who can create companies and manage the platform",
|
|
1883
2422
|
linkAs: Link,
|
|
1884
|
-
actions: writable && /* @__PURE__ */ (0,
|
|
2423
|
+
actions: writable && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_twreact_ui11.Button, { leftIcon: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(IconPlus, {}), onClick: () => setAdding(true), children: "Add platform admin" })
|
|
1885
2424
|
}
|
|
1886
2425
|
),
|
|
1887
|
-
flash && /* @__PURE__ */ (0,
|
|
1888
|
-
secret && /* @__PURE__ */ (0,
|
|
1889
|
-
error && !data ? /* @__PURE__ */ (0,
|
|
2426
|
+
flash && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_twreact_ui11.Alert, { variant: flash.kind, dismissible: true, onDismiss: () => setFlash(null), children: flash.msg }),
|
|
2427
|
+
secret && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(OneTimeSecret, { title: secret.title, loginUrl, email: secret.email, password: secret.password, onDone: () => setSecret(null) }),
|
|
2428
|
+
error && !data ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_twreact_ui11.ErrorState, { description: error.message, onRetry: reload }) : loading && !data ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_twreact_ui11.LoadingState, { variant: "table", rows: 3 }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("ul", { className: "divide-y divide-line rounded-xl border border-line bg-surface", children: ((_a = data == null ? void 0 : data.admins) != null ? _a : []).map((a) => {
|
|
1890
2429
|
var _a2, _b, _c;
|
|
1891
2430
|
const me = a.id === data.me;
|
|
1892
2431
|
const roles = Array.isArray(a.roles) ? a.roles : [];
|
|
1893
|
-
return /* @__PURE__ */ (0,
|
|
1894
|
-
/* @__PURE__ */ (0,
|
|
1895
|
-
/* @__PURE__ */ (0,
|
|
1896
|
-
/* @__PURE__ */ (0,
|
|
1897
|
-
me && /* @__PURE__ */ (0,
|
|
2432
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("li", { className: "flex flex-wrap items-center gap-3 px-4 py-3", children: [
|
|
2433
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
2434
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex flex-wrap items-center gap-2 text-sm font-medium text-foreground", children: [
|
|
2435
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "truncate", children: a.email }),
|
|
2436
|
+
me && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_twreact_ui11.Badge, { variant: "primary", size: "sm", children: "you" })
|
|
1898
2437
|
] }),
|
|
1899
|
-
/* @__PURE__ */ (0,
|
|
2438
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "text-xs text-muted", children: [
|
|
1900
2439
|
roles.length ? roles.join(", ") : "No role",
|
|
1901
2440
|
" \xB7 last sign-in ",
|
|
1902
2441
|
timeAgo((_a2 = a.lastLoginAt) != null ? _a2 : a.last_login_at),
|
|
1903
2442
|
!((_b = a.totpEnrolled) != null ? _b : Number(a.strong_factor_enrolled)) && " \xB7 no authenticator yet"
|
|
1904
2443
|
] })
|
|
1905
2444
|
] }),
|
|
1906
|
-
/* @__PURE__ */ (0,
|
|
1907
|
-
writable && /* @__PURE__ */ (0,
|
|
1908
|
-
/* @__PURE__ */ (0,
|
|
1909
|
-
!me && ((_c = a.totpEnrolled) != null ? _c : Number(a.strong_factor_enrolled)) ? /* @__PURE__ */ (0,
|
|
1910
|
-
!me && (a.status === "active" ? /* @__PURE__ */ (0,
|
|
2445
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(StatusPill, { status: a.status }),
|
|
2446
|
+
writable && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
|
|
2447
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_twreact_ui11.Button, { size: "sm", variant: "ghost", onClick: () => act(a, "reset-password"), children: "Reset password" }),
|
|
2448
|
+
!me && ((_c = a.totpEnrolled) != null ? _c : Number(a.strong_factor_enrolled)) ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_twreact_ui11.Button, { size: "sm", variant: "ghost", onClick: () => setConfirm({ admin: a, action: "reset-totp" }), children: "Reset authenticator" }) : null,
|
|
2449
|
+
!me && (a.status === "active" ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_twreact_ui11.Button, { size: "sm", variant: "ghost", onClick: () => setConfirm({ admin: a, action: "disable" }), children: "Disable" }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_twreact_ui11.Button, { size: "sm", variant: "ghost", onClick: () => act(a, "enable"), children: "Enable" }))
|
|
1911
2450
|
] })
|
|
1912
2451
|
] }, a.id);
|
|
1913
2452
|
}) }),
|
|
1914
|
-
/* @__PURE__ */ (0,
|
|
1915
|
-
|
|
2453
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2454
|
+
import_twreact_ui11.Dialog,
|
|
1916
2455
|
{
|
|
1917
2456
|
isOpen: adding,
|
|
1918
2457
|
onClose: () => setAdding(false),
|
|
1919
2458
|
title: "Add platform admin",
|
|
1920
2459
|
size: "sm",
|
|
1921
|
-
footer: /* @__PURE__ */ (0,
|
|
1922
|
-
/* @__PURE__ */ (0,
|
|
1923
|
-
/* @__PURE__ */ (0,
|
|
2460
|
+
footer: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex justify-end gap-2", children: [
|
|
2461
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_twreact_ui11.Button, { variant: "outline", onClick: () => setAdding(false), children: "Cancel" }),
|
|
2462
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_twreact_ui11.Button, { loading: busy, disabled: !EMAIL_RE.test(email.trim()), onClick: add, children: "Add admin" })
|
|
1924
2463
|
] }),
|
|
1925
|
-
children: /* @__PURE__ */ (0,
|
|
1926
|
-
/* @__PURE__ */ (0,
|
|
1927
|
-
/* @__PURE__ */ (0,
|
|
1928
|
-
/* @__PURE__ */ (0,
|
|
2464
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "space-y-4", children: [
|
|
2465
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "text-sm text-muted", children: "A temporary password is generated and shown once. They set up an authenticator app at first sign-in." }),
|
|
2466
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_twreact_ui11.Input, { label: "Email", type: "email", autoFocus: true, value: email, onChange: (e) => setEmail(e.target.value) }),
|
|
2467
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_twreact_ui11.Select, { label: "Role", options: ROLE_OPTIONS, value: role, onChange: (v) => setRole(String(v)) })
|
|
1929
2468
|
] })
|
|
1930
2469
|
}
|
|
1931
2470
|
),
|
|
1932
|
-
/* @__PURE__ */ (0,
|
|
1933
|
-
|
|
2471
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2472
|
+
import_twreact_ui11.ConfirmDialog,
|
|
1934
2473
|
{
|
|
1935
2474
|
isOpen: confirm !== null,
|
|
1936
2475
|
title: (confirm == null ? void 0 : confirm.action) === "disable" ? `Disable ${confirm == null ? void 0 : confirm.admin.email}?` : `Reset ${confirm == null ? void 0 : confirm.admin.email}'s authenticator?`,
|
|
@@ -1949,17 +2488,17 @@ function AdminsView() {
|
|
|
1949
2488
|
}
|
|
1950
2489
|
|
|
1951
2490
|
// src/platform/views/Audit.jsx
|
|
1952
|
-
var
|
|
1953
|
-
var
|
|
1954
|
-
var
|
|
2491
|
+
var import_react13 = __toESM(require("react"), 1);
|
|
2492
|
+
var import_twreact_ui12 = require("@dreamtree-org/twreact-ui");
|
|
2493
|
+
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
1955
2494
|
function AuditView() {
|
|
1956
2495
|
const { client, Link, href } = usePlatform();
|
|
1957
|
-
const [q, setQ] = (0,
|
|
1958
|
-
const [applied, setApplied] = (0,
|
|
1959
|
-
const [events, setEvents] = (0,
|
|
1960
|
-
const [cursor, setCursor] = (0,
|
|
1961
|
-
const [state, setState] = (0,
|
|
1962
|
-
const [exportError, setExportError] = (0,
|
|
2496
|
+
const [q, setQ] = (0, import_react13.useState)("");
|
|
2497
|
+
const [applied, setApplied] = (0, import_react13.useState)("");
|
|
2498
|
+
const [events, setEvents] = (0, import_react13.useState)([]);
|
|
2499
|
+
const [cursor, setCursor] = (0, import_react13.useState)(null);
|
|
2500
|
+
const [state, setState] = (0, import_react13.useState)({ loading: true, error: null });
|
|
2501
|
+
const [exportError, setExportError] = (0, import_react13.useState)(null);
|
|
1963
2502
|
const load = async (more = false) => {
|
|
1964
2503
|
setState({ loading: true, error: null });
|
|
1965
2504
|
try {
|
|
@@ -1971,7 +2510,7 @@ function AuditView() {
|
|
|
1971
2510
|
setState({ loading: false, error: e });
|
|
1972
2511
|
}
|
|
1973
2512
|
};
|
|
1974
|
-
(0,
|
|
2513
|
+
(0, import_react13.useEffect)(() => {
|
|
1975
2514
|
void load(false);
|
|
1976
2515
|
}, [applied]);
|
|
1977
2516
|
const exportCsv = async () => {
|
|
@@ -1990,18 +2529,18 @@ function AuditView() {
|
|
|
1990
2529
|
}
|
|
1991
2530
|
};
|
|
1992
2531
|
const rows = events.map(normalizeEvent);
|
|
1993
|
-
return /* @__PURE__ */ (0,
|
|
1994
|
-
/* @__PURE__ */ (0,
|
|
1995
|
-
|
|
2532
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "mx-auto max-w-6xl space-y-6 px-4 py-6 sm:px-6", children: [
|
|
2533
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2534
|
+
import_twreact_ui12.PageHeader,
|
|
1996
2535
|
{
|
|
1997
2536
|
title: "Audit log",
|
|
1998
2537
|
subtitle: "Every platform action: who did what, to which company",
|
|
1999
2538
|
linkAs: Link,
|
|
2000
|
-
actions: /* @__PURE__ */ (0,
|
|
2539
|
+
actions: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_twreact_ui12.Button, { variant: "outline", leftIcon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(IconDownload, {}), onClick: exportCsv, children: "Export CSV" })
|
|
2001
2540
|
}
|
|
2002
2541
|
),
|
|
2003
|
-
exportError && /* @__PURE__ */ (0,
|
|
2004
|
-
/* @__PURE__ */ (0,
|
|
2542
|
+
exportError && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_twreact_ui12.Alert, { variant: "error", children: exportError }),
|
|
2543
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
2005
2544
|
"form",
|
|
2006
2545
|
{
|
|
2007
2546
|
className: "flex gap-2",
|
|
@@ -2010,50 +2549,50 @@ function AuditView() {
|
|
|
2010
2549
|
setApplied(q.trim());
|
|
2011
2550
|
},
|
|
2012
2551
|
children: [
|
|
2013
|
-
/* @__PURE__ */ (0,
|
|
2014
|
-
/* @__PURE__ */ (0,
|
|
2552
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "flex-1", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_twreact_ui12.Input, { type: "search", "aria-label": "Search the audit log", placeholder: "Search action, target or company", value: q, onChange: (e) => setQ(e.target.value) }) }),
|
|
2553
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_twreact_ui12.Button, { type: "submit", variant: "outline", children: "Search" })
|
|
2015
2554
|
]
|
|
2016
2555
|
}
|
|
2017
2556
|
),
|
|
2018
|
-
state.error && !rows.length ? /* @__PURE__ */ (0,
|
|
2019
|
-
/* @__PURE__ */ (0,
|
|
2557
|
+
state.error && !rows.length ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_twreact_ui12.ErrorState, { description: state.error.message, onRetry: () => load(false) }) : state.loading && !rows.length ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_twreact_ui12.LoadingState, { variant: "table", rows: 8 }) : !rows.length ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_twreact_ui12.EmptyState, { icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(IconList, { size: 24 }), title: applied ? "Nothing matches" : "No events yet", compact: true }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
|
|
2558
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("ol", { className: "divide-y divide-line rounded-xl border border-line bg-surface", children: rows.map((e, i) => {
|
|
2020
2559
|
var _a;
|
|
2021
|
-
return /* @__PURE__ */ (0,
|
|
2022
|
-
/* @__PURE__ */ (0,
|
|
2023
|
-
/* @__PURE__ */ (0,
|
|
2024
|
-
/* @__PURE__ */ (0,
|
|
2560
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("li", { className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 px-4 py-2.5", children: [
|
|
2561
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "w-40 shrink-0 text-xs text-muted", children: formatDate(e.at) }),
|
|
2562
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: "min-w-0 flex-1 text-sm text-foreground", children: [
|
|
2563
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "font-medium", children: e.actor }),
|
|
2025
2564
|
" ",
|
|
2026
2565
|
describeAction(e.action),
|
|
2027
|
-
e.company && e.companyId && /* @__PURE__ */ (0,
|
|
2566
|
+
e.company && e.companyId && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
|
|
2028
2567
|
" \xB7 ",
|
|
2029
|
-
/* @__PURE__ */ (0,
|
|
2568
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Link, { href: href(`companies/${e.companyId}`), className: "text-primary-600 hover:underline", children: e.company })
|
|
2030
2569
|
] }),
|
|
2031
|
-
e.target && /* @__PURE__ */ (0,
|
|
2570
|
+
e.target && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: "text-muted", children: [
|
|
2032
2571
|
" \xB7 ",
|
|
2033
2572
|
e.target
|
|
2034
2573
|
] })
|
|
2035
2574
|
] }),
|
|
2036
|
-
/* @__PURE__ */ (0,
|
|
2575
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("code", { className: "text-xs text-muted", children: e.action })
|
|
2037
2576
|
] }, (_a = e.id) != null ? _a : i);
|
|
2038
2577
|
}) }),
|
|
2039
|
-
cursor && /* @__PURE__ */ (0,
|
|
2578
|
+
cursor && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-center", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_twreact_ui12.Button, { variant: "outline", loading: state.loading, onClick: () => load(true), children: "Load older" }) })
|
|
2040
2579
|
] })
|
|
2041
2580
|
] });
|
|
2042
2581
|
}
|
|
2043
2582
|
|
|
2044
2583
|
// src/platform/views/Security.jsx
|
|
2045
|
-
var
|
|
2046
|
-
var
|
|
2047
|
-
var
|
|
2584
|
+
var import_react14 = __toESM(require("react"), 1);
|
|
2585
|
+
var import_twreact_ui13 = require("@dreamtree-org/twreact-ui");
|
|
2586
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
2048
2587
|
var grouped = (s) => String(s != null ? s : "").replace(/(.{4})/g, "$1 ").trim();
|
|
2049
2588
|
function SecurityView({ gate = false }) {
|
|
2050
2589
|
var _a, _b;
|
|
2051
2590
|
const { client, me, reloadMe, Link } = usePlatform();
|
|
2052
2591
|
const sensitive = useSensitive();
|
|
2053
|
-
const [setup, setSetup] = (0,
|
|
2054
|
-
const [code, setCode] = (0,
|
|
2055
|
-
const [busy, setBusy] = (0,
|
|
2056
|
-
const [error, setError] = (0,
|
|
2592
|
+
const [setup, setSetup] = (0, import_react14.useState)(null);
|
|
2593
|
+
const [code, setCode] = (0, import_react14.useState)("");
|
|
2594
|
+
const [busy, setBusy] = (0, import_react14.useState)(false);
|
|
2595
|
+
const [error, setError] = (0, import_react14.useState)(null);
|
|
2057
2596
|
const enrolled = (_a = me == null ? void 0 : me.admin) == null ? void 0 : _a.totpEnrolled;
|
|
2058
2597
|
const begin = async () => {
|
|
2059
2598
|
setBusy(true);
|
|
@@ -2081,36 +2620,187 @@ function SecurityView({ gate = false }) {
|
|
|
2081
2620
|
setBusy(false);
|
|
2082
2621
|
}
|
|
2083
2622
|
};
|
|
2084
|
-
return /* @__PURE__ */ (0,
|
|
2085
|
-
/* @__PURE__ */ (0,
|
|
2086
|
-
gate && /* @__PURE__ */ (0,
|
|
2087
|
-
error && /* @__PURE__ */ (0,
|
|
2088
|
-
/* @__PURE__ */ (0,
|
|
2089
|
-
/* @__PURE__ */ (0,
|
|
2623
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "mx-auto max-w-2xl space-y-6 px-4 py-6 sm:px-6", children: [
|
|
2624
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_twreact_ui13.PageHeader, { title: gate ? "Set up your authenticator" : "Security", subtitle: (_b = me == null ? void 0 : me.admin) == null ? void 0 : _b.email, linkAs: Link }),
|
|
2625
|
+
gate && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_twreact_ui13.Alert, { variant: "info", title: "One more step", children: "The platform console needs a second factor. Set up an authenticator app (Google Authenticator, 1Password, Authy, \u2026) to continue." }),
|
|
2626
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_twreact_ui13.Alert, { variant: "error", children: error }),
|
|
2627
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Card, { title: "Authenticator app", children: enrolled ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("p", { className: "flex items-center gap-2 text-sm text-success-700", children: [
|
|
2628
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(IconCheck, {}),
|
|
2090
2629
|
" An authenticator is set up. Sign-in and sensitive actions ask for its code. If you lose the device, ask another platform admin to reset it."
|
|
2091
|
-
] }) : !setup ? /* @__PURE__ */ (0,
|
|
2092
|
-
/* @__PURE__ */ (0,
|
|
2093
|
-
/* @__PURE__ */ (0,
|
|
2094
|
-
] }) : /* @__PURE__ */ (0,
|
|
2095
|
-
/* @__PURE__ */ (0,
|
|
2096
|
-
/* @__PURE__ */ (0,
|
|
2630
|
+
] }) : !setup ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "space-y-3", children: [
|
|
2631
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-sm text-muted", children: "You'll scan or type a setup key into the app, then enter the 6-digit code it shows." }),
|
|
2632
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_twreact_ui13.Button, { leftIcon: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(IconShield, {}), loading: busy, onClick: begin, children: "Start setup" })
|
|
2633
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("form", { className: "space-y-4", onSubmit: verify, children: [
|
|
2634
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("ol", { className: "list-decimal space-y-3 pl-5 text-sm text-foreground", children: [
|
|
2635
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("li", { children: [
|
|
2097
2636
|
"Add an account in your authenticator app with this setup key:",
|
|
2098
|
-
/* @__PURE__ */ (0,
|
|
2099
|
-
/* @__PURE__ */ (0,
|
|
2100
|
-
/* @__PURE__ */ (0,
|
|
2101
|
-
/* @__PURE__ */ (0,
|
|
2637
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "mt-2 flex flex-wrap items-center gap-2", children: [
|
|
2638
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("code", { className: "rounded-md bg-background px-2 py-1 font-mono text-sm tracking-wider", children: grouped(setup.secret) }),
|
|
2639
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CopyButton, { value: setup.secret, label: "Copy key" }),
|
|
2640
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("a", { href: setup.otpauthUrl, className: "text-xs text-primary-600 underline", children: "Open in authenticator app" })
|
|
2102
2641
|
] })
|
|
2103
2642
|
] }),
|
|
2104
|
-
/* @__PURE__ */ (0,
|
|
2643
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("li", { children: "Enter the 6-digit code it shows." })
|
|
2105
2644
|
] }),
|
|
2106
|
-
/* @__PURE__ */ (0,
|
|
2107
|
-
/* @__PURE__ */ (0,
|
|
2645
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "max-w-xs", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_twreact_ui13.Input, { label: "Code", otp: true, autoFocus: true, value: code, onChange: (e) => setCode(e.target.value.replace(/\s/g, "")) }) }),
|
|
2646
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_twreact_ui13.Button, { type: "submit", loading: busy, disabled: code.length < 6, children: "Verify and finish" })
|
|
2108
2647
|
] }) })
|
|
2109
2648
|
] });
|
|
2110
2649
|
}
|
|
2111
2650
|
|
|
2651
|
+
// src/platform/views/Plans.jsx
|
|
2652
|
+
var import_react15 = __toESM(require("react"), 1);
|
|
2653
|
+
var import_twreact_ui14 = require("@dreamtree-org/twreact-ui");
|
|
2654
|
+
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
2655
|
+
var INTERVALS = [
|
|
2656
|
+
{ value: "month", label: "Monthly" },
|
|
2657
|
+
{ value: "year", label: "Yearly" },
|
|
2658
|
+
{ value: "one-time", label: "One-time" }
|
|
2659
|
+
];
|
|
2660
|
+
var blank = { code: "", name: "", price: "0", currency: "INR", interval: "month", seats: "", limits: "", features: "" };
|
|
2661
|
+
function toForm(plan) {
|
|
2662
|
+
var _a, _b, _c, _d, _e;
|
|
2663
|
+
return {
|
|
2664
|
+
code: plan.code,
|
|
2665
|
+
name: plan.name,
|
|
2666
|
+
price: String((Number((_a = plan.priceMinor) != null ? _a : 0) / 100).toFixed(2)).replace(/\.00$/, ""),
|
|
2667
|
+
currency: (_b = plan.currency) != null ? _b : "INR",
|
|
2668
|
+
interval: (_c = plan.interval) != null ? _c : "month",
|
|
2669
|
+
seats: plan.seats == null ? "" : String(plan.seats),
|
|
2670
|
+
limits: Object.entries((_d = plan.limits) != null ? _d : {}).map(([k, v]) => `${k} = ${v != null ? v : ""}`).join("\n"),
|
|
2671
|
+
features: ((_e = plan.features) != null ? _e : []).join(", ")
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
function planBody(form) {
|
|
2675
|
+
const limits = {};
|
|
2676
|
+
for (const line of form.limits.split("\n")) {
|
|
2677
|
+
const [k, ...rest] = line.split("=");
|
|
2678
|
+
if (!k.trim()) continue;
|
|
2679
|
+
const v = rest.join("=").trim();
|
|
2680
|
+
limits[k.trim()] = v === "" ? null : Number(v);
|
|
2681
|
+
}
|
|
2682
|
+
return {
|
|
2683
|
+
code: form.code.trim().toLowerCase(),
|
|
2684
|
+
name: form.name.trim(),
|
|
2685
|
+
priceMinor: Math.round(Number(form.price || 0) * 100),
|
|
2686
|
+
currency: form.currency.trim().toUpperCase(),
|
|
2687
|
+
interval: form.interval,
|
|
2688
|
+
seats: form.seats.trim() === "" ? null : Number(form.seats),
|
|
2689
|
+
limits,
|
|
2690
|
+
features: form.features.split(",").map((f) => f.trim()).filter(Boolean)
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
function PlansView() {
|
|
2694
|
+
var _a;
|
|
2695
|
+
const { client, can, Link } = usePlatform();
|
|
2696
|
+
const sensitive = useSensitive();
|
|
2697
|
+
const { data, error, loading, reload } = useLoad("plans");
|
|
2698
|
+
const [flash, setFlash] = (0, import_react15.useState)(null);
|
|
2699
|
+
const [editing, setEditing] = (0, import_react15.useState)(null);
|
|
2700
|
+
const [busy, setBusy] = (0, import_react15.useState)(false);
|
|
2701
|
+
const writable = can("plans.write");
|
|
2702
|
+
const plans = (_a = data == null ? void 0 : data.plans) != null ? _a : [];
|
|
2703
|
+
const save = async () => {
|
|
2704
|
+
setBusy(true);
|
|
2705
|
+
setFlash(null);
|
|
2706
|
+
try {
|
|
2707
|
+
const body = planBody(editing.form);
|
|
2708
|
+
if (editing.plan) {
|
|
2709
|
+
const { code: _code, ...patch } = body;
|
|
2710
|
+
await sensitive(() => client.patch(`plans/${encodeURIComponent(editing.plan.id)}`, patch));
|
|
2711
|
+
} else {
|
|
2712
|
+
await sensitive(() => client.post("plans", body));
|
|
2713
|
+
}
|
|
2714
|
+
setFlash({ kind: "success", msg: `${body.name} saved` });
|
|
2715
|
+
setEditing(null);
|
|
2716
|
+
await reload();
|
|
2717
|
+
} catch (e) {
|
|
2718
|
+
const msg = errorText(e);
|
|
2719
|
+
if (msg) setFlash({ kind: "error", msg });
|
|
2720
|
+
} finally {
|
|
2721
|
+
setBusy(false);
|
|
2722
|
+
}
|
|
2723
|
+
};
|
|
2724
|
+
const setStatus = async (plan, status) => {
|
|
2725
|
+
setFlash(null);
|
|
2726
|
+
try {
|
|
2727
|
+
await sensitive(() => client.patch(`plans/${encodeURIComponent(plan.id)}`, { status }));
|
|
2728
|
+
setFlash({ kind: "success", msg: `${plan.name} ${status === "archived" ? "archived" : "restored"}` });
|
|
2729
|
+
await reload();
|
|
2730
|
+
} catch (e) {
|
|
2731
|
+
const msg = errorText(e);
|
|
2732
|
+
if (msg) setFlash({ kind: "error", msg });
|
|
2733
|
+
}
|
|
2734
|
+
};
|
|
2735
|
+
const field = (key) => {
|
|
2736
|
+
var _a2;
|
|
2737
|
+
return {
|
|
2738
|
+
value: (_a2 = editing == null ? void 0 : editing.form[key]) != null ? _a2 : "",
|
|
2739
|
+
onChange: (e) => setEditing((s) => ({ ...s, form: { ...s.form, [key]: e.target.value } }))
|
|
2740
|
+
};
|
|
2741
|
+
};
|
|
2742
|
+
const valid = editing && /^[a-z0-9][a-z0-9_-]{0,47}$/.test(editing.form.code.trim().toLowerCase()) && editing.form.name.trim() && Number(editing.form.price) >= 0;
|
|
2743
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "mx-auto max-w-5xl space-y-6 px-4 py-6 sm:px-6", children: [
|
|
2744
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
2745
|
+
import_twreact_ui14.PageHeader,
|
|
2746
|
+
{
|
|
2747
|
+
title: "Plans",
|
|
2748
|
+
subtitle: "What companies can subscribe to. Archived plans stay on the companies already using them.",
|
|
2749
|
+
linkAs: Link,
|
|
2750
|
+
actions: writable && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.Button, { leftIcon: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(IconPlus, {}), onClick: () => setEditing({ form: { ...blank } }), children: "New plan" })
|
|
2751
|
+
}
|
|
2752
|
+
),
|
|
2753
|
+
flash && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.Alert, { variant: flash.kind, dismissible: true, onDismiss: () => setFlash(null), children: flash.msg }),
|
|
2754
|
+
error && !data ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.ErrorState, { description: error.message, onRetry: reload }) : loading && !data ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.LoadingState, { variant: "table", rows: 3 }) : plans.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.EmptyState, { icon: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(IconTag, { size: 24 }), title: "No plans yet", description: writable ? "Create the first plan companies can subscribe to." : "A superadmin creates plans." }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("ul", { className: "divide-y divide-line rounded-xl border border-line bg-surface", children: plans.map((p) => {
|
|
2755
|
+
var _a2, _b;
|
|
2756
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("li", { className: "flex flex-wrap items-center gap-3 px-4 py-3", children: [
|
|
2757
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
2758
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex flex-wrap items-center gap-2 text-sm font-medium text-foreground", children: [
|
|
2759
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: p.name }),
|
|
2760
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "font-mono text-xs text-muted", children: p.code })
|
|
2761
|
+
] }),
|
|
2762
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "text-xs text-muted", children: [
|
|
2763
|
+
formatPrice(p.priceMinor, p.currency, p.interval),
|
|
2764
|
+
p.seats ? ` \xB7 ${p.seats} seats` : "",
|
|
2765
|
+
Object.keys((_a2 = p.limits) != null ? _a2 : {}).length ? ` \xB7 ${Object.entries(p.limits).map(([k, v]) => `${k} ${v != null ? v : "\u221E"}`).join(", ")}` : ""
|
|
2766
|
+
] }),
|
|
2767
|
+
((_b = p.features) == null ? void 0 : _b.length) ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "mt-1 text-xs text-muted", children: p.features.join(" \xB7 ") }) : null
|
|
2768
|
+
] }),
|
|
2769
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(StatusPill, { status: p.status }),
|
|
2770
|
+
writable && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex gap-1", children: [
|
|
2771
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.Button, { size: "sm", variant: "ghost", onClick: () => setEditing({ plan: p, form: toForm(p) }), children: "Edit" }),
|
|
2772
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.Button, { size: "sm", variant: "ghost", onClick: () => setStatus(p, p.status === "archived" ? "active" : "archived"), children: p.status === "archived" ? "Restore" : "Archive" })
|
|
2773
|
+
] })
|
|
2774
|
+
] }, p.id);
|
|
2775
|
+
}) }),
|
|
2776
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
2777
|
+
import_twreact_ui14.Dialog,
|
|
2778
|
+
{
|
|
2779
|
+
isOpen: editing !== null,
|
|
2780
|
+
onClose: () => setEditing(null),
|
|
2781
|
+
title: (editing == null ? void 0 : editing.plan) ? `Edit ${editing.plan.name}` : "New plan",
|
|
2782
|
+
size: "md",
|
|
2783
|
+
footer: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex justify-end gap-2", children: [
|
|
2784
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.Button, { variant: "outline", onClick: () => setEditing(null), children: "Cancel" }),
|
|
2785
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.Button, { loading: busy, disabled: !valid, onClick: save, children: "Save plan" })
|
|
2786
|
+
] }),
|
|
2787
|
+
children: editing && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "grid gap-4 sm:grid-cols-2", children: [
|
|
2788
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.Input, { label: "Name", autoFocus: true, ...field("name") }),
|
|
2789
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.Input, { label: "Code", disabled: Boolean(editing.plan), helperText: editing.plan ? "Fixed once created" : "Lowercase, e.g. pro or team-yearly", ...field("code") }),
|
|
2790
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.Input, { label: "Price", type: "number", min: "0", step: "0.01", ...field("price") }),
|
|
2791
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.Input, { label: "Currency", maxLength: 3, ...field("currency") }),
|
|
2792
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.Select, { label: "Billing", options: INTERVALS, value: editing.form.interval, onChange: (v) => setEditing((s) => ({ ...s, form: { ...s.form, interval: String(v) } })) }),
|
|
2793
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.Input, { label: "Seats", type: "number", min: "1", helperText: "Leave empty for no seat limit", ...field("seats") }),
|
|
2794
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "sm:col-span-2", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.Textarea, { label: "Limits", rows: 3, placeholder: "users = 10\nbranches =", helperText: "One per line: name = number. Leave the number empty for unlimited.", ...field("limits") }) }),
|
|
2795
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "sm:col-span-2", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_twreact_ui14.Input, { label: "Features", placeholder: "gst, reports, api", helperText: "Comma-separated", ...field("features") }) })
|
|
2796
|
+
] })
|
|
2797
|
+
}
|
|
2798
|
+
)
|
|
2799
|
+
] });
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2112
2802
|
// src/platform/PlatformConsole.jsx
|
|
2113
|
-
var
|
|
2803
|
+
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
2114
2804
|
var trim = (s) => String(s != null ? s : "").replace(/^\/+|\/+$/g, "");
|
|
2115
2805
|
function matchConsoleRoute(rel, pages = []) {
|
|
2116
2806
|
const parts = trim(rel).split("/").filter(Boolean);
|
|
@@ -2122,7 +2812,8 @@ function matchConsoleRoute(rel, pages = []) {
|
|
|
2122
2812
|
if (parts[1] === "new" && parts.length === 2) return { view: "new-company" };
|
|
2123
2813
|
if (parts.length === 2) return { view: "company", id: decodeURIComponent(parts[1]) };
|
|
2124
2814
|
}
|
|
2125
|
-
if (parts.length === 1 && ["admins", "audit", "security"].includes(parts[0])) return { view: parts[0] };
|
|
2815
|
+
if (parts.length === 1 && ["admins", "audit", "security", "plans"].includes(parts[0])) return { view: parts[0] };
|
|
2816
|
+
if (parts.length === 1 && parts[0] === "support-access") return { view: "support" };
|
|
2126
2817
|
return { view: "not-found" };
|
|
2127
2818
|
}
|
|
2128
2819
|
function PlatformConsole({
|
|
@@ -2148,25 +2839,25 @@ function PlatformConsole({
|
|
|
2148
2839
|
const browser = useBrowserLocation(!routed);
|
|
2149
2840
|
const pathname = routed ? givenPath : browser.pathname;
|
|
2150
2841
|
const navigate = givenNavigate != null ? givenNavigate : browser.navigate;
|
|
2151
|
-
const
|
|
2152
|
-
const client = (0,
|
|
2153
|
-
() => given != null ? given : createPlatformClient({ basePath: apiBasePath, loginPath: loginPath != null ? loginPath : `${
|
|
2154
|
-
[given, apiBasePath, loginPath,
|
|
2842
|
+
const base3 = `/${trim(basePath)}`.replace(/\/$/, "") || "";
|
|
2843
|
+
const client = (0, import_react16.useMemo)(
|
|
2844
|
+
() => given != null ? given : createPlatformClient({ basePath: apiBasePath, loginPath: loginPath != null ? loginPath : `${base3}/login` }),
|
|
2845
|
+
[given, apiBasePath, loginPath, base3]
|
|
2155
2846
|
);
|
|
2156
|
-
const Link = (0,
|
|
2157
|
-
const [me, setMe] = (0,
|
|
2158
|
-
const reloadMe = (0,
|
|
2847
|
+
const Link = (0, import_react16.useMemo)(() => makeLink(linkAs, navigate), [linkAs, navigate]);
|
|
2848
|
+
const [me, setMe] = (0, import_react16.useState)(initialMe);
|
|
2849
|
+
const reloadMe = (0, import_react16.useCallback)(async () => {
|
|
2159
2850
|
try {
|
|
2160
2851
|
setMe(await client.get("me"));
|
|
2161
2852
|
} catch (e) {
|
|
2162
2853
|
if (e.status !== 401) setMe(null);
|
|
2163
2854
|
}
|
|
2164
2855
|
}, [client]);
|
|
2165
|
-
(0,
|
|
2856
|
+
(0, import_react16.useEffect)(() => {
|
|
2166
2857
|
void reloadMe();
|
|
2167
2858
|
}, [reloadMe]);
|
|
2168
|
-
const href = (0,
|
|
2169
|
-
const companyLoginPath = (0,
|
|
2859
|
+
const href = (0, import_react16.useCallback)((rel2 = "") => `${base3}/${trim(rel2)}`.replace(/\/$/, "") || "/", [base3]);
|
|
2860
|
+
const companyLoginPath = (0, import_react16.useCallback)(
|
|
2170
2861
|
(slug) => {
|
|
2171
2862
|
var _a2, _b2;
|
|
2172
2863
|
const pattern = (_b2 = loginPathOpt != null ? loginPathOpt : (_a2 = me == null ? void 0 : me.app) == null ? void 0 : _a2.companyLoginPath) != null ? _b2 : "/c/:slug/login";
|
|
@@ -2175,7 +2866,7 @@ function PlatformConsole({
|
|
|
2175
2866
|
[loginPathOpt, me]
|
|
2176
2867
|
);
|
|
2177
2868
|
const origin = typeof window === "undefined" ? "" : window.location.origin;
|
|
2178
|
-
const can = (0,
|
|
2869
|
+
const can = (0, import_react16.useCallback)((perm) => (me == null ? void 0 : me.permissions) ? me.permissions.includes(perm) : true, [me]);
|
|
2179
2870
|
const enabled = enabledFeatures(features, me);
|
|
2180
2871
|
const visiblePages = pages.filter((p) => (!p.feature || enabled.includes(p.feature)) && (!p.permission || can(p.permission)));
|
|
2181
2872
|
const signOut = async () => {
|
|
@@ -2183,20 +2874,22 @@ function PlatformConsole({
|
|
|
2183
2874
|
await client.post("auth/logout", {}, { skipAuthRedirect: true });
|
|
2184
2875
|
} finally {
|
|
2185
2876
|
if (onSignOut) onSignOut();
|
|
2186
|
-
else if (typeof window !== "undefined") window.location.assign(loginPath != null ? loginPath : `${
|
|
2877
|
+
else if (typeof window !== "undefined") window.location.assign(loginPath != null ? loginPath : `${base3}/login`);
|
|
2187
2878
|
}
|
|
2188
2879
|
};
|
|
2189
|
-
const rel = pathname.startsWith(
|
|
2880
|
+
const rel = pathname.startsWith(base3) ? pathname.slice(base3.length) : pathname;
|
|
2190
2881
|
const route = matchConsoleRoute(rel, visiblePages);
|
|
2191
2882
|
const gate = me && me.access && me.access.console === false;
|
|
2192
2883
|
const active = (prefix) => prefix === "" ? route.view === "overview" : trim(rel) === prefix || trim(rel).startsWith(`${prefix}/`);
|
|
2193
|
-
const nav = gate ? [{ id: "security", label: "Set up authenticator", href: href("security"), icon: /* @__PURE__ */ (0,
|
|
2194
|
-
enabled.includes("overview") && { id: "overview", label: "Overview", href: href(""), icon: /* @__PURE__ */ (0,
|
|
2195
|
-
enabled.includes("companies") && { id: "companies", label: "Companies", href: href("companies"), icon: /* @__PURE__ */ (0,
|
|
2884
|
+
const nav = gate ? [{ id: "security", label: "Set up authenticator", href: href("security"), icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(IconShield, {}), active: true }] : [
|
|
2885
|
+
enabled.includes("overview") && { id: "overview", label: "Overview", href: href(""), icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(IconDashboard, {}), active: active("") },
|
|
2886
|
+
enabled.includes("companies") && { id: "companies", label: "Companies", href: href("companies"), icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(IconBuilding, {}), active: active("companies") },
|
|
2196
2887
|
...visiblePages.map((p) => ({ id: `page:${p.path}`, label: p.label, href: href(p.path), icon: p.icon, group: p.group, active: active(trim(p.path)) })),
|
|
2197
|
-
enabled.includes("
|
|
2198
|
-
enabled.includes("
|
|
2199
|
-
{ id: "
|
|
2888
|
+
enabled.includes("plans") && can("plans.read") && { id: "plans", label: "Plans", href: href("plans"), icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(IconTag, {}), active: active("plans") },
|
|
2889
|
+
enabled.includes("support") && can("support.request") && { id: "support", label: "Support access", href: href("support-access"), icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(IconLifebuoy, {}), active: active("support-access") },
|
|
2890
|
+
enabled.includes("audit") && can("audit.read") && { id: "audit", label: "Audit log", href: href("audit"), icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(IconList, {}), active: active("audit"), group: "Platform" },
|
|
2891
|
+
enabled.includes("admins") && can("admins.read") && { id: "admins", label: "Platform admins", href: href("admins"), icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(IconUsers, {}), active: active("admins"), group: "Platform" },
|
|
2892
|
+
{ id: "security", label: "Security", href: href("security"), icon: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(IconShield, {}), active: active("security"), group: "Platform" }
|
|
2200
2893
|
].filter(Boolean);
|
|
2201
2894
|
const value = {
|
|
2202
2895
|
client,
|
|
@@ -2207,36 +2900,39 @@ function PlatformConsole({
|
|
|
2207
2900
|
navigate,
|
|
2208
2901
|
can,
|
|
2209
2902
|
companyTabs,
|
|
2903
|
+
features: enabled,
|
|
2210
2904
|
companyLoginPath,
|
|
2211
2905
|
companyLoginUrl: (slug) => `${origin}${companyLoginPath(slug)}`,
|
|
2212
|
-
loginUrl: `${origin}${loginPath != null ? loginPath : `${
|
|
2906
|
+
loginUrl: `${origin}${loginPath != null ? loginPath : `${base3}/login`}`,
|
|
2213
2907
|
brand,
|
|
2214
2908
|
appName: appName != null ? appName : (_a = me == null ? void 0 : me.app) == null ? void 0 : _a.name
|
|
2215
2909
|
};
|
|
2216
2910
|
let body;
|
|
2217
|
-
if (me === void 0) body = /* @__PURE__ */ (0,
|
|
2218
|
-
else if (gate) body = /* @__PURE__ */ (0,
|
|
2219
|
-
else if (route.view === "overview") body = /* @__PURE__ */ (0,
|
|
2220
|
-
else if (route.view === "companies") body = /* @__PURE__ */ (0,
|
|
2221
|
-
else if (route.view === "new-company") body = /* @__PURE__ */ (0,
|
|
2222
|
-
else if (route.view === "company") body = /* @__PURE__ */ (0,
|
|
2223
|
-
else if (route.view === "admins") body = /* @__PURE__ */ (0,
|
|
2224
|
-
else if (route.view === "audit") body = /* @__PURE__ */ (0,
|
|
2225
|
-
else if (route.view === "security") body = /* @__PURE__ */ (0,
|
|
2911
|
+
if (me === void 0) body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "p-6", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_twreact_ui15.LoadingState, { variant: "stats" }) });
|
|
2912
|
+
else if (gate) body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SecurityView, { gate: true });
|
|
2913
|
+
else if (route.view === "overview") body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(OverviewView, {});
|
|
2914
|
+
else if (route.view === "companies") body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(CompaniesView, {});
|
|
2915
|
+
else if (route.view === "new-company") body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(NewCompanyView, {});
|
|
2916
|
+
else if (route.view === "company") body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(CompanyDetailView, { companyId: route.id }, route.id);
|
|
2917
|
+
else if (route.view === "admins") body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(AdminsView, {});
|
|
2918
|
+
else if (route.view === "audit") body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(AuditView, {});
|
|
2919
|
+
else if (route.view === "security") body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SecurityView, {});
|
|
2920
|
+
else if (route.view === "plans" && enabled.includes("plans") && can("plans.read")) body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(PlansView, {});
|
|
2921
|
+
else if (route.view === "support" && enabled.includes("support") && can("support.request")) body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SupportAccessView, {});
|
|
2226
2922
|
else if (route.view === "page") body = route.page.render({ client, path: route.rest, navigate, href, me });
|
|
2227
|
-
else body = /* @__PURE__ */ (0,
|
|
2228
|
-
return /* @__PURE__ */ (0,
|
|
2923
|
+
else body = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "p-6", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_twreact_ui15.EmptyState, { title: "Page not found", description: "This console page does not exist." }) });
|
|
2924
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(PlatformProvider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(StepUpProvider, { client, totpEnrolled: Boolean((_b = me == null ? void 0 : me.admin) == null ? void 0 : _b.totpEnrolled), children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(PlatformShell, { brand, appName: value.appName, nav, adminEmail: (_d = (_c = me == null ? void 0 : me.admin) == null ? void 0 : _c.email) != null ? _d : adminEmail, onSignOut: signOut, linkAs: Link, pageTitle: (_e = nav.find((n) => n.active)) == null ? void 0 : _e.label, children: body }) }) });
|
|
2229
2925
|
}
|
|
2230
2926
|
|
|
2231
2927
|
// src/platform/PlatformLogin.jsx
|
|
2232
|
-
var
|
|
2233
|
-
var
|
|
2928
|
+
var import_react22 = __toESM(require("react"), 1);
|
|
2929
|
+
var import_twreact_ui16 = require("@dreamtree-org/twreact-ui");
|
|
2234
2930
|
|
|
2235
2931
|
// src/AuthShell.jsx
|
|
2236
|
-
var
|
|
2932
|
+
var import_react21 = require("react");
|
|
2237
2933
|
|
|
2238
2934
|
// src/DemoAccounts.jsx
|
|
2239
|
-
var
|
|
2935
|
+
var import_react17 = require("react");
|
|
2240
2936
|
|
|
2241
2937
|
// src/demoFill.js
|
|
2242
2938
|
function normalizeDemoAccounts(data) {
|
|
@@ -2250,12 +2946,12 @@ function normalizeDemoAccounts(data) {
|
|
|
2250
2946
|
}
|
|
2251
2947
|
|
|
2252
2948
|
// src/DemoAccounts.jsx
|
|
2253
|
-
var
|
|
2949
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
2254
2950
|
function DemoAccounts({ load, onSelect, className = "" }) {
|
|
2255
|
-
const [state, setState] = (0,
|
|
2256
|
-
const [picked, setPicked] = (0,
|
|
2257
|
-
const loadRef = (0,
|
|
2258
|
-
(0,
|
|
2951
|
+
const [state, setState] = (0, import_react17.useState)({ status: "loading", accounts: [] });
|
|
2952
|
+
const [picked, setPicked] = (0, import_react17.useState)(null);
|
|
2953
|
+
const loadRef = (0, import_react17.useRef)(load);
|
|
2954
|
+
(0, import_react17.useEffect)(() => {
|
|
2259
2955
|
let live = true;
|
|
2260
2956
|
Promise.resolve().then(() => loadRef.current()).then((data) => live && setState({ status: "ready", ...normalizeDemoAccounts(data) })).catch(() => live && setState({ status: "ready", accounts: [] }));
|
|
2261
2957
|
return () => {
|
|
@@ -2267,10 +2963,10 @@ function DemoAccounts({ load, onSelect, className = "" }) {
|
|
|
2267
2963
|
setPicked(i);
|
|
2268
2964
|
onSelect == null ? void 0 : onSelect(account);
|
|
2269
2965
|
};
|
|
2270
|
-
return /* @__PURE__ */ (0,
|
|
2271
|
-
/* @__PURE__ */ (0,
|
|
2272
|
-
state.status === "loading" ? /* @__PURE__ */ (0,
|
|
2273
|
-
/* @__PURE__ */ (0,
|
|
2966
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: `dt-auth-demo ${className}`, "aria-busy": state.status === "loading" || void 0, children: [
|
|
2967
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "dt-auth-demo-label", children: "Demo accounts" }),
|
|
2968
|
+
state.status === "loading" ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dt-auth-demo-chips", "aria-hidden": "true", children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "dt-auth-demo-chip dt-auth-demo-chip--skeleton" }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
|
|
2969
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dt-auth-demo-chips", children: state.accounts.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
2274
2970
|
"button",
|
|
2275
2971
|
{
|
|
2276
2972
|
type: "button",
|
|
@@ -2282,22 +2978,125 @@ function DemoAccounts({ load, onSelect, className = "" }) {
|
|
|
2282
2978
|
},
|
|
2283
2979
|
`${a.label}-${a.identifier}`
|
|
2284
2980
|
)) }),
|
|
2285
|
-
/* @__PURE__ */ (0,
|
|
2981
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "dt-auth-demo-note", children: state.sharedPassword ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
|
|
2286
2982
|
"Shared password ",
|
|
2287
|
-
/* @__PURE__ */ (0,
|
|
2983
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("code", { children: state.sharedPassword }),
|
|
2288
2984
|
" \u2014 tap a role to fill."
|
|
2289
2985
|
] }) : "Tap a role to fill." })
|
|
2290
2986
|
] })
|
|
2291
2987
|
] });
|
|
2292
2988
|
}
|
|
2293
2989
|
|
|
2990
|
+
// src/DreamtreeSso.jsx
|
|
2991
|
+
var import_react19 = require("react");
|
|
2992
|
+
|
|
2993
|
+
// src/sso.js
|
|
2994
|
+
function decodeKey(part) {
|
|
2995
|
+
const key = part.split("=")[0];
|
|
2996
|
+
try {
|
|
2997
|
+
return decodeURIComponent(key.replace(/\+/g, " "));
|
|
2998
|
+
} catch {
|
|
2999
|
+
return key;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
function buildSsoUrl(startUrl, { returnTo, companySlug } = {}) {
|
|
3003
|
+
const raw = String(startUrl != null ? startUrl : "");
|
|
3004
|
+
const hashAt = raw.indexOf("#");
|
|
3005
|
+
const hash = hashAt === -1 ? "" : raw.slice(hashAt);
|
|
3006
|
+
const beforeHash = hashAt === -1 ? raw : raw.slice(0, hashAt);
|
|
3007
|
+
const queryAt = beforeHash.indexOf("?");
|
|
3008
|
+
const path = queryAt === -1 ? beforeHash : beforeHash.slice(0, queryAt);
|
|
3009
|
+
const query = queryAt === -1 ? "" : beforeHash.slice(queryAt + 1);
|
|
3010
|
+
const add = [
|
|
3011
|
+
["return_to", returnTo],
|
|
3012
|
+
["dt_company", companySlug]
|
|
3013
|
+
].filter(([, v]) => v != null && v !== "");
|
|
3014
|
+
const replaced = new Set(add.map(([k]) => k));
|
|
3015
|
+
const params = query.split("&").filter((p) => p && !replaced.has(decodeKey(p))).concat(add.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`));
|
|
3016
|
+
return `${path}${params.length ? `?${params.join("&")}` : ""}${hash}`;
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
// src/icons.jsx
|
|
3020
|
+
var import_react18 = require("react");
|
|
3021
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
3022
|
+
var base2 = {
|
|
3023
|
+
viewBox: "0 0 24 24",
|
|
3024
|
+
fill: "none",
|
|
3025
|
+
stroke: "currentColor",
|
|
3026
|
+
strokeWidth: 2,
|
|
3027
|
+
strokeLinecap: "round",
|
|
3028
|
+
strokeLinejoin: "round",
|
|
3029
|
+
"aria-hidden": "true",
|
|
3030
|
+
focusable: "false"
|
|
3031
|
+
};
|
|
3032
|
+
var make2 = (paths) => function Icon({ size = 16, className = "dt-lp-icon" }) {
|
|
3033
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("svg", { ...base2, width: size, height: size, className, children: paths });
|
|
3034
|
+
};
|
|
3035
|
+
var ArrowRight = make2(/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("path", { d: "M5 12h14M13 6l6 6-6 6" }));
|
|
3036
|
+
var ChevronRight = make2(/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("path", { d: "m9 6 6 6-6 6" }));
|
|
3037
|
+
var Check = make2(/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("path", { d: "M20 6 9 17l-5-5" }));
|
|
3038
|
+
var Help = make2(
|
|
3039
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
|
|
3040
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
|
|
3041
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("path", { d: "M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01" })
|
|
3042
|
+
] })
|
|
3043
|
+
);
|
|
3044
|
+
var Alert9 = make2(
|
|
3045
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
|
|
3046
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("path", { d: "M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" }),
|
|
3047
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("path", { d: "M12 9v4M12 17h.01" })
|
|
3048
|
+
] })
|
|
3049
|
+
);
|
|
3050
|
+
var MenuIcon = make2(/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("path", { d: "M4 6h16M4 12h16M4 18h16" }));
|
|
3051
|
+
var Close = make2(/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("path", { d: "M18 6 6 18M6 6l12 12" }));
|
|
3052
|
+
var Calendar = make2(
|
|
3053
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
|
|
3054
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }),
|
|
3055
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("path", { d: "M16 2v4M8 2v4M3 10h18m-12 6 2 2 4-4" })
|
|
3056
|
+
] })
|
|
3057
|
+
);
|
|
3058
|
+
|
|
3059
|
+
// src/DreamtreeSso.jsx
|
|
3060
|
+
var import_jsx_runtime20 = require("react/jsx-runtime");
|
|
3061
|
+
var DEFAULT_SSO_LABEL = "Continue with Dreamtree";
|
|
3062
|
+
function DreamtreeSso({ provider, children }) {
|
|
3063
|
+
const { startUrl, mode, label = DEFAULT_SSO_LABEL, companySlug, returnTo } = provider;
|
|
3064
|
+
const isSso = mode === "sso";
|
|
3065
|
+
const autoRedirect = isSso && provider.autoRedirect !== false;
|
|
3066
|
+
const href = buildSsoUrl(startUrl, { returnTo, companySlug });
|
|
3067
|
+
const redirected = (0, import_react19.useRef)(false);
|
|
3068
|
+
(0, import_react19.useEffect)(() => {
|
|
3069
|
+
if (!autoRedirect || redirected.current) return;
|
|
3070
|
+
redirected.current = true;
|
|
3071
|
+
window.location.assign(href);
|
|
3072
|
+
}, [autoRedirect, href]);
|
|
3073
|
+
const button = /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("a", { className: "dt-auth-sso-btn", href, children: [
|
|
3074
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { children: label }),
|
|
3075
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ArrowRight, { size: 16, className: "dt-auth-sso-icon" })
|
|
3076
|
+
] });
|
|
3077
|
+
if (isSso) {
|
|
3078
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "dt-auth-sso dt-auth-sso--only", children: [
|
|
3079
|
+
autoRedirect ? /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("p", { className: "dt-auth-sso-status", role: "status", "aria-live": "polite", children: [
|
|
3080
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "dt-auth-sso-spinner", "aria-hidden": "true" }),
|
|
3081
|
+
"Redirecting to Dreamtree ID\u2026"
|
|
3082
|
+
] }) : null,
|
|
3083
|
+
button
|
|
3084
|
+
] });
|
|
3085
|
+
}
|
|
3086
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
|
|
3087
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "dt-auth-sso", children: button }),
|
|
3088
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "dt-auth-or", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { children: "or" }) }),
|
|
3089
|
+
children
|
|
3090
|
+
] });
|
|
3091
|
+
}
|
|
3092
|
+
|
|
2294
3093
|
// src/useTapReveal.js
|
|
2295
|
-
var
|
|
3094
|
+
var import_react20 = require("react");
|
|
2296
3095
|
function useTapReveal({ taps = 4, windowMs = 1500 } = {}) {
|
|
2297
|
-
const [revealed, setRevealed] = (0,
|
|
2298
|
-
const count = (0,
|
|
2299
|
-
const last = (0,
|
|
2300
|
-
const onClick = (0,
|
|
3096
|
+
const [revealed, setRevealed] = (0, import_react20.useState)(false);
|
|
3097
|
+
const count = (0, import_react20.useRef)(0);
|
|
3098
|
+
const last = (0, import_react20.useRef)(0);
|
|
3099
|
+
const onClick = (0, import_react20.useCallback)(() => {
|
|
2301
3100
|
if (revealed) return;
|
|
2302
3101
|
const now = Date.now();
|
|
2303
3102
|
count.current = now - last.current <= windowMs ? count.current + 1 : 1;
|
|
@@ -2308,7 +3107,7 @@ function useTapReveal({ taps = 4, windowMs = 1500 } = {}) {
|
|
|
2308
3107
|
}
|
|
2309
3108
|
|
|
2310
3109
|
// src/AuthShell.jsx
|
|
2311
|
-
var
|
|
3110
|
+
var import_jsx_runtime21 = require("react/jsx-runtime");
|
|
2312
3111
|
var DEFAULT_LEGAL = [
|
|
2313
3112
|
{ label: "Privacy Policy", href: "/privacy" },
|
|
2314
3113
|
{ label: "Terms", href: "/terms" },
|
|
@@ -2316,10 +3115,10 @@ var DEFAULT_LEGAL = [
|
|
|
2316
3115
|
];
|
|
2317
3116
|
function BrandMark({ brand, size }) {
|
|
2318
3117
|
if (brand.logoSrc) {
|
|
2319
|
-
return /* @__PURE__ */ (0,
|
|
3118
|
+
return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("img", { className: "dt-auth-mark-img", src: brand.logoSrc, alt: "", width: size, height: size });
|
|
2320
3119
|
}
|
|
2321
|
-
if (brand.mark) return /* @__PURE__ */ (0,
|
|
2322
|
-
return /* @__PURE__ */ (0,
|
|
3120
|
+
if (brand.mark) return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "dt-auth-mark-node", children: brand.mark });
|
|
3121
|
+
return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "dt-auth-monogram", style: { width: size, height: size }, "aria-hidden": "true", children: brand.name.charAt(0) });
|
|
2323
3122
|
}
|
|
2324
3123
|
function brandStyle(brand) {
|
|
2325
3124
|
return {
|
|
@@ -2338,52 +3137,53 @@ function AuthShell({
|
|
|
2338
3137
|
legalLinks = DEFAULT_LEGAL,
|
|
2339
3138
|
linkComponent: LinkComponent,
|
|
2340
3139
|
className = "",
|
|
2341
|
-
demoAccounts
|
|
3140
|
+
demoAccounts,
|
|
3141
|
+
ssoProvider
|
|
2342
3142
|
}) {
|
|
2343
3143
|
var _a, _b;
|
|
2344
|
-
const headingId = (0,
|
|
3144
|
+
const headingId = (0, import_react21.useId)();
|
|
2345
3145
|
const { revealed, tapProps } = useTapReveal({ taps: (_a = demoAccounts == null ? void 0 : demoAccounts.taps) != null ? _a : 4 });
|
|
2346
3146
|
const isPlatform = variant === "platform";
|
|
2347
3147
|
const headline = isPlatform ? brand.platformHeadline || "Platform administration" : brand.headline;
|
|
2348
3148
|
const pitch = isPlatform ? brand.platformPitch || `Super administrator console for ${brand.name}. Every action here is audited.` : brand.pitch;
|
|
2349
|
-
const renderLink = (l) => LinkComponent ? /* @__PURE__ */ (0,
|
|
2350
|
-
return /* @__PURE__ */ (0,
|
|
3149
|
+
const renderLink = (l) => LinkComponent ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(LinkComponent, { to: l.href, href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("a", { href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href);
|
|
3150
|
+
return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
|
|
2351
3151
|
"div",
|
|
2352
3152
|
{
|
|
2353
3153
|
className: `dt-auth ${isPlatform ? "dt-auth--platform" : ""} ${className}`,
|
|
2354
3154
|
style: brandStyle(brand),
|
|
2355
3155
|
"data-product": brand.id,
|
|
2356
3156
|
children: [
|
|
2357
|
-
/* @__PURE__ */ (0,
|
|
2358
|
-
/* @__PURE__ */ (0,
|
|
2359
|
-
/* @__PURE__ */ (0,
|
|
2360
|
-
/* @__PURE__ */ (0,
|
|
2361
|
-
isPlatform ? /* @__PURE__ */ (0,
|
|
3157
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("aside", { className: "dt-auth-panel", children: [
|
|
3158
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("a", { href: "/", className: "dt-auth-brand", children: [
|
|
3159
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(BrandMark, { brand, size: 28 }),
|
|
3160
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "dt-auth-brand-name", children: brand.name }),
|
|
3161
|
+
isPlatform ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "dt-auth-badge", children: "Platform" }) : null
|
|
2362
3162
|
] }),
|
|
2363
|
-
/* @__PURE__ */ (0,
|
|
2364
|
-
/* @__PURE__ */ (0,
|
|
2365
|
-
pitch ? /* @__PURE__ */ (0,
|
|
2366
|
-
!isPlatform && ((_b = brand.points) == null ? void 0 : _b.length) ? /* @__PURE__ */ (0,
|
|
3163
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "dt-auth-pitch", children: [
|
|
3164
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h2", { children: headline }),
|
|
3165
|
+
pitch ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { children: pitch }) : null,
|
|
3166
|
+
!isPlatform && ((_b = brand.points) == null ? void 0 : _b.length) ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("ul", { children: brand.points.map((p) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("li", { children: p }, p)) }) : null
|
|
2367
3167
|
] }),
|
|
2368
|
-
/* @__PURE__ */ (0,
|
|
3168
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { className: "dt-auth-panel-foot", children: brand.category || "by Dreamtree-Org" })
|
|
2369
3169
|
] }),
|
|
2370
|
-
/* @__PURE__ */ (0,
|
|
2371
|
-
/* @__PURE__ */ (0,
|
|
2372
|
-
/* @__PURE__ */ (0,
|
|
2373
|
-
/* @__PURE__ */ (0,
|
|
2374
|
-
isPlatform ? /* @__PURE__ */ (0,
|
|
3170
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("main", { className: "dt-auth-main", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "dt-auth-column", children: [
|
|
3171
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("a", { href: "/", className: "dt-auth-brand dt-auth-brand--mobile", children: [
|
|
3172
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(BrandMark, { brand, size: 24 }),
|
|
3173
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "dt-auth-brand-name", children: brand.name }),
|
|
3174
|
+
isPlatform ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "dt-auth-badge", children: "Platform" }) : null
|
|
2375
3175
|
] }),
|
|
2376
|
-
/* @__PURE__ */ (0,
|
|
2377
|
-
/* @__PURE__ */ (0,
|
|
2378
|
-
/* @__PURE__ */ (0,
|
|
2379
|
-
subtitle ? /* @__PURE__ */ (0,
|
|
3176
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("section", { className: "dt-auth-card", "aria-labelledby": headingId, children: [
|
|
3177
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("header", { className: "dt-auth-head", children: [
|
|
3178
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h1", { id: headingId, ...demoAccounts ? tapProps : {}, children: title }),
|
|
3179
|
+
subtitle ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { children: subtitle }) : null
|
|
2380
3180
|
] }),
|
|
2381
|
-
children,
|
|
2382
|
-
demoAccounts && revealed ? /* @__PURE__ */ (0,
|
|
3181
|
+
ssoProvider ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(DreamtreeSso, { provider: ssoProvider, children }) : children,
|
|
3182
|
+
demoAccounts && revealed && (ssoProvider == null ? void 0 : ssoProvider.mode) !== "sso" ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(DemoAccounts, { load: demoAccounts.load, onSelect: demoAccounts.onSelect }) : null
|
|
2383
3183
|
] }),
|
|
2384
|
-
footer ? /* @__PURE__ */ (0,
|
|
2385
|
-
(legalLinks == null ? void 0 : legalLinks.length) ? /* @__PURE__ */ (0,
|
|
2386
|
-
i > 0 ? /* @__PURE__ */ (0,
|
|
3184
|
+
footer ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "dt-auth-footer", children: footer }) : null,
|
|
3185
|
+
(legalLinks == null ? void 0 : legalLinks.length) ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("nav", { className: "dt-auth-legal", "aria-label": "Legal", children: legalLinks.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("span", { className: "dt-auth-legal-item", children: [
|
|
3186
|
+
i > 0 ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { "aria-hidden": "true", children: "\xB7" }) : null,
|
|
2387
3187
|
renderLink(l)
|
|
2388
3188
|
] }, l.href)) }) : null
|
|
2389
3189
|
] }) })
|
|
@@ -2393,17 +3193,17 @@ function AuthShell({
|
|
|
2393
3193
|
}
|
|
2394
3194
|
|
|
2395
3195
|
// src/platform/PlatformLogin.jsx
|
|
2396
|
-
var
|
|
3196
|
+
var import_jsx_runtime22 = require("react/jsx-runtime");
|
|
2397
3197
|
function PlatformLogin({ brand, basePath, client: given, onSignedIn, redirectTo = "/platform", title = "Platform administration", subtitle = "Administrator sign-in", linkComponent, legalLinks }) {
|
|
2398
|
-
const client = (0,
|
|
3198
|
+
const client = (0, import_react22.useMemo)(() => given != null ? given : createPlatformClient({ basePath, onUnauthorized: () => {
|
|
2399
3199
|
} }), [given, basePath]);
|
|
2400
|
-
const [step, setStep] = (0,
|
|
2401
|
-
const [email, setEmail] = (0,
|
|
2402
|
-
const [password, setPassword] = (0,
|
|
2403
|
-
const [ticket, setTicket] = (0,
|
|
2404
|
-
const [code, setCode] = (0,
|
|
2405
|
-
const [busy, setBusy] = (0,
|
|
2406
|
-
const [error, setError] = (0,
|
|
3200
|
+
const [step, setStep] = (0, import_react22.useState)("password");
|
|
3201
|
+
const [email, setEmail] = (0, import_react22.useState)("");
|
|
3202
|
+
const [password, setPassword] = (0, import_react22.useState)("");
|
|
3203
|
+
const [ticket, setTicket] = (0, import_react22.useState)(null);
|
|
3204
|
+
const [code, setCode] = (0, import_react22.useState)("");
|
|
3205
|
+
const [busy, setBusy] = (0, import_react22.useState)(false);
|
|
3206
|
+
const [error, setError] = (0, import_react22.useState)(null);
|
|
2407
3207
|
const done = (res) => {
|
|
2408
3208
|
if (onSignedIn) onSignedIn(res);
|
|
2409
3209
|
else if (typeof window !== "undefined") window.location.assign(redirectTo);
|
|
@@ -2443,18 +3243,18 @@ function PlatformLogin({ brand, basePath, client: given, onSignedIn, redirectTo
|
|
|
2443
3243
|
setBusy(false);
|
|
2444
3244
|
}
|
|
2445
3245
|
};
|
|
2446
|
-
return /* @__PURE__ */ (0,
|
|
2447
|
-
/* @__PURE__ */ (0,
|
|
2448
|
-
/* @__PURE__ */ (0,
|
|
2449
|
-
error && /* @__PURE__ */ (0,
|
|
2450
|
-
/* @__PURE__ */ (0,
|
|
2451
|
-
] }) : /* @__PURE__ */ (0,
|
|
2452
|
-
/* @__PURE__ */ (0,
|
|
2453
|
-
/* @__PURE__ */ (0,
|
|
2454
|
-
error && /* @__PURE__ */ (0,
|
|
2455
|
-
/* @__PURE__ */ (0,
|
|
2456
|
-
/* @__PURE__ */ (0,
|
|
2457
|
-
|
|
3246
|
+
return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(AuthShell, { brand, variant: "platform", title, subtitle, linkComponent, legalLinks, children: step === "password" ? /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("form", { onSubmit: submitPassword, className: "flex flex-col gap-4", children: [
|
|
3247
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_twreact_ui16.Input, { label: "Admin email", type: "email", autoComplete: "username", autoFocus: true, value: email, onChange: (e) => setEmail(e.target.value) }),
|
|
3248
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_twreact_ui16.Input, { label: "Password", type: "password", autoComplete: "current-password", value: password, onChange: (e) => setPassword(e.target.value) }),
|
|
3249
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_twreact_ui16.Alert, { variant: "error", children: error }),
|
|
3250
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_twreact_ui16.Button, { type: "submit", loading: busy, disabled: !email || !password, fullWidth: true, children: "Sign in" })
|
|
3251
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("form", { onSubmit: submitCode, className: "flex flex-col gap-4", children: [
|
|
3252
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { className: "text-sm", children: "Enter the 6-digit code from your authenticator app." }),
|
|
3253
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_twreact_ui16.Input, { label: "Authenticator code", otp: true, autoFocus: true, value: code, onChange: (e) => setCode(e.target.value.replace(/\s/g, "")) }),
|
|
3254
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_twreact_ui16.Alert, { variant: "error", children: error }),
|
|
3255
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_twreact_ui16.Button, { type: "submit", loading: busy, disabled: code.length < 6, fullWidth: true, children: "Verify" }),
|
|
3256
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
3257
|
+
import_twreact_ui16.Button,
|
|
2458
3258
|
{
|
|
2459
3259
|
variant: "ghost",
|
|
2460
3260
|
onClick: () => {
|