@dreamtree-org/landing-kit 0.7.2 → 0.9.0
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 +55 -0
- package/PLATFORM-API.md +213 -0
- package/README.md +96 -0
- package/ai-skills/landing-kit.md +97 -0
- package/dist/index.cjs +323 -15
- package/dist/index.js +323 -15
- package/dist/platform.cjs +2469 -0
- package/dist/platform.js +2450 -0
- package/dist/schema.cjs +324 -3
- package/dist/schema.js +304 -3
- package/index.d.ts +15 -1
- package/package.json +26 -4
- package/platform.d.ts +342 -0
- package/schema.d.ts +50 -0
package/dist/platform.js
ADDED
|
@@ -0,0 +1,2450 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/platform/client.js
|
|
4
|
+
var PLATFORM_API_BASE = "/api/platform";
|
|
5
|
+
var PLATFORM_LOGIN_PATH = "/platform/login";
|
|
6
|
+
var STEP_UP_REQUIRED = "STEP_UP_REQUIRED";
|
|
7
|
+
var PLATFORM_FEATURES = ["overview", "companies", "jobs", "plans", "audit", "support", "settings", "admins"];
|
|
8
|
+
var SHIPPED_FEATURES = ["overview", "companies", "jobs", "admins"];
|
|
9
|
+
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
10
|
+
var CSRF_HEADER = "x-csrf-token";
|
|
11
|
+
var PlatformApiError = class extends Error {
|
|
12
|
+
constructor(message, status, code, body) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "PlatformApiError";
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.body = body;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
function isStepUpRequired(err) {
|
|
21
|
+
return err instanceof PlatformApiError && err.status === 403 && err.code === STEP_UP_REQUIRED;
|
|
22
|
+
}
|
|
23
|
+
function readPlatformCsrf(cookieString, name) {
|
|
24
|
+
for (const part of String(cookieString != null ? cookieString : "").split(/;\s*/)) {
|
|
25
|
+
const eq = part.indexOf("=");
|
|
26
|
+
if (eq < 1) continue;
|
|
27
|
+
const key = part.slice(0, eq);
|
|
28
|
+
if (name ? key === name : key.endsWith("_platform_csrf")) {
|
|
29
|
+
try {
|
|
30
|
+
return decodeURIComponent(part.slice(eq + 1));
|
|
31
|
+
} catch {
|
|
32
|
+
return part.slice(eq + 1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function joinUrl(basePath, path, query) {
|
|
39
|
+
const base2 = basePath.replace(/\/+$/, "");
|
|
40
|
+
const rel = path ? `/${String(path).replace(/^\/+/, "")}` : "";
|
|
41
|
+
const params = new URLSearchParams();
|
|
42
|
+
for (const [k, v] of Object.entries(query != null ? query : {})) {
|
|
43
|
+
if (v === void 0 || v === null || v === "") continue;
|
|
44
|
+
params.append(k, String(v));
|
|
45
|
+
}
|
|
46
|
+
const qs = params.toString();
|
|
47
|
+
return `${base2}${rel}${qs ? `?${qs}` : ""}`;
|
|
48
|
+
}
|
|
49
|
+
function createPlatformClient(opts = {}) {
|
|
50
|
+
var _a, _b, _c, _d, _e;
|
|
51
|
+
const basePath = (_a = opts.basePath) != null ? _a : PLATFORM_API_BASE;
|
|
52
|
+
const loginPath = (_b = opts.loginPath) != null ? _b : PLATFORM_LOGIN_PATH;
|
|
53
|
+
const doFetch = (_c = opts.fetch) != null ? _c : ((...args) => globalThis.fetch(...args));
|
|
54
|
+
const cookies = (_d = opts.cookies) != null ? _d : (() => typeof document === "undefined" ? "" : document.cookie);
|
|
55
|
+
const onUnauthorized = (_e = opts.onUnauthorized) != null ? _e : (() => {
|
|
56
|
+
if (typeof window !== "undefined") window.location.assign(loginPath);
|
|
57
|
+
});
|
|
58
|
+
const url = (path, query) => joinUrl(basePath, path, query);
|
|
59
|
+
async function raw(path, { method = "GET", json, query, headers, ...init } = {}) {
|
|
60
|
+
const verb = method.toUpperCase();
|
|
61
|
+
const h = new Headers(headers);
|
|
62
|
+
if (json !== void 0) h.set("Content-Type", "application/json");
|
|
63
|
+
if (!h.has("Accept")) h.set("Accept", "application/json");
|
|
64
|
+
if (!SAFE_METHODS.has(verb)) {
|
|
65
|
+
const token = readPlatformCsrf(cookies(), opts.csrfCookie);
|
|
66
|
+
if (token) h.set(CSRF_HEADER, token);
|
|
67
|
+
}
|
|
68
|
+
return doFetch(url(path, query), {
|
|
69
|
+
credentials: "same-origin",
|
|
70
|
+
cache: "no-store",
|
|
71
|
+
...init,
|
|
72
|
+
method: verb,
|
|
73
|
+
headers: h,
|
|
74
|
+
body: json !== void 0 ? JSON.stringify(json) : init.body
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async function request(path, init = {}) {
|
|
78
|
+
const res = await raw(path, init);
|
|
79
|
+
const body = await res.json().catch(() => ({}));
|
|
80
|
+
if (res.status === 401) {
|
|
81
|
+
const err = new PlatformApiError((body == null ? void 0 : body.error) || "Signed out", 401, body == null ? void 0 : body.code, body);
|
|
82
|
+
if (!init.skipAuthRedirect) onUnauthorized(err);
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
if (!res.ok) throw new PlatformApiError((body == null ? void 0 : body.error) || `Request failed (${res.status})`, res.status, body == null ? void 0 : body.code, body);
|
|
86
|
+
return body;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
basePath,
|
|
90
|
+
loginPath,
|
|
91
|
+
url,
|
|
92
|
+
raw,
|
|
93
|
+
request,
|
|
94
|
+
get: (path, query, init) => request(path, { ...init, method: "GET", query }),
|
|
95
|
+
post: (path, json, init) => request(path, { ...init, method: "POST", json: json != null ? json : {} }),
|
|
96
|
+
patch: (path, json, init) => request(path, { ...init, method: "PATCH", json: json != null ? json : {} }),
|
|
97
|
+
put: (path, json, init) => request(path, { ...init, method: "PUT", json: json != null ? json : {} }),
|
|
98
|
+
del: (path, init) => request(path, { ...init, method: "DELETE" })
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function createStepUp(client, ask) {
|
|
102
|
+
return async function sensitive(action) {
|
|
103
|
+
try {
|
|
104
|
+
return await action();
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (!isStepUpRequired(err)) throw err;
|
|
107
|
+
for (; ; ) {
|
|
108
|
+
const secret = await ask(err);
|
|
109
|
+
if (!secret) throw new PlatformApiError("Cancelled", 0, "CANCELLED");
|
|
110
|
+
try {
|
|
111
|
+
await client.post("auth/confirm", secret, { skipAuthRedirect: true });
|
|
112
|
+
break;
|
|
113
|
+
} catch (confirmErr) {
|
|
114
|
+
if (confirmErr instanceof PlatformApiError && confirmErr.status === 401) {
|
|
115
|
+
err = confirmErr;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
throw confirmErr;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return action();
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function enabledFeatures(requested = PLATFORM_FEATURES, me) {
|
|
126
|
+
const advertised = me == null ? void 0 : me.features;
|
|
127
|
+
return requested.filter((f) => {
|
|
128
|
+
if (f === "overview" || f === "companies" || f === "admins") return true;
|
|
129
|
+
if (!advertised) return SHIPPED_FEATURES.includes(f);
|
|
130
|
+
return advertised[f] === true;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function formatDate(value) {
|
|
134
|
+
if (!value) return "\u2014";
|
|
135
|
+
const d = new Date(value);
|
|
136
|
+
if (Number.isNaN(d.getTime())) return "\u2014";
|
|
137
|
+
return d.toLocaleString(void 0, { dateStyle: "medium", timeStyle: "short" });
|
|
138
|
+
}
|
|
139
|
+
function timeAgo(value, now = Date.now()) {
|
|
140
|
+
if (!value) return "never";
|
|
141
|
+
const s = Math.round((now - new Date(value).getTime()) / 1e3);
|
|
142
|
+
if (Number.isNaN(s)) return "never";
|
|
143
|
+
if (s < 60) return "just now";
|
|
144
|
+
if (s < 3600) return `${Math.floor(s / 60)} min ago`;
|
|
145
|
+
if (s < 86400) return `${Math.floor(s / 3600)} h ago`;
|
|
146
|
+
return `${Math.floor(s / 86400)} d ago`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/platform/PlatformConsole.jsx
|
|
150
|
+
import React13, { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo3, useState as useState11 } from "react";
|
|
151
|
+
import { EmptyState as EmptyState4, LoadingState as LoadingState6 } from "@dreamtree-org/twreact-ui";
|
|
152
|
+
|
|
153
|
+
// src/platform/context.jsx
|
|
154
|
+
import React, { createContext, forwardRef, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
155
|
+
import { jsx } from "react/jsx-runtime";
|
|
156
|
+
var Ctx = createContext(null);
|
|
157
|
+
function usePlatform() {
|
|
158
|
+
const v = useContext(Ctx);
|
|
159
|
+
if (!v) throw new Error("Platform console components must render inside <PlatformConsole> (or <PlatformProvider>)");
|
|
160
|
+
return v;
|
|
161
|
+
}
|
|
162
|
+
function PlatformProvider({ value, children }) {
|
|
163
|
+
return /* @__PURE__ */ jsx(Ctx.Provider, { value, children });
|
|
164
|
+
}
|
|
165
|
+
function makeLink(linkAs, navigate) {
|
|
166
|
+
if (linkAs) return linkAs;
|
|
167
|
+
return forwardRef(function ConsoleLink({ href, onClick, children, ...rest }, ref) {
|
|
168
|
+
return /* @__PURE__ */ jsx(
|
|
169
|
+
"a",
|
|
170
|
+
{
|
|
171
|
+
ref,
|
|
172
|
+
href,
|
|
173
|
+
onClick: (e) => {
|
|
174
|
+
onClick == null ? void 0 : onClick(e);
|
|
175
|
+
if (e.defaultPrevented || !navigate || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
navigate(href);
|
|
178
|
+
},
|
|
179
|
+
...rest,
|
|
180
|
+
children
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function useBrowserLocation(enabled) {
|
|
186
|
+
const read = () => typeof window === "undefined" ? "/" : window.location.pathname;
|
|
187
|
+
const [pathname, setPathname] = useState(read);
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (!enabled) return void 0;
|
|
190
|
+
const on = () => setPathname(read());
|
|
191
|
+
window.addEventListener("popstate", on);
|
|
192
|
+
return () => window.removeEventListener("popstate", on);
|
|
193
|
+
}, [enabled]);
|
|
194
|
+
const navigate = useCallback((href) => {
|
|
195
|
+
window.history.pushState(null, "", href);
|
|
196
|
+
setPathname(read());
|
|
197
|
+
}, []);
|
|
198
|
+
return { pathname, navigate };
|
|
199
|
+
}
|
|
200
|
+
function useLoad(path, query) {
|
|
201
|
+
const { client } = usePlatform();
|
|
202
|
+
const key = path === null ? null : client.url(path, query);
|
|
203
|
+
const [state, setState] = useState({ data: null, error: null, loading: path !== null });
|
|
204
|
+
const reload = useCallback(async () => {
|
|
205
|
+
if (path === null) return;
|
|
206
|
+
setState((s) => ({ ...s, loading: true }));
|
|
207
|
+
try {
|
|
208
|
+
const data = await client.get(path, query);
|
|
209
|
+
setState({ data, error: null, loading: false });
|
|
210
|
+
} catch (e) {
|
|
211
|
+
setState((s) => ({ data: s.data, error: e, loading: false }));
|
|
212
|
+
}
|
|
213
|
+
}, [key]);
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
void reload();
|
|
216
|
+
}, [reload]);
|
|
217
|
+
return useMemo(() => ({ ...state, reload, setData: (data) => setState((s) => ({ ...s, data })) }), [state, reload]);
|
|
218
|
+
}
|
|
219
|
+
function errorText(e) {
|
|
220
|
+
if (!e || e.code === "CANCELLED") return null;
|
|
221
|
+
return e.message || "Something went wrong. Try again.";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/platform/StepUp.jsx
|
|
225
|
+
import React2, { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useMemo as useMemo2, useRef, useState as useState2 } from "react";
|
|
226
|
+
import { Button, Dialog, Input } from "@dreamtree-org/twreact-ui";
|
|
227
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
228
|
+
var Ctx2 = createContext2(null);
|
|
229
|
+
function useSensitive() {
|
|
230
|
+
const v = useContext2(Ctx2);
|
|
231
|
+
if (!v) throw new Error("useSensitive must be used inside <StepUpProvider>");
|
|
232
|
+
return v;
|
|
233
|
+
}
|
|
234
|
+
function StepUpProvider({ client, totpEnrolled = false, children }) {
|
|
235
|
+
const [open, setOpen] = useState2(false);
|
|
236
|
+
const [secret, setSecret] = useState2("");
|
|
237
|
+
const [error, setError] = useState2(null);
|
|
238
|
+
const pending = useRef(null);
|
|
239
|
+
const ask = useCallback2(
|
|
240
|
+
(reason) => new Promise((resolve) => {
|
|
241
|
+
pending.current = resolve;
|
|
242
|
+
setSecret("");
|
|
243
|
+
setError((reason == null ? void 0 : reason.status) === 401 ? reason.message : null);
|
|
244
|
+
setOpen(true);
|
|
245
|
+
}),
|
|
246
|
+
[]
|
|
247
|
+
);
|
|
248
|
+
const sensitive = useMemo2(() => createStepUp(client, ask), [client, ask]);
|
|
249
|
+
const finish = (value) => {
|
|
250
|
+
setOpen(false);
|
|
251
|
+
const resolve = pending.current;
|
|
252
|
+
pending.current = null;
|
|
253
|
+
resolve == null ? void 0 : resolve(value);
|
|
254
|
+
};
|
|
255
|
+
const submit = (e) => {
|
|
256
|
+
e == null ? void 0 : e.preventDefault();
|
|
257
|
+
if (!secret) return;
|
|
258
|
+
finish(totpEnrolled ? { code: secret.trim() } : { password: secret });
|
|
259
|
+
};
|
|
260
|
+
return /* @__PURE__ */ jsxs(Ctx2.Provider, { value: sensitive, children: [
|
|
261
|
+
children,
|
|
262
|
+
/* @__PURE__ */ jsx2(
|
|
263
|
+
Dialog,
|
|
264
|
+
{
|
|
265
|
+
isOpen: open,
|
|
266
|
+
onClose: () => finish(null),
|
|
267
|
+
title: "Confirm it's you",
|
|
268
|
+
size: "sm",
|
|
269
|
+
footer: /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
270
|
+
/* @__PURE__ */ jsx2(Button, { variant: "outline", onClick: () => finish(null), children: "Cancel" }),
|
|
271
|
+
/* @__PURE__ */ jsx2(Button, { onClick: submit, disabled: !secret, children: "Confirm" })
|
|
272
|
+
] }),
|
|
273
|
+
children: /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "space-y-3", children: [
|
|
274
|
+
/* @__PURE__ */ jsx2("p", { className: "text-sm text-muted", children: "This action changes live platform access. You won't be asked again for 5 minutes." }),
|
|
275
|
+
/* @__PURE__ */ jsx2(
|
|
276
|
+
Input,
|
|
277
|
+
{
|
|
278
|
+
label: totpEnrolled ? "Authenticator code" : "Your password",
|
|
279
|
+
type: totpEnrolled ? "text" : "password",
|
|
280
|
+
otp: totpEnrolled,
|
|
281
|
+
autoFocus: true,
|
|
282
|
+
autoComplete: totpEnrolled ? "one-time-code" : "current-password",
|
|
283
|
+
value: secret,
|
|
284
|
+
error: error != null ? error : void 0,
|
|
285
|
+
onChange: (e) => setSecret(e.target.value)
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
] })
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
] });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/platform/PlatformShell.jsx
|
|
295
|
+
import React3 from "react";
|
|
296
|
+
import { AppShell } from "@dreamtree-org/twreact-ui";
|
|
297
|
+
|
|
298
|
+
// src/platform/icons.jsx
|
|
299
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
300
|
+
var base = {
|
|
301
|
+
viewBox: "0 0 24 24",
|
|
302
|
+
fill: "none",
|
|
303
|
+
stroke: "currentColor",
|
|
304
|
+
strokeWidth: 1.8,
|
|
305
|
+
strokeLinecap: "round",
|
|
306
|
+
strokeLinejoin: "round",
|
|
307
|
+
"aria-hidden": "true",
|
|
308
|
+
focusable: "false"
|
|
309
|
+
};
|
|
310
|
+
function make(paths) {
|
|
311
|
+
return function Icon({ size = 16, className }) {
|
|
312
|
+
return /* @__PURE__ */ jsx3("svg", { width: size, height: size, className, ...base, children: paths });
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
var IconDashboard = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
316
|
+
/* @__PURE__ */ jsx3("rect", { x: "3", y: "3", width: "7", height: "9", rx: "1" }),
|
|
317
|
+
/* @__PURE__ */ jsx3("rect", { x: "14", y: "3", width: "7", height: "5", rx: "1" }),
|
|
318
|
+
/* @__PURE__ */ jsx3("rect", { x: "14", y: "12", width: "7", height: "9", rx: "1" }),
|
|
319
|
+
/* @__PURE__ */ jsx3("rect", { x: "3", y: "16", width: "7", height: "5", rx: "1" })
|
|
320
|
+
] }));
|
|
321
|
+
var IconBuilding = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
322
|
+
/* @__PURE__ */ jsx3("rect", { x: "4", y: "2", width: "16", height: "20", rx: "2" }),
|
|
323
|
+
/* @__PURE__ */ jsx3("path", { d: "M9 22v-4h6v4M8 6h.01M16 6h.01M12 6h.01M12 10h.01M12 14h.01M16 10h.01M16 14h.01M8 10h.01M8 14h.01" })
|
|
324
|
+
] }));
|
|
325
|
+
var IconShield = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
326
|
+
/* @__PURE__ */ jsx3("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z" }),
|
|
327
|
+
/* @__PURE__ */ jsx3("path", { d: "m9 12 2 2 4-4" })
|
|
328
|
+
] }));
|
|
329
|
+
var IconClock = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
330
|
+
/* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "9" }),
|
|
331
|
+
/* @__PURE__ */ jsx3("path", { d: "M12 7v5l3 2" })
|
|
332
|
+
] }));
|
|
333
|
+
var IconList = make(/* @__PURE__ */ jsx3("path", { d: "M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" }));
|
|
334
|
+
var IconLogout = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
335
|
+
/* @__PURE__ */ jsx3("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
|
|
336
|
+
/* @__PURE__ */ jsx3("path", { d: "m16 17 5-5-5-5M21 12H9" })
|
|
337
|
+
] }));
|
|
338
|
+
var IconPlus = make(/* @__PURE__ */ jsx3("path", { d: "M12 5v14M5 12h14" }));
|
|
339
|
+
var IconCopy = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
340
|
+
/* @__PURE__ */ jsx3("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2" }),
|
|
341
|
+
/* @__PURE__ */ jsx3("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
|
|
342
|
+
] }));
|
|
343
|
+
var IconCheck = make(/* @__PURE__ */ jsx3("path", { d: "M20 6 9 17l-5-5" }));
|
|
344
|
+
var IconX = make(/* @__PURE__ */ jsx3("path", { d: "M18 6 6 18M6 6l12 12" }));
|
|
345
|
+
var IconKey = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
346
|
+
/* @__PURE__ */ jsx3("circle", { cx: "7.5", cy: "15.5", r: "5.5" }),
|
|
347
|
+
/* @__PURE__ */ jsx3("path", { d: "m21 2-9.6 9.6M15.5 7.5l3 3L22 7l-3-3" })
|
|
348
|
+
] }));
|
|
349
|
+
var IconDatabase = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
350
|
+
/* @__PURE__ */ jsx3("ellipse", { cx: "12", cy: "5", rx: "9", ry: "3" }),
|
|
351
|
+
/* @__PURE__ */ jsx3("path", { d: "M3 5v14a9 3 0 0 0 18 0V5M3 12a9 3 0 0 0 18 0" })
|
|
352
|
+
] }));
|
|
353
|
+
var IconServer = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
354
|
+
/* @__PURE__ */ jsx3("rect", { x: "2", y: "2", width: "20", height: "8", rx: "2" }),
|
|
355
|
+
/* @__PURE__ */ jsx3("rect", { x: "2", y: "14", width: "20", height: "8", rx: "2" }),
|
|
356
|
+
/* @__PURE__ */ jsx3("path", { d: "M6 6h.01M6 18h.01" })
|
|
357
|
+
] }));
|
|
358
|
+
var IconLink = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
359
|
+
/* @__PURE__ */ jsx3("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }),
|
|
360
|
+
/* @__PURE__ */ jsx3("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
|
|
361
|
+
] }));
|
|
362
|
+
var IconUsers = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
363
|
+
/* @__PURE__ */ jsx3("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }),
|
|
364
|
+
/* @__PURE__ */ jsx3("circle", { cx: "9", cy: "7", r: "4" }),
|
|
365
|
+
/* @__PURE__ */ jsx3("path", { d: "M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" })
|
|
366
|
+
] }));
|
|
367
|
+
var IconBranch = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
368
|
+
/* @__PURE__ */ jsx3("path", { d: "M6 3v12" }),
|
|
369
|
+
/* @__PURE__ */ jsx3("circle", { cx: "18", cy: "6", r: "3" }),
|
|
370
|
+
/* @__PURE__ */ jsx3("circle", { cx: "6", cy: "18", r: "3" }),
|
|
371
|
+
/* @__PURE__ */ jsx3("path", { d: "M18 9a9 9 0 0 1-9 9" })
|
|
372
|
+
] }));
|
|
373
|
+
var IconActivity = make(/* @__PURE__ */ jsx3("path", { d: "M22 12h-4l-3 9L9 3l-3 9H2" }));
|
|
374
|
+
var IconRefresh = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
375
|
+
/* @__PURE__ */ jsx3("path", { d: "M3 12a9 9 0 0 1 15-6.7L21 8" }),
|
|
376
|
+
/* @__PURE__ */ jsx3("path", { d: "M21 3v5h-5" }),
|
|
377
|
+
/* @__PURE__ */ jsx3("path", { d: "M21 12a9 9 0 0 1-15 6.7L3 16" }),
|
|
378
|
+
/* @__PURE__ */ jsx3("path", { d: "M8 16H3v5" })
|
|
379
|
+
] }));
|
|
380
|
+
var IconAlert = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
381
|
+
/* @__PURE__ */ jsx3("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" }),
|
|
382
|
+
/* @__PURE__ */ jsx3("path", { d: "M12 9v4M12 17h.01" })
|
|
383
|
+
] }));
|
|
384
|
+
var IconChevron = make(/* @__PURE__ */ jsx3("path", { d: "m9 6 6 6-6 6" }));
|
|
385
|
+
var IconEye = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
386
|
+
/* @__PURE__ */ jsx3("path", { d: "M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" }),
|
|
387
|
+
/* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "3" })
|
|
388
|
+
] }));
|
|
389
|
+
var IconEyeOff = make(/* @__PURE__ */ jsx3(Fragment, { children: /* @__PURE__ */ jsx3("path", { d: "M9.88 9.88a3 3 0 1 0 4.24 4.24M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68M6.61 6.61A13.53 13.53 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61M2 2l20 20" }) }));
|
|
390
|
+
var IconPause = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
391
|
+
/* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "10" }),
|
|
392
|
+
/* @__PURE__ */ jsx3("path", { d: "M10 15V9M14 15V9" })
|
|
393
|
+
] }));
|
|
394
|
+
var IconPlay = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
395
|
+
/* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "10" }),
|
|
396
|
+
/* @__PURE__ */ jsx3("path", { d: "m10 8 6 4-6 4V8z" })
|
|
397
|
+
] }));
|
|
398
|
+
var IconArchive = make(/* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
399
|
+
/* @__PURE__ */ jsx3("rect", { x: "2", y: "3", width: "20", height: "5", rx: "1" }),
|
|
400
|
+
/* @__PURE__ */ jsx3("path", { d: "M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8M10 12h4" })
|
|
401
|
+
] }));
|
|
402
|
+
var IconDownload = make(/* @__PURE__ */ jsx3("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" }));
|
|
403
|
+
|
|
404
|
+
// src/platform/PlatformShell.jsx
|
|
405
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
406
|
+
function ShellBrand({ brand, appName }) {
|
|
407
|
+
var _a, _b, _c, _d;
|
|
408
|
+
const name = (_b = (_a = brand == null ? void 0 : brand.name) != null ? _a : appName) != null ? _b : "Dreamtree";
|
|
409
|
+
return /* @__PURE__ */ jsxs3("span", { className: "flex min-w-0 items-center gap-2", children: [
|
|
410
|
+
(brand == null ? void 0 : brand.logoSrc) ? /* @__PURE__ */ jsx4("img", { src: brand.logoSrc, alt: "", width: 28, height: 28, className: "h-7 w-7 rounded-md" }) : /* @__PURE__ */ jsx4(
|
|
411
|
+
"span",
|
|
412
|
+
{
|
|
413
|
+
className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-sm font-bold text-white",
|
|
414
|
+
style: { background: (_d = (_c = brand == null ? void 0 : brand.accent) != null ? _c : brand == null ? void 0 : brand.primary) != null ? _d : "currentColor" },
|
|
415
|
+
"aria-hidden": "true",
|
|
416
|
+
children: name.slice(0, 1)
|
|
417
|
+
}
|
|
418
|
+
),
|
|
419
|
+
/* @__PURE__ */ jsxs3("span", { className: "min-w-0 leading-tight", children: [
|
|
420
|
+
/* @__PURE__ */ jsx4("span", { className: "block truncate text-sm font-semibold", children: name }),
|
|
421
|
+
/* @__PURE__ */ jsx4("span", { className: "block truncate text-xs opacity-70", children: "Platform console" })
|
|
422
|
+
] })
|
|
423
|
+
] });
|
|
424
|
+
}
|
|
425
|
+
function PlatformShell({ brand, appName, nav, adminEmail, onSignOut, linkAs, pageTitle, children }) {
|
|
426
|
+
return /* @__PURE__ */ jsx4(
|
|
427
|
+
AppShell,
|
|
428
|
+
{
|
|
429
|
+
brand: /* @__PURE__ */ jsx4(ShellBrand, { brand, appName }),
|
|
430
|
+
nav,
|
|
431
|
+
linkAs,
|
|
432
|
+
pageTitle,
|
|
433
|
+
sidebarFooter: /* @__PURE__ */ jsxs3("div", { className: "space-y-2 text-shell-foreground", children: [
|
|
434
|
+
adminEmail && /* @__PURE__ */ jsx4("div", { className: "truncate px-2 text-xs opacity-80", title: adminEmail, children: adminEmail }),
|
|
435
|
+
/* @__PURE__ */ jsxs3(
|
|
436
|
+
"button",
|
|
437
|
+
{
|
|
438
|
+
type: "button",
|
|
439
|
+
onClick: onSignOut,
|
|
440
|
+
className: "flex w-full items-center gap-2 rounded-md px-2 py-2 text-sm hover:bg-shell-active focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500",
|
|
441
|
+
children: [
|
|
442
|
+
/* @__PURE__ */ jsx4(IconLogout, {}),
|
|
443
|
+
" Sign out"
|
|
444
|
+
]
|
|
445
|
+
}
|
|
446
|
+
)
|
|
447
|
+
] }),
|
|
448
|
+
children
|
|
449
|
+
}
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/platform/views/Overview.jsx
|
|
454
|
+
import React5 from "react";
|
|
455
|
+
import { Alert, Button as Button2, ErrorState, LoadingState, PageHeader, StatCard } from "@dreamtree-org/twreact-ui";
|
|
456
|
+
|
|
457
|
+
// src/platform/ui.jsx
|
|
458
|
+
import React4, { useState as useState3 } from "react";
|
|
459
|
+
import { StatusPill as TwStatusPill, Badge } from "@dreamtree-org/twreact-ui";
|
|
460
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
461
|
+
var STATUS_MAP = {
|
|
462
|
+
active: { label: "Active", tone: "success" },
|
|
463
|
+
draft: { label: "Draft", tone: "neutral" },
|
|
464
|
+
provisioning: { label: "Provisioning", tone: "info" },
|
|
465
|
+
provisioning_failed: { label: "Setup failed", tone: "danger" },
|
|
466
|
+
maintenance: { label: "Maintenance", tone: "warning" },
|
|
467
|
+
suspended: { label: "Suspended", tone: "warning" },
|
|
468
|
+
archived: { label: "Archived", tone: "muted" },
|
|
469
|
+
disabled: { label: "Disabled", tone: "muted" },
|
|
470
|
+
inactive: { label: "Inactive", tone: "muted" },
|
|
471
|
+
succeeded: { label: "Succeeded", tone: "success" },
|
|
472
|
+
failed: { label: "Failed", tone: "danger" },
|
|
473
|
+
running: { label: "Running", tone: "info" },
|
|
474
|
+
pending: { label: "Pending", tone: "neutral" },
|
|
475
|
+
queued: { label: "Queued", tone: "neutral" },
|
|
476
|
+
cancelled: { label: "Cancelled", tone: "muted" }
|
|
477
|
+
};
|
|
478
|
+
function StatusPill({ status }) {
|
|
479
|
+
return /* @__PURE__ */ jsx5(TwStatusPill, { status: String(status != null ? status : ""), map: STATUS_MAP });
|
|
480
|
+
}
|
|
481
|
+
function ModePill({ mode }) {
|
|
482
|
+
if (!mode) return /* @__PURE__ */ jsx5("span", { className: "text-xs text-muted", children: "No database" });
|
|
483
|
+
return mode === "external" ? /* @__PURE__ */ jsx5(Badge, { variant: "secondary", size: "sm", children: "Own database" }) : /* @__PURE__ */ jsx5(Badge, { variant: "info", size: "sm", children: "Managed" });
|
|
484
|
+
}
|
|
485
|
+
function HealthDot({ ok }) {
|
|
486
|
+
if (ok === null || ok === void 0) return /* @__PURE__ */ jsx5("span", { className: "text-xs text-muted", children: "Not checked" });
|
|
487
|
+
return ok ? /* @__PURE__ */ jsxs4("span", { className: "inline-flex items-center gap-1 text-xs text-success-600", children: [
|
|
488
|
+
/* @__PURE__ */ jsx5(IconCheck, { size: 14 }),
|
|
489
|
+
" Healthy"
|
|
490
|
+
] }) : /* @__PURE__ */ jsxs4("span", { className: "inline-flex items-center gap-1 text-xs text-error-600", children: [
|
|
491
|
+
/* @__PURE__ */ jsx5(IconX, { size: 14 }),
|
|
492
|
+
" Failing"
|
|
493
|
+
] });
|
|
494
|
+
}
|
|
495
|
+
function Card({ title, actions, children }) {
|
|
496
|
+
return /* @__PURE__ */ jsxs4("section", { className: "rounded-xl border border-line bg-surface", children: [
|
|
497
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between gap-2 border-b border-line px-4 py-3", children: [
|
|
498
|
+
/* @__PURE__ */ jsx5("h2", { className: "text-sm font-medium text-foreground", children: title }),
|
|
499
|
+
actions
|
|
500
|
+
] }),
|
|
501
|
+
/* @__PURE__ */ jsx5("div", { className: "p-4", children })
|
|
502
|
+
] });
|
|
503
|
+
}
|
|
504
|
+
function Facts({ rows }) {
|
|
505
|
+
return /* @__PURE__ */ jsx5("dl", { className: "grid gap-x-6 gap-y-3 [grid-template-columns:repeat(auto-fit,minmax(14rem,1fr))]", children: rows.map(([k, v]) => /* @__PURE__ */ jsxs4("div", { className: "min-w-0", children: [
|
|
506
|
+
/* @__PURE__ */ jsx5("dt", { className: "text-xs text-muted", children: k }),
|
|
507
|
+
/* @__PURE__ */ jsx5("dd", { className: "mt-0.5 break-words text-sm text-foreground", children: v })
|
|
508
|
+
] }, k)) });
|
|
509
|
+
}
|
|
510
|
+
function CopyButton({ value, label = "Copy" }) {
|
|
511
|
+
const [copied, setCopied] = useState3(false);
|
|
512
|
+
return /* @__PURE__ */ jsxs4(
|
|
513
|
+
"button",
|
|
514
|
+
{
|
|
515
|
+
type: "button",
|
|
516
|
+
onClick: async () => {
|
|
517
|
+
try {
|
|
518
|
+
await navigator.clipboard.writeText(value);
|
|
519
|
+
} catch {
|
|
520
|
+
}
|
|
521
|
+
setCopied(true);
|
|
522
|
+
setTimeout(() => setCopied(false), 1500);
|
|
523
|
+
},
|
|
524
|
+
className: "inline-flex shrink-0 items-center gap-1 rounded-md border border-line bg-surface px-2 py-1 text-xs font-medium text-foreground hover:bg-background",
|
|
525
|
+
"aria-label": `${label}: ${String(value).length > 40 ? "value" : value}`,
|
|
526
|
+
children: [
|
|
527
|
+
copied ? /* @__PURE__ */ jsx5(IconCheck, { size: 12 }) : /* @__PURE__ */ jsx5(IconCopy, { size: 12 }),
|
|
528
|
+
copied ? "Copied" : label
|
|
529
|
+
]
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
function OneTimeSecret({ title = "Sign-in details", loginUrl, email, password, onDone }) {
|
|
534
|
+
const rows = [...loginUrl ? [["Sign-in page", loginUrl, false]] : [], ["Email", email, false], ["Temporary password", password, true]];
|
|
535
|
+
return /* @__PURE__ */ jsxs4("div", { className: "rounded-xl border border-warning-200 bg-warning-50 p-4", role: "status", children: [
|
|
536
|
+
/* @__PURE__ */ jsxs4("div", { className: "mb-3 flex items-center gap-2 text-sm font-medium text-warning-800", children: [
|
|
537
|
+
/* @__PURE__ */ jsx5(IconKey, {}),
|
|
538
|
+
" ",
|
|
539
|
+
title
|
|
540
|
+
] }),
|
|
541
|
+
/* @__PURE__ */ jsx5("dl", { className: "space-y-2", children: rows.map(([k, v, mono]) => /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center justify-between gap-2 rounded-lg bg-surface px-3 py-2", children: [
|
|
542
|
+
/* @__PURE__ */ jsx5("dt", { className: "text-xs text-muted", children: k }),
|
|
543
|
+
/* @__PURE__ */ jsxs4("dd", { className: "flex min-w-0 items-center gap-2", children: [
|
|
544
|
+
/* @__PURE__ */ jsx5("span", { className: `truncate text-sm text-foreground ${mono ? "font-mono" : ""}`, children: v }),
|
|
545
|
+
/* @__PURE__ */ jsx5(CopyButton, { value: v })
|
|
546
|
+
] })
|
|
547
|
+
] }, k)) }),
|
|
548
|
+
/* @__PURE__ */ jsxs4("p", { className: "mt-3 flex items-start gap-1.5 text-xs text-warning-800", children: [
|
|
549
|
+
/* @__PURE__ */ jsx5(IconAlert, { size: 14, className: "mt-0.5 shrink-0" }),
|
|
550
|
+
"Shown only once and never stored. Share it securely; the admin should change it after signing in."
|
|
551
|
+
] }),
|
|
552
|
+
onDone && /* @__PURE__ */ jsx5("div", { className: "mt-3 text-right", children: /* @__PURE__ */ jsx5("button", { type: "button", onClick: onDone, className: "rounded-md border border-line bg-surface px-3 py-1.5 text-xs font-medium text-foreground hover:bg-background", children: "Done, I've saved it" }) })
|
|
553
|
+
] });
|
|
554
|
+
}
|
|
555
|
+
function InspectResult({ report }) {
|
|
556
|
+
var _a, _b, _c, _d, _e, _f;
|
|
557
|
+
const stateText = {
|
|
558
|
+
empty: "Empty database: the full schema will be created",
|
|
559
|
+
company: "Existing database of this company: it will be re-attached and synced",
|
|
560
|
+
"other-company": "Belongs to a different company",
|
|
561
|
+
foreign: `Contains ${(_a = report.tableCount) != null ? _a : "other"} unrelated table(s)`
|
|
562
|
+
};
|
|
563
|
+
const missing = (_b = report.missingPrivileges) != null ? _b : [];
|
|
564
|
+
const foreign = (_c = report.foreignTables) != null ? _c : [];
|
|
565
|
+
const items = [
|
|
566
|
+
{
|
|
567
|
+
ok: report.reachable,
|
|
568
|
+
label: report.reachable ? `Connected to ${(_d = report.server) != null ? _d : "server"}` : "Could not connect",
|
|
569
|
+
detail: report.reachable ? report.target : report.error
|
|
570
|
+
}
|
|
571
|
+
];
|
|
572
|
+
if (report.reachable && report.state) {
|
|
573
|
+
items.push({
|
|
574
|
+
ok: missing.length === 0,
|
|
575
|
+
label: missing.length === 0 ? "Privileges to build and maintain the schema" : "Missing privileges",
|
|
576
|
+
detail: missing.length ? missing.join(", ") : void 0
|
|
577
|
+
});
|
|
578
|
+
items.push({
|
|
579
|
+
ok: report.state === "empty" || report.state === "company",
|
|
580
|
+
label: (_e = stateText[report.state]) != null ? _e : report.state,
|
|
581
|
+
detail: foreign.length ? `Tables: ${foreign.join(", ")}${((_f = report.tableCount) != null ? _f : 0) > 10 ? ", \u2026" : ""}` : void 0
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
return /* @__PURE__ */ jsxs4("div", { className: `mt-3 rounded-xl border p-3 ${report.ok ? "border-success-200 bg-success-50" : "border-error-200 bg-error-50"}`, children: [
|
|
585
|
+
/* @__PURE__ */ jsx5("ul", { className: "space-y-2", children: items.map((it) => /* @__PURE__ */ jsxs4("li", { className: "flex gap-2", children: [
|
|
586
|
+
/* @__PURE__ */ jsx5("span", { className: `mt-0.5 shrink-0 ${it.ok ? "text-success-600" : "text-error-600"}`, children: it.ok ? /* @__PURE__ */ jsx5(IconCheck, {}) : /* @__PURE__ */ jsx5(IconX, {}) }),
|
|
587
|
+
/* @__PURE__ */ jsxs4("div", { className: "min-w-0", children: [
|
|
588
|
+
/* @__PURE__ */ jsx5("div", { className: "text-xs font-medium text-foreground", children: it.label }),
|
|
589
|
+
it.detail && /* @__PURE__ */ jsx5("div", { className: "break-words text-xs text-muted", children: it.detail })
|
|
590
|
+
] })
|
|
591
|
+
] }, it.label)) }),
|
|
592
|
+
!report.ok && report.hint && /* @__PURE__ */ jsx5("p", { className: "mt-2 border-t border-error-200 pt-2 text-xs text-error-700", children: report.hint })
|
|
593
|
+
] });
|
|
594
|
+
}
|
|
595
|
+
function jobErrorHint(error) {
|
|
596
|
+
if (!error) return null;
|
|
597
|
+
if (/Access denied for user/i.test(error)) {
|
|
598
|
+
return "The provisioner database login was rejected. Check PROVISIONER_DB_URL on the server and that the MySQL user is allowed from the app server's IP, then retry.";
|
|
599
|
+
}
|
|
600
|
+
if (/ETIMEDOUT|ECONNREFUSED|ENOTFOUND/.test(error)) return "The database server could not be reached. Check the host, port and firewall, then retry.";
|
|
601
|
+
if (/GRANT|command denied/i.test(error)) return "The provisioner is missing privileges (it needs CREATE USER and GRANT OPTION).";
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
var ACTION_LABEL = {
|
|
605
|
+
"company.create": "created company",
|
|
606
|
+
"company.provision": "ran provisioning",
|
|
607
|
+
"company.update": "updated company details",
|
|
608
|
+
"company.status.active": "activated company",
|
|
609
|
+
"company.status.suspended": "suspended company",
|
|
610
|
+
"company.status.archived": "archived company",
|
|
611
|
+
"company.admin.create": "added company admin",
|
|
612
|
+
"company.admin.reset_password": "reset company admin password",
|
|
613
|
+
"company.admin.disable": "disabled company admin",
|
|
614
|
+
"company.admin.enable": "enabled company admin",
|
|
615
|
+
"branch.create": "added branch",
|
|
616
|
+
"branch.update": "updated branch",
|
|
617
|
+
"database.test": "tested database",
|
|
618
|
+
"database.sync": "synced database schema",
|
|
619
|
+
"database.replace": "replaced database URL",
|
|
620
|
+
"database.credentials.reveal": "revealed database URL",
|
|
621
|
+
"provisioning.retry": "retried provisioning",
|
|
622
|
+
"platform.login": "signed in",
|
|
623
|
+
"platform.login.failed": "failed to sign in",
|
|
624
|
+
"platform.stepup": "confirmed identity",
|
|
625
|
+
"platform.stepup.failed": "failed an identity confirmation",
|
|
626
|
+
"platform.admin.create": "added platform admin",
|
|
627
|
+
"platform.admin.reset_password": "reset platform admin password",
|
|
628
|
+
"platform.admin.reset_totp": "reset an authenticator",
|
|
629
|
+
"platform.admin.totp_enrolled": "set up an authenticator",
|
|
630
|
+
"platform.admin.roles": "changed platform admin roles",
|
|
631
|
+
"platform.admin.disable": "disabled platform admin",
|
|
632
|
+
"platform.admin.enable": "enabled platform admin",
|
|
633
|
+
"audit.export": "exported the audit log"
|
|
634
|
+
};
|
|
635
|
+
function describeAction(action) {
|
|
636
|
+
var _a;
|
|
637
|
+
return (_a = ACTION_LABEL[action]) != null ? _a : String(action != null ? action : "").replace(/[._]/g, " ");
|
|
638
|
+
}
|
|
639
|
+
function normalizeEvent(e) {
|
|
640
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
641
|
+
return {
|
|
642
|
+
id: e.id,
|
|
643
|
+
at: (_a = e.at) != null ? _a : e.occurredAt,
|
|
644
|
+
actor: (_d = (_c = (_b = e.actorEmail) != null ? _b : e.actor) != null ? _c : e.actorRef) != null ? _d : "system",
|
|
645
|
+
action: e.action,
|
|
646
|
+
companyId: (_e = e.companyId) != null ? _e : null,
|
|
647
|
+
company: (_g = (_f = e.companyName) != null ? _f : e.company) != null ? _g : null,
|
|
648
|
+
target: (_h = e.target) != null ? _h : null
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
var EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
652
|
+
function slugify(s) {
|
|
653
|
+
return String(s != null ? s : "").toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/platform/views/Overview.jsx
|
|
657
|
+
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
658
|
+
function attentionReason(c) {
|
|
659
|
+
if (c.status === "provisioning_failed") return "Database setup failed: open to retry";
|
|
660
|
+
if (c.status === "draft") return "Database not provisioned yet";
|
|
661
|
+
if (c.status === "provisioning") return "Provisioning in progress";
|
|
662
|
+
if (Number(c.lastCheckOk) === 0) return `Database check failing${c.lastCheckError ? `: ${c.lastCheckError}` : ""}`;
|
|
663
|
+
return "Needs review";
|
|
664
|
+
}
|
|
665
|
+
function OverviewView() {
|
|
666
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
667
|
+
const { Link, href, can } = usePlatform();
|
|
668
|
+
const { data, error, loading, reload } = useLoad("overview");
|
|
669
|
+
const c = data == null ? void 0 : data.counts;
|
|
670
|
+
const problems = c ? ((_a = c.failedJobs) != null ? _a : 0) + ((_b = c.dbFailures) != null ? _b : 0) : null;
|
|
671
|
+
return /* @__PURE__ */ jsxs5("div", { className: "mx-auto max-w-7xl space-y-6 px-4 py-6 sm:px-6", children: [
|
|
672
|
+
/* @__PURE__ */ jsx6(
|
|
673
|
+
PageHeader,
|
|
674
|
+
{
|
|
675
|
+
title: "Overview",
|
|
676
|
+
subtitle: "Company health across the platform",
|
|
677
|
+
linkAs: Link,
|
|
678
|
+
actions: can("companies.write") && /* @__PURE__ */ jsx6(Button2, { as: Link, href: href("companies/new"), leftIcon: /* @__PURE__ */ jsx6(IconPlus, {}), children: "New company" })
|
|
679
|
+
}
|
|
680
|
+
),
|
|
681
|
+
error && !data && /* @__PURE__ */ jsx6(ErrorState, { description: error.message, onRetry: reload }),
|
|
682
|
+
data && data.provisioner && !data.provisioner.ok && /* @__PURE__ */ jsxs5(Alert, { variant: "error", title: "Managed databases are unavailable", children: [
|
|
683
|
+
data.provisioner.principal ? `${data.provisioner.principal}: ` : "",
|
|
684
|
+
data.provisioner.error,
|
|
685
|
+
" New companies can still be set up with their own database URL. To restore managed provisioning, give the provisioner MySQL user CREATE USER and GRANT OPTION from the app server's IP and set PROVISIONER_DB_URL on the server."
|
|
686
|
+
] }),
|
|
687
|
+
/* @__PURE__ */ jsxs5("div", { className: "grid gap-4 [grid-template-columns:repeat(auto-fit,minmax(9.5rem,1fr))]", children: [
|
|
688
|
+
/* @__PURE__ */ jsx6(StatCard, { label: "Companies", value: (_c = c == null ? void 0 : c.total) != null ? _c : "\u2014", icon: /* @__PURE__ */ jsx6(IconBuilding, {}), tone: "primary", loading: loading && !data }),
|
|
689
|
+
/* @__PURE__ */ jsx6(StatCard, { label: "Active", value: (_d = c == null ? void 0 : c.active) != null ? _d : "\u2014", icon: /* @__PURE__ */ jsx6(IconCheck, {}), tone: "success", loading: loading && !data }),
|
|
690
|
+
/* @__PURE__ */ jsx6(StatCard, { label: "Being set up", value: (_e = c == null ? void 0 : c.setup) != null ? _e : "\u2014", icon: /* @__PURE__ */ jsx6(IconClock, {}), tone: "warning", loading: loading && !data }),
|
|
691
|
+
/* @__PURE__ */ jsx6(StatCard, { label: "Suspended", value: (_f = c == null ? void 0 : c.suspended) != null ? _f : "\u2014", icon: /* @__PURE__ */ jsx6(IconPause, {}), tone: "secondary", loading: loading && !data }),
|
|
692
|
+
/* @__PURE__ */ jsx6(StatCard, { label: "Problems", value: problems != null ? problems : "\u2014", icon: /* @__PURE__ */ jsx6(IconAlert, {}), tone: problems ? "error" : "secondary", loading: loading && !data })
|
|
693
|
+
] }),
|
|
694
|
+
/* @__PURE__ */ jsxs5("div", { className: "grid gap-6 [grid-template-columns:repeat(auto-fit,minmax(min(22rem,100%),1fr))]", children: [
|
|
695
|
+
/* @__PURE__ */ jsxs5("section", { className: "rounded-xl border border-line bg-surface", "aria-labelledby": "pc-attention", children: [
|
|
696
|
+
/* @__PURE__ */ jsx6("h2", { id: "pc-attention", className: "border-b border-line px-4 py-3 text-sm font-medium text-foreground", children: "Needs attention" }),
|
|
697
|
+
loading && !data ? /* @__PURE__ */ jsx6(LoadingState, { variant: "table", rows: 3 }) : ((_g = data == null ? void 0 : data.attention) == null ? void 0 : _g.length) ? /* @__PURE__ */ jsx6("ul", { className: "divide-y divide-line", children: data.attention.map((co) => /* @__PURE__ */ jsx6("li", { children: /* @__PURE__ */ jsxs5(Link, { href: href(`companies/${co.id}`), className: "flex items-center gap-3 px-4 py-3 hover:bg-background", children: [
|
|
698
|
+
/* @__PURE__ */ jsxs5("div", { className: "min-w-0 flex-1", children: [
|
|
699
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
700
|
+
/* @__PURE__ */ jsx6("span", { className: "truncate text-sm font-medium text-foreground", children: co.displayName }),
|
|
701
|
+
/* @__PURE__ */ jsx6(StatusPill, { status: co.status })
|
|
702
|
+
] }),
|
|
703
|
+
/* @__PURE__ */ jsx6("div", { className: "mt-0.5 truncate text-xs text-muted", children: attentionReason(co) })
|
|
704
|
+
] }),
|
|
705
|
+
/* @__PURE__ */ jsx6(IconChevron, { className: "text-muted" })
|
|
706
|
+
] }) }, co.id)) }) : /* @__PURE__ */ jsx6("p", { className: "px-4 py-8 text-center text-sm text-muted", children: "Everything looks healthy." })
|
|
707
|
+
] }),
|
|
708
|
+
/* @__PURE__ */ jsxs5("section", { className: "rounded-xl border border-line bg-surface", "aria-labelledby": "pc-activity", children: [
|
|
709
|
+
/* @__PURE__ */ jsx6("h2", { id: "pc-activity", className: "border-b border-line px-4 py-3 text-sm font-medium text-foreground", children: "Recent activity" }),
|
|
710
|
+
loading && !data ? /* @__PURE__ */ jsx6(LoadingState, { variant: "table", rows: 4 }) : ((_h = data == null ? void 0 : data.recent) == null ? void 0 : _h.length) ? /* @__PURE__ */ jsx6("ol", { className: "divide-y divide-line", children: data.recent.map(normalizeEvent).map((e, i) => {
|
|
711
|
+
var _a2;
|
|
712
|
+
return /* @__PURE__ */ jsxs5("li", { className: "px-4 py-2.5", children: [
|
|
713
|
+
/* @__PURE__ */ jsxs5("div", { className: "text-sm text-foreground", children: [
|
|
714
|
+
/* @__PURE__ */ jsx6("span", { className: "font-medium", children: e.actor }),
|
|
715
|
+
" ",
|
|
716
|
+
describeAction(e.action),
|
|
717
|
+
e.company && e.companyId && /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
718
|
+
" \xB7 ",
|
|
719
|
+
/* @__PURE__ */ jsx6(Link, { href: href(`companies/${e.companyId}`), className: "text-primary-600 hover:underline", children: e.company })
|
|
720
|
+
] }),
|
|
721
|
+
e.target && !e.company && /* @__PURE__ */ jsxs5("span", { className: "text-muted", children: [
|
|
722
|
+
" \xB7 ",
|
|
723
|
+
e.target
|
|
724
|
+
] })
|
|
725
|
+
] }),
|
|
726
|
+
/* @__PURE__ */ jsx6("div", { className: "text-xs text-muted", children: timeAgo(e.at) })
|
|
727
|
+
] }, (_a2 = e.id) != null ? _a2 : i);
|
|
728
|
+
}) }) : /* @__PURE__ */ jsx6("p", { className: "px-4 py-8 text-center text-sm text-muted", children: "No activity yet." })
|
|
729
|
+
] })
|
|
730
|
+
] })
|
|
731
|
+
] });
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/platform/views/Companies.jsx
|
|
735
|
+
import React6, { useState as useState4 } from "react";
|
|
736
|
+
import { Button as Button3, EmptyState, ErrorState as ErrorState2, LoadingState as LoadingState2, PageHeader as PageHeader2, SegmentedControl, Table } from "@dreamtree-org/twreact-ui";
|
|
737
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
738
|
+
var FILTERS = [
|
|
739
|
+
{ value: "", label: "All" },
|
|
740
|
+
{ value: "active", label: "Active" },
|
|
741
|
+
{ value: "provisioning_failed", label: "Setup failed" },
|
|
742
|
+
{ value: "draft", label: "Draft" },
|
|
743
|
+
{ value: "suspended", label: "Suspended" },
|
|
744
|
+
{ value: "archived", label: "Archived" }
|
|
745
|
+
];
|
|
746
|
+
function CompaniesView() {
|
|
747
|
+
var _a;
|
|
748
|
+
const { Link, href, navigate, can, companyLoginPath } = usePlatform();
|
|
749
|
+
const [status, setStatus] = useState4("");
|
|
750
|
+
const { data, error, loading, reload } = useLoad("companies", { status });
|
|
751
|
+
const rows = (_a = data == null ? void 0 : data.companies) != null ? _a : [];
|
|
752
|
+
const columns = [
|
|
753
|
+
{
|
|
754
|
+
key: "displayName",
|
|
755
|
+
label: "Company",
|
|
756
|
+
sortable: true,
|
|
757
|
+
render: (r) => /* @__PURE__ */ jsxs6("div", { children: [
|
|
758
|
+
/* @__PURE__ */ jsx7("div", { className: "text-sm font-medium text-foreground", children: r.displayName }),
|
|
759
|
+
/* @__PURE__ */ jsx7("div", { className: "font-mono text-xs text-muted", children: companyLoginPath(r.slug) })
|
|
760
|
+
] })
|
|
761
|
+
},
|
|
762
|
+
{ key: "status", label: "Status", render: (r) => /* @__PURE__ */ jsx7(StatusPill, { status: r.status }), searchable: false },
|
|
763
|
+
{ key: "dbMode", label: "Database", render: (r) => /* @__PURE__ */ jsx7(ModePill, { mode: r.dbStatus === "active" || r.dbMode === "external" ? r.dbMode : null }), searchable: false },
|
|
764
|
+
{
|
|
765
|
+
key: "lastCheckOk",
|
|
766
|
+
label: "Health",
|
|
767
|
+
searchable: false,
|
|
768
|
+
hideOnMobile: true,
|
|
769
|
+
render: (r) => /* @__PURE__ */ jsxs6("div", { children: [
|
|
770
|
+
/* @__PURE__ */ jsx7(HealthDot, { ok: r.lastCheckOk === null || r.lastCheckOk === void 0 ? null : Number(r.lastCheckOk) === 1 }),
|
|
771
|
+
r.lastCheckAt && /* @__PURE__ */ jsx7("div", { className: "text-xs text-muted", children: timeAgo(r.lastCheckAt) })
|
|
772
|
+
] })
|
|
773
|
+
},
|
|
774
|
+
{ key: "branchCount", label: "Branches", sortable: true, searchable: false, hideOnMobile: true },
|
|
775
|
+
{ key: "createdAt", label: "Created", sortable: true, searchable: false, hideOnMobile: true, render: (r) => /* @__PURE__ */ jsx7("span", { className: "text-xs text-muted", children: timeAgo(r.createdAt) }) }
|
|
776
|
+
];
|
|
777
|
+
const newCompany = can("companies.write") && /* @__PURE__ */ jsx7(Button3, { as: Link, href: href("companies/new"), leftIcon: /* @__PURE__ */ jsx7(IconPlus, {}), children: "New company" });
|
|
778
|
+
return /* @__PURE__ */ jsxs6("div", { className: "mx-auto max-w-7xl space-y-6 px-4 py-6 sm:px-6", children: [
|
|
779
|
+
/* @__PURE__ */ jsx7(PageHeader2, { title: "Companies", subtitle: "Every company on the platform, its database and health", actions: newCompany, linkAs: Link }),
|
|
780
|
+
/* @__PURE__ */ jsx7("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsx7(
|
|
781
|
+
SegmentedControl,
|
|
782
|
+
{
|
|
783
|
+
"aria-label": "Filter by status",
|
|
784
|
+
options: FILTERS.map((f) => ({ value: f.value, label: f.label })),
|
|
785
|
+
value: status,
|
|
786
|
+
onChange: (v) => setStatus(String(v))
|
|
787
|
+
}
|
|
788
|
+
) }),
|
|
789
|
+
error && !data ? /* @__PURE__ */ jsx7(ErrorState2, { description: error.message, onRetry: reload }) : loading && !data ? /* @__PURE__ */ jsx7(LoadingState2, { variant: "table", rows: 6 }) : rows.length === 0 ? /* @__PURE__ */ jsx7(
|
|
790
|
+
EmptyState,
|
|
791
|
+
{
|
|
792
|
+
icon: /* @__PURE__ */ jsx7(IconBuilding, { size: 24 }),
|
|
793
|
+
title: status ? "No companies with this status" : "No companies yet",
|
|
794
|
+
description: status ? "Try another filter." : "Set up the first company to get started.",
|
|
795
|
+
action: !status && newCompany
|
|
796
|
+
}
|
|
797
|
+
) : /* @__PURE__ */ jsx7("div", { className: "rounded-xl border border-line bg-surface p-2", children: /* @__PURE__ */ jsx7(
|
|
798
|
+
Table,
|
|
799
|
+
{
|
|
800
|
+
data: rows,
|
|
801
|
+
columns,
|
|
802
|
+
globalSearch: true,
|
|
803
|
+
showReloadButton: true,
|
|
804
|
+
onReload: reload,
|
|
805
|
+
pagination: rows.length > 25,
|
|
806
|
+
pageSize: 25,
|
|
807
|
+
showColumnPicker: false,
|
|
808
|
+
withAction: false,
|
|
809
|
+
showSerial: false,
|
|
810
|
+
onRowClick: (r) => navigate(href(`companies/${r.id}`))
|
|
811
|
+
}
|
|
812
|
+
) })
|
|
813
|
+
] });
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// src/platform/views/NewCompany.jsx
|
|
817
|
+
import React8, { useEffect as useEffect3, useRef as useRef2, useState as useState6 } from "react";
|
|
818
|
+
import { Alert as Alert2, Button as Button5, Input as Input3, PageHeader as PageHeader3, Stepper } from "@dreamtree-org/twreact-ui";
|
|
819
|
+
|
|
820
|
+
// src/platform/views/DatabaseChoice.jsx
|
|
821
|
+
import React7, { useEffect as useEffect2, useState as useState5 } from "react";
|
|
822
|
+
import { Button as Button4, Input as Input2 } from "@dreamtree-org/twreact-ui";
|
|
823
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
824
|
+
function useProvisioner() {
|
|
825
|
+
const { client } = usePlatform();
|
|
826
|
+
const [p, setP] = useState5(null);
|
|
827
|
+
useEffect2(() => {
|
|
828
|
+
let live = true;
|
|
829
|
+
client.get("overview").then((o) => {
|
|
830
|
+
var _a;
|
|
831
|
+
return live && setP((_a = o.provisioner) != null ? _a : { ok: true });
|
|
832
|
+
}).catch(() => live && setP({ ok: false, error: "Could not check the provisioner" }));
|
|
833
|
+
return () => {
|
|
834
|
+
live = false;
|
|
835
|
+
};
|
|
836
|
+
}, [client]);
|
|
837
|
+
return p;
|
|
838
|
+
}
|
|
839
|
+
function DatabaseChoice({ value, onChange, provisioner, companyId, acceptStates = ["empty"], storedTarget, compact = false }) {
|
|
840
|
+
var _a;
|
|
841
|
+
const { client } = usePlatform();
|
|
842
|
+
const [show, setShow] = useState5(false);
|
|
843
|
+
const [testing, setTesting] = useState5(false);
|
|
844
|
+
const set = (patch) => onChange({ ...value, ...patch });
|
|
845
|
+
useEffect2(() => {
|
|
846
|
+
if (provisioner && !provisioner.ok && value.mode === "managed") set({ mode: "external" });
|
|
847
|
+
}, [provisioner]);
|
|
848
|
+
const test = async () => {
|
|
849
|
+
setTesting(true);
|
|
850
|
+
set({ report: null });
|
|
851
|
+
try {
|
|
852
|
+
const report = await client.post("databases/inspect", { url: value.url.trim(), ...companyId ? { companyId } : {} });
|
|
853
|
+
onChange({ ...value, report, testedUrl: value.url.trim() });
|
|
854
|
+
} catch (e) {
|
|
855
|
+
onChange({ ...value, report: { ok: false, reachable: false, missingPrivileges: [], foreignTables: [], error: e.message }, testedUrl: value.url.trim() });
|
|
856
|
+
} finally {
|
|
857
|
+
setTesting(false);
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
const options = [
|
|
861
|
+
{ value: "managed", icon: /* @__PURE__ */ jsx8(IconServer, { size: 20 }), title: "Managed database", body: "We create a dedicated database and users on the platform's MySQL server.", disabled: provisioner !== null && provisioner !== void 0 && !provisioner.ok },
|
|
862
|
+
{ value: "external", icon: /* @__PURE__ */ jsx8(IconLink, { size: 20 }), title: "Bring your own database", body: "An existing empty MySQL database. We build and maintain the schema.", disabled: false }
|
|
863
|
+
];
|
|
864
|
+
return /* @__PURE__ */ jsxs7("div", { children: [
|
|
865
|
+
/* @__PURE__ */ jsx8("div", { className: "grid gap-3 [grid-template-columns:repeat(auto-fit,minmax(14rem,1fr))]", role: "radiogroup", "aria-label": "Database", children: options.map((o) => {
|
|
866
|
+
const on = value.mode === o.value;
|
|
867
|
+
return /* @__PURE__ */ jsxs7(
|
|
868
|
+
"button",
|
|
869
|
+
{
|
|
870
|
+
type: "button",
|
|
871
|
+
role: "radio",
|
|
872
|
+
"aria-checked": on,
|
|
873
|
+
disabled: o.disabled,
|
|
874
|
+
onClick: () => set({ mode: o.value }),
|
|
875
|
+
className: `rounded-xl border text-left transition-colors disabled:cursor-not-allowed disabled:opacity-60 ${compact ? "p-3" : "p-4"} ${on ? "border-primary-500 bg-primary-50 ring-1 ring-primary-500" : "border-line hover:bg-background"}`,
|
|
876
|
+
children: [
|
|
877
|
+
/* @__PURE__ */ jsx8("span", { className: on ? "text-primary-600" : "text-muted", children: o.icon }),
|
|
878
|
+
/* @__PURE__ */ jsx8("div", { className: "mt-1 text-sm font-medium text-foreground", children: o.title }),
|
|
879
|
+
/* @__PURE__ */ jsx8("div", { className: "mt-1 text-xs text-muted", children: o.body }),
|
|
880
|
+
o.disabled && (provisioner == null ? void 0 : provisioner.error) && /* @__PURE__ */ jsxs7("div", { className: "mt-2 text-xs text-error-600", children: [
|
|
881
|
+
"Unavailable: ",
|
|
882
|
+
provisioner.error
|
|
883
|
+
] })
|
|
884
|
+
]
|
|
885
|
+
},
|
|
886
|
+
o.value
|
|
887
|
+
);
|
|
888
|
+
}) }),
|
|
889
|
+
value.mode === "external" && /* @__PURE__ */ jsxs7("div", { className: "mt-4", children: [
|
|
890
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-end gap-2", children: [
|
|
891
|
+
/* @__PURE__ */ jsx8("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx8(
|
|
892
|
+
Input2,
|
|
893
|
+
{
|
|
894
|
+
label: "Database URL",
|
|
895
|
+
type: show ? "text" : "password",
|
|
896
|
+
autoComplete: "off",
|
|
897
|
+
spellCheck: false,
|
|
898
|
+
value: value.url,
|
|
899
|
+
placeholder: "mysql://user:password@db.example.com:3306/company_db",
|
|
900
|
+
helperText: storedTarget ? `A URL is already saved (${storedTarget}). Leave empty to keep it, or paste a new one.` : "The user needs SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX and REFERENCES on that database.",
|
|
901
|
+
onChange: (e) => set({ url: e.target.value, report: null })
|
|
902
|
+
}
|
|
903
|
+
) }),
|
|
904
|
+
/* @__PURE__ */ jsx8(Button4, { variant: "ghost", size: "icon", "aria-label": show ? "Hide URL" : "Show URL", onClick: () => setShow((v) => !v), children: show ? /* @__PURE__ */ jsx8(IconEyeOff, {}) : /* @__PURE__ */ jsx8(IconEye, {}) }),
|
|
905
|
+
/* @__PURE__ */ jsx8(Button4, { variant: "outline", onClick: test, loading: testing, disabled: !value.url.trim(), children: "Test connection" })
|
|
906
|
+
] }),
|
|
907
|
+
value.report && /* @__PURE__ */ jsx8(InspectResult, { report: value.report }),
|
|
908
|
+
((_a = value.report) == null ? void 0 : _a.ok) && !acceptStates.includes(value.report.state) && /* @__PURE__ */ jsx8("p", { className: "mt-2 text-xs text-warning-700", children: "That database can't be used here. New companies need an empty database." })
|
|
909
|
+
] }),
|
|
910
|
+
value.mode === "managed" && (provisioner == null ? void 0 : provisioner.ok) && /* @__PURE__ */ jsxs7("p", { className: "mt-3 flex items-center gap-1.5 text-xs text-success-600", children: [
|
|
911
|
+
/* @__PURE__ */ jsx8(IconCheck, { size: 14 }),
|
|
912
|
+
" Provisioner ready"
|
|
913
|
+
] })
|
|
914
|
+
] });
|
|
915
|
+
}
|
|
916
|
+
function databaseReady(value, provisioner, { acceptStates = ["empty"], keepStored = false } = {}) {
|
|
917
|
+
var _a;
|
|
918
|
+
if (value.mode === "managed") return (provisioner == null ? void 0 : provisioner.ok) === true;
|
|
919
|
+
if (keepStored && !value.url.trim()) return true;
|
|
920
|
+
return Boolean(((_a = value.report) == null ? void 0 : _a.ok) && value.testedUrl === value.url.trim() && acceptStates.includes(value.report.state));
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// src/platform/views/NewCompany.jsx
|
|
924
|
+
import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
925
|
+
var STEPS = [{ title: "Company" }, { title: "Database" }, { title: "First admin" }, { title: "Review" }];
|
|
926
|
+
function NewCompanyView() {
|
|
927
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
928
|
+
const { client, Link, href, companyLoginUrl, companyLoginPath } = usePlatform();
|
|
929
|
+
const sensitive = useSensitive();
|
|
930
|
+
const provisioner = useProvisioner();
|
|
931
|
+
const [step, setStep] = useState6(0);
|
|
932
|
+
const [displayName, setDisplayName] = useState6("");
|
|
933
|
+
const [slug, setSlug] = useState6("");
|
|
934
|
+
const [slugTouched, setSlugTouched] = useState6(false);
|
|
935
|
+
const [slugState, setSlugState] = useState6({ checking: false });
|
|
936
|
+
const [branchCode, setBranchCode] = useState6("HQ");
|
|
937
|
+
const [branchName, setBranchName] = useState6("Head Office");
|
|
938
|
+
const [db, setDb] = useState6({ mode: "managed", url: "", report: null, testedUrl: null });
|
|
939
|
+
const [adminName, setAdminName] = useState6("");
|
|
940
|
+
const [adminEmail, setAdminEmail] = useState6("");
|
|
941
|
+
const [submitting, setSubmitting] = useState6(false);
|
|
942
|
+
const [error, setError] = useState6(null);
|
|
943
|
+
const [result, setResult] = useState6(null);
|
|
944
|
+
const [retrying, setRetrying] = useState6(false);
|
|
945
|
+
useEffect3(() => {
|
|
946
|
+
if (!slugTouched) setSlug(slugify(displayName));
|
|
947
|
+
}, [displayName, slugTouched]);
|
|
948
|
+
const timer = useRef2(null);
|
|
949
|
+
useEffect3(() => {
|
|
950
|
+
clearTimeout(timer.current);
|
|
951
|
+
if (!slug) {
|
|
952
|
+
setSlugState({ checking: false });
|
|
953
|
+
return void 0;
|
|
954
|
+
}
|
|
955
|
+
setSlugState({ checking: true });
|
|
956
|
+
timer.current = setTimeout(async () => {
|
|
957
|
+
try {
|
|
958
|
+
const r = await client.get("companies/slug-available", { slug });
|
|
959
|
+
setSlugState({ checking: false, available: r.available, reason: r.reason });
|
|
960
|
+
} catch {
|
|
961
|
+
setSlugState({ checking: false });
|
|
962
|
+
}
|
|
963
|
+
}, 350);
|
|
964
|
+
return () => clearTimeout(timer.current);
|
|
965
|
+
}, [slug, client]);
|
|
966
|
+
const valid = [
|
|
967
|
+
displayName.trim() && slugState.available === true && /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/.test(branchCode) && branchName.trim(),
|
|
968
|
+
databaseReady(db, provisioner),
|
|
969
|
+
adminName.trim() && EMAIL_RE.test(adminEmail.trim()),
|
|
970
|
+
true
|
|
971
|
+
];
|
|
972
|
+
async function create() {
|
|
973
|
+
setSubmitting(true);
|
|
974
|
+
setError(null);
|
|
975
|
+
try {
|
|
976
|
+
const r = await sensitive(
|
|
977
|
+
() => client.post("companies", {
|
|
978
|
+
displayName: displayName.trim(),
|
|
979
|
+
slug,
|
|
980
|
+
branch: { code: branchCode.trim().toUpperCase(), name: branchName.trim() },
|
|
981
|
+
database: db.mode === "external" ? { mode: "external", url: db.url.trim() } : { mode: "managed" },
|
|
982
|
+
admin: { fullName: adminName.trim(), email: adminEmail.trim().toLowerCase() }
|
|
983
|
+
})
|
|
984
|
+
);
|
|
985
|
+
setResult(r);
|
|
986
|
+
setDb((d) => ({ ...d, url: "" }));
|
|
987
|
+
} catch (e) {
|
|
988
|
+
setError(errorText(e));
|
|
989
|
+
} finally {
|
|
990
|
+
setSubmitting(false);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
async function retry() {
|
|
994
|
+
var _a2;
|
|
995
|
+
setRetrying(true);
|
|
996
|
+
setError(null);
|
|
997
|
+
try {
|
|
998
|
+
const pending = result.admin ? void 0 : { fullName: adminName.trim(), email: adminEmail.trim().toLowerCase() };
|
|
999
|
+
const r = await sensitive(() => client.post(`companies/${result.company.id}/provision`, { admin: pending }));
|
|
1000
|
+
setResult({ ...result, job: r.job, admin: (_a2 = r.admin) != null ? _a2 : result.admin, adminError: r.adminError });
|
|
1001
|
+
} catch (e) {
|
|
1002
|
+
setError(errorText(e));
|
|
1003
|
+
} finally {
|
|
1004
|
+
setRetrying(false);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (result) {
|
|
1008
|
+
const ok = ((_a = result.job) == null ? void 0 : _a.status) === "succeeded";
|
|
1009
|
+
const lastError = (_d = (_b = result.job) == null ? void 0 : _b.lastError) != null ? _d : (_c = result.job) == null ? void 0 : _c.last_error;
|
|
1010
|
+
return /* @__PURE__ */ jsxs8("div", { className: "mx-auto max-w-3xl space-y-6 px-4 py-6 sm:px-6", children: [
|
|
1011
|
+
/* @__PURE__ */ jsx9(
|
|
1012
|
+
PageHeader3,
|
|
1013
|
+
{
|
|
1014
|
+
title: ok ? `${(_e = result.company.displayName) != null ? _e : displayName} is ready` : `${(_f = result.company.displayName) != null ? _f : displayName} needs attention`,
|
|
1015
|
+
subtitle: ok ? "The database is provisioned and the first admin can sign in." : "The company was created but its database setup did not finish.",
|
|
1016
|
+
linkAs: Link
|
|
1017
|
+
}
|
|
1018
|
+
),
|
|
1019
|
+
error && /* @__PURE__ */ jsx9(Alert2, { variant: "error", children: error }),
|
|
1020
|
+
ok && result.admin && /* @__PURE__ */ jsx9(OneTimeSecret, { title: "First admin: sign-in details", loginUrl: companyLoginUrl(result.company.slug), email: result.admin.email, password: result.admin.password }),
|
|
1021
|
+
ok && !result.admin && /* @__PURE__ */ jsxs8(Alert2, { variant: "warning", children: [
|
|
1022
|
+
"The database is ready, but the first admin could not be created",
|
|
1023
|
+
result.adminError ? `: ${result.adminError}` : "",
|
|
1024
|
+
". Add one from the company's Admins tab."
|
|
1025
|
+
] }),
|
|
1026
|
+
!ok && /* @__PURE__ */ jsxs8(Alert2, { variant: "error", title: `Stopped at \u201C${(_h = (_g = result.job) == null ? void 0 : _g.step) != null ? _h : "setup"}\u201D`, children: [
|
|
1027
|
+
/* @__PURE__ */ jsx9("span", { className: "block break-words font-mono text-xs", children: lastError }),
|
|
1028
|
+
jobErrorHint(lastError) && /* @__PURE__ */ jsx9("span", { className: "mt-2 block", children: jobErrorHint(lastError) }),
|
|
1029
|
+
/* @__PURE__ */ jsx9("span", { className: "mt-2 block", children: "Fix the cause, then retry. It resumes from where it stopped." })
|
|
1030
|
+
] }),
|
|
1031
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap gap-2", children: [
|
|
1032
|
+
!ok && /* @__PURE__ */ jsx9(Button5, { onClick: retry, loading: retrying, leftIcon: /* @__PURE__ */ jsx9(IconRefresh, {}), children: "Retry setup" }),
|
|
1033
|
+
/* @__PURE__ */ jsx9(Button5, { as: Link, href: href(`companies/${result.company.id}`), variant: ok ? "primary" : "outline", children: "Open company" })
|
|
1034
|
+
] })
|
|
1035
|
+
] });
|
|
1036
|
+
}
|
|
1037
|
+
return /* @__PURE__ */ jsxs8("div", { className: "mx-auto max-w-3xl space-y-6 px-4 py-6 sm:px-6", children: [
|
|
1038
|
+
/* @__PURE__ */ jsx9(
|
|
1039
|
+
PageHeader3,
|
|
1040
|
+
{
|
|
1041
|
+
title: "New company",
|
|
1042
|
+
subtitle: "Create the company, its database and its first administrator",
|
|
1043
|
+
back: { href: href("companies"), label: "Companies" },
|
|
1044
|
+
linkAs: Link
|
|
1045
|
+
}
|
|
1046
|
+
),
|
|
1047
|
+
/* @__PURE__ */ jsx9(Stepper, { steps: STEPS, currentStep: step, orientation: "auto" }),
|
|
1048
|
+
/* @__PURE__ */ jsxs8("div", { className: "space-y-4 rounded-xl border border-line bg-surface p-4 sm:p-6", children: [
|
|
1049
|
+
step === 0 && /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
1050
|
+
/* @__PURE__ */ jsx9("h2", { className: "text-sm font-medium text-foreground", children: "Company details" }),
|
|
1051
|
+
/* @__PURE__ */ jsx9(Input3, { label: "Company name", required: true, autoFocus: true, value: displayName, placeholder: "Acme Industries Pvt Ltd", onChange: (e) => setDisplayName(e.target.value) }),
|
|
1052
|
+
/* @__PURE__ */ jsx9(
|
|
1053
|
+
Input3,
|
|
1054
|
+
{
|
|
1055
|
+
label: "Company URL slug",
|
|
1056
|
+
required: true,
|
|
1057
|
+
value: slug,
|
|
1058
|
+
helperText: `Sign-in address: ${companyLoginPath(slug || "your-company")}. Lowercase letters, digits and hyphens.`,
|
|
1059
|
+
error: !slugState.checking && slugState.available === false ? slugState.reason || "Not available" : void 0,
|
|
1060
|
+
success: !slugState.checking && slugState.available ? "Available" : void 0,
|
|
1061
|
+
onChange: (e) => {
|
|
1062
|
+
setSlugTouched(true);
|
|
1063
|
+
setSlug(e.target.value.toLowerCase());
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
),
|
|
1067
|
+
/* @__PURE__ */ jsxs8("div", { className: "grid gap-4 [grid-template-columns:minmax(6rem,1fr)_minmax(10rem,2fr)]", children: [
|
|
1068
|
+
/* @__PURE__ */ jsx9(Input3, { label: "Default branch code", required: true, value: branchCode, onChange: (e) => setBranchCode(e.target.value.toUpperCase()) }),
|
|
1069
|
+
/* @__PURE__ */ jsx9(Input3, { label: "Default branch name", required: true, value: branchName, onChange: (e) => setBranchName(e.target.value) })
|
|
1070
|
+
] })
|
|
1071
|
+
] }),
|
|
1072
|
+
step === 1 && /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
1073
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
1074
|
+
/* @__PURE__ */ jsx9("h2", { className: "text-sm font-medium text-foreground", children: "Where does this company's data live?" }),
|
|
1075
|
+
/* @__PURE__ */ jsx9("p", { className: "mt-1 text-xs text-muted", children: "Each company gets its own database. Its URL is stored encrypted and never shown again." })
|
|
1076
|
+
] }),
|
|
1077
|
+
/* @__PURE__ */ jsx9(DatabaseChoice, { value: db, onChange: setDb, provisioner })
|
|
1078
|
+
] }),
|
|
1079
|
+
step === 2 && /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
1080
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
1081
|
+
/* @__PURE__ */ jsx9("h2", { className: "text-sm font-medium text-foreground", children: "First company administrator" }),
|
|
1082
|
+
/* @__PURE__ */ jsx9("p", { className: "mt-1 text-xs text-muted", children: "They get full access to the company and can invite everyone else. A temporary password is shown to you once at the end." })
|
|
1083
|
+
] }),
|
|
1084
|
+
/* @__PURE__ */ jsx9(Input3, { label: "Full name", required: true, autoFocus: true, value: adminName, onChange: (e) => setAdminName(e.target.value) }),
|
|
1085
|
+
/* @__PURE__ */ jsx9(Input3, { label: "Email", type: "email", required: true, value: adminEmail, onChange: (e) => setAdminEmail(e.target.value) })
|
|
1086
|
+
] }),
|
|
1087
|
+
step === 3 && /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
1088
|
+
/* @__PURE__ */ jsx9("h2", { className: "text-sm font-medium text-foreground", children: "Review" }),
|
|
1089
|
+
/* @__PURE__ */ jsx9("dl", { className: "divide-y divide-line rounded-lg border border-line text-sm", children: [
|
|
1090
|
+
["Company", displayName],
|
|
1091
|
+
["Sign-in address", companyLoginPath(slug)],
|
|
1092
|
+
["Default branch", `${branchCode} \xB7 ${branchName}`],
|
|
1093
|
+
["Database", db.mode === "managed" ? "Managed (created on the platform server)" : `Own database \xB7 ${(_j = (_i = db.report) == null ? void 0 : _i.target) != null ? _j : ""}`],
|
|
1094
|
+
["First admin", `${adminName} <${adminEmail}>`]
|
|
1095
|
+
].map(([k, v]) => /* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap gap-x-4 px-3 py-2", children: [
|
|
1096
|
+
/* @__PURE__ */ jsx9("dt", { className: "w-36 shrink-0 text-muted", children: k }),
|
|
1097
|
+
/* @__PURE__ */ jsx9("dd", { className: "min-w-0 break-words text-foreground", children: v })
|
|
1098
|
+
] }, k)) }),
|
|
1099
|
+
error && /* @__PURE__ */ jsx9(Alert2, { variant: "error", children: error }),
|
|
1100
|
+
submitting && /* @__PURE__ */ jsx9(Alert2, { variant: "info", children: "Creating the database, applying the schema and seeding catalogs. This can take a minute\u2026" })
|
|
1101
|
+
] })
|
|
1102
|
+
] }),
|
|
1103
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between", children: [
|
|
1104
|
+
/* @__PURE__ */ jsx9(Button5, { variant: "outline", onClick: () => setStep((s) => Math.max(0, s - 1)), disabled: step === 0 || submitting, children: "Back" }),
|
|
1105
|
+
step < STEPS.length - 1 ? /* @__PURE__ */ jsx9(Button5, { onClick: () => setStep((s) => s + 1), disabled: !valid[step], children: "Continue" }) : /* @__PURE__ */ jsx9(Button5, { onClick: create, loading: submitting, children: "Create company" })
|
|
1106
|
+
] })
|
|
1107
|
+
] });
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// src/platform/views/CompanyDetail.jsx
|
|
1111
|
+
import React9, { useCallback as useCallback3, useEffect as useEffect4, useState as useState7 } from "react";
|
|
1112
|
+
import {
|
|
1113
|
+
Alert as Alert3,
|
|
1114
|
+
Button as Button6,
|
|
1115
|
+
ConfirmDialog,
|
|
1116
|
+
Dialog as Dialog2,
|
|
1117
|
+
EmptyState as EmptyState2,
|
|
1118
|
+
ErrorState as ErrorState3,
|
|
1119
|
+
Input as Input4,
|
|
1120
|
+
LoadingState as LoadingState3,
|
|
1121
|
+
PageHeader as PageHeader4,
|
|
1122
|
+
Tabs,
|
|
1123
|
+
Textarea
|
|
1124
|
+
} from "@dreamtree-org/twreact-ui";
|
|
1125
|
+
import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1126
|
+
var SETUP_STATES = ["draft", "provisioning", "provisioning_failed"];
|
|
1127
|
+
function CompanyDetailView({ companyId }) {
|
|
1128
|
+
var _a;
|
|
1129
|
+
const { client, Link, href, can, companyTabs, companyLoginUrl, companyLoginPath } = usePlatform();
|
|
1130
|
+
const sensitive = useSensitive();
|
|
1131
|
+
const { data, error, reload } = useLoad(`companies/${encodeURIComponent(companyId)}`);
|
|
1132
|
+
const [tab, setTab] = useState7(0);
|
|
1133
|
+
const [flash, setFlash] = useState7(null);
|
|
1134
|
+
const [secret, setSecret] = useState7(null);
|
|
1135
|
+
const run = useCallback3(
|
|
1136
|
+
async (fn, ok, { stepUp = false } = {}) => {
|
|
1137
|
+
setFlash(null);
|
|
1138
|
+
try {
|
|
1139
|
+
const r = await (stepUp ? sensitive(fn) : fn());
|
|
1140
|
+
if (ok) setFlash({ kind: "success", msg: ok });
|
|
1141
|
+
await reload();
|
|
1142
|
+
return r != null ? r : true;
|
|
1143
|
+
} catch (e) {
|
|
1144
|
+
const msg = errorText(e);
|
|
1145
|
+
if (msg) setFlash({ kind: "error", msg });
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
},
|
|
1149
|
+
[reload, sensitive]
|
|
1150
|
+
);
|
|
1151
|
+
if (error && !data) return /* @__PURE__ */ jsx10("div", { className: "mx-auto max-w-6xl px-4 py-6 sm:px-6", children: /* @__PURE__ */ jsx10(ErrorState3, { description: error.message, onRetry: reload }) });
|
|
1152
|
+
if (!data) return /* @__PURE__ */ jsx10("div", { className: "mx-auto max-w-6xl px-4 py-6 sm:px-6", children: /* @__PURE__ */ jsx10(LoadingState3, { variant: "detail" }) });
|
|
1153
|
+
const company = data.company;
|
|
1154
|
+
const database = (_a = company.database) != null ? _a : { configured: false };
|
|
1155
|
+
const ctx = { company, users: data.users, run, setFlash, setSecret, client, can, reload };
|
|
1156
|
+
const tabs = [
|
|
1157
|
+
{ id: "overview", label: "Overview", render: () => /* @__PURE__ */ jsx10(OverviewTab, { ...ctx }) },
|
|
1158
|
+
{ id: "branches", label: "Branches", render: () => /* @__PURE__ */ jsx10(BranchesTab, { ...ctx }) },
|
|
1159
|
+
{ id: "database", label: "Database", render: () => /* @__PURE__ */ jsx10(DatabaseTab, { ...ctx }) },
|
|
1160
|
+
{ id: "admins", label: "Admins", render: () => /* @__PURE__ */ jsx10(AdminsTab, { ...ctx }) },
|
|
1161
|
+
...(companyTabs != null ? companyTabs : []).map((t) => ({ id: t.id, label: t.label, render: () => t.render({ company, reload, client, sensitive }) })),
|
|
1162
|
+
{ id: "activity", label: "Activity", render: () => /* @__PURE__ */ jsx10(ActivityTab, { ...ctx }) }
|
|
1163
|
+
];
|
|
1164
|
+
return /* @__PURE__ */ jsxs9("div", { className: "mx-auto max-w-6xl space-y-5 px-4 py-6 sm:px-6", children: [
|
|
1165
|
+
/* @__PURE__ */ jsx10(
|
|
1166
|
+
PageHeader4,
|
|
1167
|
+
{
|
|
1168
|
+
title: company.displayName,
|
|
1169
|
+
subtitle: /* @__PURE__ */ jsxs9("span", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1170
|
+
/* @__PURE__ */ jsx10("span", { className: "font-mono", children: companyLoginPath(company.slug) }),
|
|
1171
|
+
/* @__PURE__ */ jsx10(StatusPill, { status: company.status }),
|
|
1172
|
+
/* @__PURE__ */ jsx10(ModePill, { mode: database.configured ? database.mode : null })
|
|
1173
|
+
] }),
|
|
1174
|
+
back: { href: href("companies"), label: "Companies" },
|
|
1175
|
+
linkAs: Link
|
|
1176
|
+
}
|
|
1177
|
+
),
|
|
1178
|
+
flash && /* @__PURE__ */ jsx10(Alert3, { variant: flash.kind, dismissible: true, onDismiss: () => setFlash(null), children: flash.msg }),
|
|
1179
|
+
secret && /* @__PURE__ */ jsx10(OneTimeSecret, { title: secret.title, loginUrl: companyLoginUrl(company.slug), email: secret.email, password: secret.password, onDone: () => setSecret(null) }),
|
|
1180
|
+
/* @__PURE__ */ jsxs9(Tabs, { index: tab, onChange: setTab, variant: "line", children: [
|
|
1181
|
+
/* @__PURE__ */ jsx10(Tabs.List, { children: tabs.map((t) => /* @__PURE__ */ jsx10(Tabs.Tab, { children: t.label }, t.id)) }),
|
|
1182
|
+
/* @__PURE__ */ jsx10(Tabs.Panels, { children: tabs.map((t, i) => /* @__PURE__ */ jsx10(Tabs.Panel, { children: i === tab ? /* @__PURE__ */ jsx10("div", { className: "pt-4", children: t.render() }) : null }, t.id)) })
|
|
1183
|
+
] })
|
|
1184
|
+
] });
|
|
1185
|
+
}
|
|
1186
|
+
function OverviewTab({ company, users, run, setSecret, client, can }) {
|
|
1187
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1188
|
+
const { companyLoginUrl } = usePlatform();
|
|
1189
|
+
const [confirm, setConfirm] = useState7(null);
|
|
1190
|
+
const [editing, setEditing] = useState7(false);
|
|
1191
|
+
const setup = SETUP_STATES.includes(company.status);
|
|
1192
|
+
const writable = can("companies.write");
|
|
1193
|
+
const db = (_a = company.database) != null ? _a : {};
|
|
1194
|
+
const url = companyLoginUrl(company.slug);
|
|
1195
|
+
const LABEL = { active: "Activate", suspended: "Suspend", archived: "Archive" };
|
|
1196
|
+
return /* @__PURE__ */ jsxs9("div", { className: "grid gap-5 [grid-template-columns:repeat(auto-fit,minmax(min(20rem,100%),1fr))]", children: [
|
|
1197
|
+
/* @__PURE__ */ jsxs9("div", { className: "space-y-5", children: [
|
|
1198
|
+
setup && writable && /* @__PURE__ */ jsx10(SetupPanel, { company, run, setSecret, client }),
|
|
1199
|
+
/* @__PURE__ */ jsx10(
|
|
1200
|
+
Card,
|
|
1201
|
+
{
|
|
1202
|
+
title: "Company",
|
|
1203
|
+
actions: writable && /* @__PURE__ */ jsx10(Button6, { size: "sm", variant: "outline", onClick: () => setEditing(true), children: "Edit details" }),
|
|
1204
|
+
children: /* @__PURE__ */ jsx10(
|
|
1205
|
+
Facts,
|
|
1206
|
+
{
|
|
1207
|
+
rows: [
|
|
1208
|
+
["Name", company.displayName],
|
|
1209
|
+
[
|
|
1210
|
+
"Sign-in address",
|
|
1211
|
+
/* @__PURE__ */ jsxs9("span", { className: "flex items-center gap-2", children: [
|
|
1212
|
+
/* @__PURE__ */ jsx10("span", { className: "truncate font-mono text-xs", children: url }),
|
|
1213
|
+
/* @__PURE__ */ jsx10(CopyButton, { value: url })
|
|
1214
|
+
] }, "l")
|
|
1215
|
+
],
|
|
1216
|
+
["Branches", (_c = (_b = company.branches) == null ? void 0 : _b.length) != null ? _c : "\u2014"],
|
|
1217
|
+
["Users", users ? `${users.active} active of ${users.total}` : "\u2014"],
|
|
1218
|
+
["Database", db.configured ? (_d = db.target) != null ? _d : "\u2014" : "Not provisioned"],
|
|
1219
|
+
["Created", formatDate(company.createdAt)]
|
|
1220
|
+
]
|
|
1221
|
+
}
|
|
1222
|
+
)
|
|
1223
|
+
}
|
|
1224
|
+
)
|
|
1225
|
+
] }),
|
|
1226
|
+
/* @__PURE__ */ jsx10(Card, { title: "Lifecycle", children: setup ? /* @__PURE__ */ jsx10("p", { className: "text-sm text-muted", children: "Activation, suspension and archiving become available once the company's database is set up." }) : /* @__PURE__ */ jsxs9(Fragment4, { children: [
|
|
1227
|
+
/* @__PURE__ */ jsx10("p", { className: "mb-3 text-sm text-muted", children: "Suspending immediately blocks every sign-in and request for this company. Data is kept." }),
|
|
1228
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-2", children: [
|
|
1229
|
+
writable && company.status !== "active" && /* @__PURE__ */ jsx10(Button6, { variant: "outline", leftIcon: /* @__PURE__ */ jsx10(IconPlay, {}), onClick: () => setConfirm("active"), children: "Activate" }),
|
|
1230
|
+
writable && company.status === "active" && /* @__PURE__ */ jsx10(Button6, { variant: "outline", leftIcon: /* @__PURE__ */ jsx10(IconPause, {}), onClick: () => setConfirm("suspended"), children: "Suspend" }),
|
|
1231
|
+
writable && company.status !== "archived" && /* @__PURE__ */ jsx10(Button6, { variant: "outline", leftIcon: /* @__PURE__ */ jsx10(IconArchive, {}), onClick: () => setConfirm("archived"), children: "Archive" })
|
|
1232
|
+
] })
|
|
1233
|
+
] }) }),
|
|
1234
|
+
editing && /* @__PURE__ */ jsx10(EditDetails, { company, run, client, onClose: () => setEditing(false) }),
|
|
1235
|
+
/* @__PURE__ */ jsx10(
|
|
1236
|
+
ConfirmDialog,
|
|
1237
|
+
{
|
|
1238
|
+
isOpen: confirm !== null,
|
|
1239
|
+
title: `${(_e = LABEL[confirm]) != null ? _e : ""} ${company.displayName}?`,
|
|
1240
|
+
description: confirm === "active" ? `${company.displayName}'s users will be able to sign in again.` : `Everyone at ${company.displayName} is signed out and blocked until it is activated again. No data is deleted.`,
|
|
1241
|
+
confirmLabel: (_f = LABEL[confirm]) != null ? _f : "Confirm",
|
|
1242
|
+
destructive: confirm !== "active",
|
|
1243
|
+
onCancel: () => setConfirm(null),
|
|
1244
|
+
onConfirm: () => {
|
|
1245
|
+
const to = confirm;
|
|
1246
|
+
setConfirm(null);
|
|
1247
|
+
void run(() => client.patch(`companies/${company.id}`, { status: to }), `Company ${to === "active" ? "activated" : to}`, { stepUp: true });
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
)
|
|
1251
|
+
] });
|
|
1252
|
+
}
|
|
1253
|
+
function EditDetails({ company, run, client, onClose }) {
|
|
1254
|
+
const { companyLoginPath } = usePlatform();
|
|
1255
|
+
const [name, setName] = useState7(company.displayName);
|
|
1256
|
+
const [slug, setSlug] = useState7(company.slug);
|
|
1257
|
+
const [slugState, setSlugState] = useState7({ available: true });
|
|
1258
|
+
const [busy, setBusy] = useState7(false);
|
|
1259
|
+
useEffect4(() => {
|
|
1260
|
+
if (slug === company.slug) {
|
|
1261
|
+
setSlugState({ available: true });
|
|
1262
|
+
return void 0;
|
|
1263
|
+
}
|
|
1264
|
+
const t = setTimeout(async () => {
|
|
1265
|
+
try {
|
|
1266
|
+
setSlugState(await client.get("companies/slug-available", { slug }));
|
|
1267
|
+
} catch {
|
|
1268
|
+
setSlugState({});
|
|
1269
|
+
}
|
|
1270
|
+
}, 350);
|
|
1271
|
+
return () => clearTimeout(t);
|
|
1272
|
+
}, [slug, company.slug, client]);
|
|
1273
|
+
return /* @__PURE__ */ jsx10(
|
|
1274
|
+
Dialog2,
|
|
1275
|
+
{
|
|
1276
|
+
isOpen: true,
|
|
1277
|
+
onClose,
|
|
1278
|
+
title: "Edit company details",
|
|
1279
|
+
size: "md",
|
|
1280
|
+
footer: /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2", children: [
|
|
1281
|
+
/* @__PURE__ */ jsx10(Button6, { variant: "outline", onClick: onClose, children: "Cancel" }),
|
|
1282
|
+
/* @__PURE__ */ jsx10(
|
|
1283
|
+
Button6,
|
|
1284
|
+
{
|
|
1285
|
+
loading: busy,
|
|
1286
|
+
disabled: !name.trim() || slugState.available !== true,
|
|
1287
|
+
onClick: async () => {
|
|
1288
|
+
setBusy(true);
|
|
1289
|
+
const ok = await run(
|
|
1290
|
+
() => client.patch(`companies/${company.id}`, { displayName: name.trim(), slug: slug.trim() }),
|
|
1291
|
+
"Company details saved",
|
|
1292
|
+
{ stepUp: slug !== company.slug }
|
|
1293
|
+
);
|
|
1294
|
+
setBusy(false);
|
|
1295
|
+
if (ok) onClose();
|
|
1296
|
+
},
|
|
1297
|
+
children: "Save"
|
|
1298
|
+
}
|
|
1299
|
+
)
|
|
1300
|
+
] }),
|
|
1301
|
+
children: /* @__PURE__ */ jsxs9("div", { className: "space-y-4", children: [
|
|
1302
|
+
/* @__PURE__ */ jsx10(Input4, { label: "Company name", value: name, onChange: (e) => setName(e.target.value) }),
|
|
1303
|
+
/* @__PURE__ */ jsx10(
|
|
1304
|
+
Input4,
|
|
1305
|
+
{
|
|
1306
|
+
label: "URL slug",
|
|
1307
|
+
value: slug,
|
|
1308
|
+
onChange: (e) => setSlug(e.target.value.toLowerCase()),
|
|
1309
|
+
helperText: `Sign-in address: ${companyLoginPath(slug)}. Changing it moves the address; old links stop working.`,
|
|
1310
|
+
error: slugState.available === false ? slugState.reason || "Not available" : void 0
|
|
1311
|
+
}
|
|
1312
|
+
)
|
|
1313
|
+
] })
|
|
1314
|
+
}
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
function SetupPanel({ company, run, setSecret, client }) {
|
|
1318
|
+
var _a, _b, _c;
|
|
1319
|
+
const provisioner = useProvisioner();
|
|
1320
|
+
const job = company.lastJob;
|
|
1321
|
+
const stored = ((_a = company.database) == null ? void 0 : _a.configured) ? company.database.mode : null;
|
|
1322
|
+
const [db, setDb] = useState7({ mode: stored === "external" ? "external" : "managed", url: "", report: null, testedUrl: null });
|
|
1323
|
+
const [adminName, setAdminName] = useState7("");
|
|
1324
|
+
const [adminEmail, setAdminEmail] = useState7("");
|
|
1325
|
+
const [busy, setBusy] = useState7(false);
|
|
1326
|
+
const ready = databaseReady(db, provisioner, { acceptStates: ["empty", "company"], keepStored: stored === "external" });
|
|
1327
|
+
const adminOk = !adminName.trim() && !adminEmail.trim() || adminName.trim() && EMAIL_RE.test(adminEmail.trim());
|
|
1328
|
+
const provision = async () => {
|
|
1329
|
+
setBusy(true);
|
|
1330
|
+
await run(
|
|
1331
|
+
async () => {
|
|
1332
|
+
var _a2, _b2, _c2, _d, _e;
|
|
1333
|
+
const r = await client.post(`companies/${company.id}/provision`, {
|
|
1334
|
+
database: db.mode === "external" ? { mode: "external", ...db.url.trim() ? { url: db.url.trim() } : {} } : { mode: "managed" },
|
|
1335
|
+
...adminEmail.trim() ? { admin: { fullName: adminName.trim(), email: adminEmail.trim().toLowerCase() } } : {}
|
|
1336
|
+
});
|
|
1337
|
+
if (((_a2 = r.job) == null ? void 0 : _a2.status) !== "succeeded") throw new Error((_e = (_d = (_b2 = r.job) == null ? void 0 : _b2.lastError) != null ? _d : (_c2 = r.job) == null ? void 0 : _c2.last_error) != null ? _e : "Provisioning did not finish");
|
|
1338
|
+
if (r.admin) setSecret({ ...r.admin, title: "First admin: sign-in details" });
|
|
1339
|
+
if (r.adminError) throw new Error(`Database ready, but the admin was not created: ${r.adminError}`);
|
|
1340
|
+
},
|
|
1341
|
+
adminEmail.trim() ? "Company is ready: share the sign-in details below" : "Database provisioned. Add the first admin from the Admins tab."
|
|
1342
|
+
);
|
|
1343
|
+
setBusy(false);
|
|
1344
|
+
};
|
|
1345
|
+
const lastError = (_b = job == null ? void 0 : job.lastError) != null ? _b : job == null ? void 0 : job.last_error;
|
|
1346
|
+
return /* @__PURE__ */ jsx10(Card, { title: "Finish setup", children: /* @__PURE__ */ jsxs9("div", { className: "space-y-4", children: [
|
|
1347
|
+
/* @__PURE__ */ jsx10("p", { className: "text-sm text-muted", children: "This company's database is not ready yet, so nobody can sign in." }),
|
|
1348
|
+
(job == null ? void 0 : job.status) === "failed" && /* @__PURE__ */ jsxs9(Alert3, { variant: "error", title: `Last attempt failed${job.step ? ` at \u201C${job.step}\u201D` : ""}`, children: [
|
|
1349
|
+
/* @__PURE__ */ jsx10("span", { className: "block break-words font-mono text-xs", children: lastError }),
|
|
1350
|
+
jobErrorHint(lastError) && /* @__PURE__ */ jsx10("span", { className: "mt-1 block", children: jobErrorHint(lastError) })
|
|
1351
|
+
] }),
|
|
1352
|
+
/* @__PURE__ */ jsx10(
|
|
1353
|
+
DatabaseChoice,
|
|
1354
|
+
{
|
|
1355
|
+
value: db,
|
|
1356
|
+
onChange: setDb,
|
|
1357
|
+
provisioner,
|
|
1358
|
+
companyId: company.id,
|
|
1359
|
+
acceptStates: ["empty", "company"],
|
|
1360
|
+
storedTarget: stored === "external" ? (_c = company.database.target) != null ? _c : "hidden" : null,
|
|
1361
|
+
compact: true
|
|
1362
|
+
}
|
|
1363
|
+
),
|
|
1364
|
+
/* @__PURE__ */ jsxs9("div", { children: [
|
|
1365
|
+
/* @__PURE__ */ jsx10("div", { className: "text-sm font-medium text-foreground", children: "First admin (recommended)" }),
|
|
1366
|
+
/* @__PURE__ */ jsx10("p", { className: "mb-2 text-xs text-muted", children: "A temporary password is generated and shown once when setup finishes." }),
|
|
1367
|
+
/* @__PURE__ */ jsxs9("div", { className: "grid gap-3 [grid-template-columns:repeat(auto-fit,minmax(12rem,1fr))]", children: [
|
|
1368
|
+
/* @__PURE__ */ jsx10(Input4, { "aria-label": "First admin full name", placeholder: "Full name", value: adminName, onChange: (e) => setAdminName(e.target.value) }),
|
|
1369
|
+
/* @__PURE__ */ jsx10(Input4, { "aria-label": "First admin email", type: "email", placeholder: "email@company.com", value: adminEmail, onChange: (e) => setAdminEmail(e.target.value) })
|
|
1370
|
+
] })
|
|
1371
|
+
] }),
|
|
1372
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex flex-wrap items-center gap-3", children: [
|
|
1373
|
+
/* @__PURE__ */ jsx10(Button6, { loading: busy, disabled: !ready || !adminOk, leftIcon: /* @__PURE__ */ jsx10(IconRefresh, {}), onClick: provision, children: (job == null ? void 0 : job.status) === "failed" ? "Retry setup" : "Provision database" }),
|
|
1374
|
+
busy && /* @__PURE__ */ jsx10("span", { className: "text-xs text-muted", children: "Applying the schema. This can take a minute\u2026" })
|
|
1375
|
+
] })
|
|
1376
|
+
] }) });
|
|
1377
|
+
}
|
|
1378
|
+
function BranchesTab({ company, run, client, can }) {
|
|
1379
|
+
var _a;
|
|
1380
|
+
const [editing, setEditing] = useState7(null);
|
|
1381
|
+
const [code, setCode] = useState7("");
|
|
1382
|
+
const [name, setName] = useState7("");
|
|
1383
|
+
const writable = can("companies.write");
|
|
1384
|
+
const branches = (_a = company.branches) != null ? _a : [];
|
|
1385
|
+
const open = (b) => {
|
|
1386
|
+
var _a2, _b;
|
|
1387
|
+
setEditing(b != null ? b : {});
|
|
1388
|
+
setCode((_a2 = b == null ? void 0 : b.code) != null ? _a2 : "");
|
|
1389
|
+
setName((_b = b == null ? void 0 : b.name) != null ? _b : "");
|
|
1390
|
+
};
|
|
1391
|
+
const save = async () => {
|
|
1392
|
+
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");
|
|
1393
|
+
if (ok) setEditing(null);
|
|
1394
|
+
};
|
|
1395
|
+
return /* @__PURE__ */ jsxs9(
|
|
1396
|
+
Card,
|
|
1397
|
+
{
|
|
1398
|
+
title: "Branches",
|
|
1399
|
+
actions: writable && /* @__PURE__ */ jsx10(Button6, { size: "sm", leftIcon: /* @__PURE__ */ jsx10(IconPlus, {}), onClick: () => open(null), children: "Add branch" }),
|
|
1400
|
+
children: [
|
|
1401
|
+
branches.length === 0 ? /* @__PURE__ */ jsx10(EmptyState2, { title: "No branches", compact: true }) : /* @__PURE__ */ jsx10("ul", { className: "divide-y divide-line", children: branches.map((b) => /* @__PURE__ */ jsxs9("li", { className: "flex flex-wrap items-center gap-3 py-2.5", children: [
|
|
1402
|
+
/* @__PURE__ */ jsx10("span", { className: "w-20 shrink-0 font-mono text-xs text-muted", children: b.code }),
|
|
1403
|
+
/* @__PURE__ */ jsxs9("span", { className: "min-w-0 flex-1 text-sm text-foreground", children: [
|
|
1404
|
+
b.name,
|
|
1405
|
+
b.id === company.defaultBranchId && /* @__PURE__ */ jsx10("span", { className: "ml-2 text-xs text-muted", children: "default" })
|
|
1406
|
+
] }),
|
|
1407
|
+
/* @__PURE__ */ jsx10(StatusPill, { status: b.status }),
|
|
1408
|
+
writable && /* @__PURE__ */ jsxs9("div", { className: "flex gap-1", children: [
|
|
1409
|
+
/* @__PURE__ */ jsx10(Button6, { size: "sm", variant: "ghost", onClick: () => open(b), children: "Rename" }),
|
|
1410
|
+
b.id !== company.defaultBranchId && /* @__PURE__ */ jsx10(
|
|
1411
|
+
Button6,
|
|
1412
|
+
{
|
|
1413
|
+
size: "sm",
|
|
1414
|
+
variant: "ghost",
|
|
1415
|
+
onClick: () => run(
|
|
1416
|
+
() => client.patch(`companies/${company.id}/branches/${b.id}`, { status: b.status === "active" ? "inactive" : "active" }),
|
|
1417
|
+
b.status === "active" ? "Branch deactivated" : "Branch activated"
|
|
1418
|
+
),
|
|
1419
|
+
children: b.status === "active" ? "Deactivate" : "Activate"
|
|
1420
|
+
}
|
|
1421
|
+
)
|
|
1422
|
+
] })
|
|
1423
|
+
] }, b.id)) }),
|
|
1424
|
+
/* @__PURE__ */ jsx10(
|
|
1425
|
+
Dialog2,
|
|
1426
|
+
{
|
|
1427
|
+
isOpen: editing !== null,
|
|
1428
|
+
onClose: () => setEditing(null),
|
|
1429
|
+
title: (editing == null ? void 0 : editing.id) ? `Rename ${editing.code}` : "Add branch",
|
|
1430
|
+
size: "sm",
|
|
1431
|
+
footer: /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2", children: [
|
|
1432
|
+
/* @__PURE__ */ jsx10(Button6, { variant: "outline", onClick: () => setEditing(null), children: "Cancel" }),
|
|
1433
|
+
/* @__PURE__ */ jsx10(Button6, { 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" })
|
|
1434
|
+
] }),
|
|
1435
|
+
children: /* @__PURE__ */ jsxs9("div", { className: "space-y-4", children: [
|
|
1436
|
+
!(editing == null ? void 0 : editing.id) && /* @__PURE__ */ jsx10(Input4, { label: "Code", value: code, onChange: (e) => setCode(e.target.value.toUpperCase()), helperText: "Letters, digits, - and _. Cannot be changed later." }),
|
|
1437
|
+
/* @__PURE__ */ jsx10(Input4, { label: "Name", value: name, onChange: (e) => setName(e.target.value) })
|
|
1438
|
+
] })
|
|
1439
|
+
}
|
|
1440
|
+
)
|
|
1441
|
+
]
|
|
1442
|
+
}
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
function DatabaseTab({ company, run, setFlash, client, can }) {
|
|
1446
|
+
var _a, _b, _c, _d, _e;
|
|
1447
|
+
const db = (_a = company.database) != null ? _a : { configured: false };
|
|
1448
|
+
const [busy, setBusy] = useState7(null);
|
|
1449
|
+
const [steps, setSteps] = useState7(null);
|
|
1450
|
+
const [replacing, setReplacing] = useState7(false);
|
|
1451
|
+
const [newUrl, setNewUrl] = useState7("");
|
|
1452
|
+
const [report, setReport] = useState7(null);
|
|
1453
|
+
const [revealing, setRevealing] = useState7(false);
|
|
1454
|
+
const [reason, setReason] = useState7("");
|
|
1455
|
+
const [revealed, setRevealed] = useState7(null);
|
|
1456
|
+
if (!db.configured) return /* @__PURE__ */ jsx10(Card, { title: "Database", children: /* @__PURE__ */ jsx10("p", { className: "text-sm text-muted", children: "No database yet. Provision it from the Overview tab." }) });
|
|
1457
|
+
const writable = can("db.write");
|
|
1458
|
+
return /* @__PURE__ */ jsxs9("div", { className: "grid gap-5 [grid-template-columns:repeat(auto-fit,minmax(min(20rem,100%),1fr))]", children: [
|
|
1459
|
+
/* @__PURE__ */ jsxs9(Card, { title: "Connection", children: [
|
|
1460
|
+
/* @__PURE__ */ jsx10(
|
|
1461
|
+
Facts,
|
|
1462
|
+
{
|
|
1463
|
+
rows: [
|
|
1464
|
+
["Type", /* @__PURE__ */ jsx10(ModePill, { mode: db.mode }, "m")],
|
|
1465
|
+
["Target", /* @__PURE__ */ jsx10("span", { className: "font-mono text-xs", children: (_b = db.target) != null ? _b : "\u2014" }, "t")],
|
|
1466
|
+
["Status", /* @__PURE__ */ jsx10(StatusPill, { status: db.status }, "s")],
|
|
1467
|
+
["Health", /* @__PURE__ */ jsxs9("span", { children: [
|
|
1468
|
+
/* @__PURE__ */ jsx10(HealthDot, { ok: db.lastCheckOk }),
|
|
1469
|
+
" ",
|
|
1470
|
+
db.lastCheckAt && /* @__PURE__ */ jsxs9("span", { className: "text-xs text-muted", children: [
|
|
1471
|
+
"\xB7 ",
|
|
1472
|
+
timeAgo(db.lastCheckAt)
|
|
1473
|
+
] })
|
|
1474
|
+
] }, "h")],
|
|
1475
|
+
["Schema version", (_c = db.schemaVersion) != null ? _c : "\u2014"],
|
|
1476
|
+
["Connection version", (_d = db.connectionVersion) != null ? _d : "\u2014"],
|
|
1477
|
+
["Encryption key", (_e = db.keyId) != null ? _e : "\u2014"],
|
|
1478
|
+
["Updated", formatDate(db.updatedAt)]
|
|
1479
|
+
]
|
|
1480
|
+
}
|
|
1481
|
+
),
|
|
1482
|
+
db.lastCheckOk === false && db.lastCheckError && /* @__PURE__ */ jsx10("div", { className: "mt-4 rounded-lg bg-error-50 p-3 font-mono text-xs text-error-800", children: db.lastCheckError }),
|
|
1483
|
+
steps && /* @__PURE__ */ jsx10("ul", { className: "mt-4 space-y-1 rounded-lg bg-background p-3 text-xs text-muted", children: steps.map((s) => /* @__PURE__ */ jsxs9("li", { children: [
|
|
1484
|
+
"\u2713 ",
|
|
1485
|
+
s
|
|
1486
|
+
] }, s)) }),
|
|
1487
|
+
revealed && /* @__PURE__ */ jsxs9("div", { className: "mt-4 rounded-lg border border-warning-200 bg-warning-50 p-3", children: [
|
|
1488
|
+
/* @__PURE__ */ jsx10("div", { className: "mb-1 text-xs font-medium text-warning-800", children: "Database URL (audited, shown once)" }),
|
|
1489
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2", children: [
|
|
1490
|
+
/* @__PURE__ */ jsx10("code", { className: "min-w-0 flex-1 break-all text-xs text-foreground", children: revealed }),
|
|
1491
|
+
/* @__PURE__ */ jsx10(CopyButton, { value: revealed })
|
|
1492
|
+
] }),
|
|
1493
|
+
/* @__PURE__ */ jsx10("button", { type: "button", className: "mt-2 text-xs text-muted underline", onClick: () => setRevealed(null), children: "Hide" })
|
|
1494
|
+
] })
|
|
1495
|
+
] }),
|
|
1496
|
+
writable && /* @__PURE__ */ jsx10(Card, { title: "Actions", children: /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-2", children: [
|
|
1497
|
+
/* @__PURE__ */ jsx10(
|
|
1498
|
+
Button6,
|
|
1499
|
+
{
|
|
1500
|
+
variant: "outline",
|
|
1501
|
+
loading: busy === "test",
|
|
1502
|
+
onClick: async () => {
|
|
1503
|
+
setBusy("test");
|
|
1504
|
+
await run(async () => {
|
|
1505
|
+
var _a2;
|
|
1506
|
+
const r = await client.post(`companies/${company.id}/database`, { action: "test" });
|
|
1507
|
+
if (!r.ok) throw new Error(`Check failed: ${(_a2 = r.error) != null ? _a2 : "unknown error"}`);
|
|
1508
|
+
setFlash({ kind: "success", msg: `Connected and identity verified${r.latencyMs ? ` in ${r.latencyMs} ms` : ""}` });
|
|
1509
|
+
}, null);
|
|
1510
|
+
setBusy(null);
|
|
1511
|
+
},
|
|
1512
|
+
children: "Test connection"
|
|
1513
|
+
}
|
|
1514
|
+
),
|
|
1515
|
+
/* @__PURE__ */ jsx10(
|
|
1516
|
+
Button6,
|
|
1517
|
+
{
|
|
1518
|
+
variant: "outline",
|
|
1519
|
+
loading: busy === "sync",
|
|
1520
|
+
onClick: async () => {
|
|
1521
|
+
setBusy("sync");
|
|
1522
|
+
setSteps(null);
|
|
1523
|
+
await run(async () => setSteps((await client.post(`companies/${company.id}/database`, { action: "sync" })).steps), "Schema is up to date (nothing was dropped)");
|
|
1524
|
+
setBusy(null);
|
|
1525
|
+
},
|
|
1526
|
+
children: "Sync schema"
|
|
1527
|
+
}
|
|
1528
|
+
),
|
|
1529
|
+
db.mode === "external" && /* @__PURE__ */ jsx10(Button6, { variant: "outline", onClick: () => setReplacing(true), children: "Replace URL" }),
|
|
1530
|
+
can("db.reveal") && /* @__PURE__ */ jsx10(Button6, { variant: "ghost", onClick: () => setRevealing(true), children: "Reveal URL\u2026" }),
|
|
1531
|
+
/* @__PURE__ */ jsx10("p", { className: "mt-1 text-xs text-muted", children: "Sync adds missing tables, columns and indexes. It never drops or renames anything." })
|
|
1532
|
+
] }) }),
|
|
1533
|
+
/* @__PURE__ */ jsxs9(
|
|
1534
|
+
Dialog2,
|
|
1535
|
+
{
|
|
1536
|
+
isOpen: replacing,
|
|
1537
|
+
onClose: () => {
|
|
1538
|
+
setReplacing(false);
|
|
1539
|
+
setNewUrl("");
|
|
1540
|
+
setReport(null);
|
|
1541
|
+
},
|
|
1542
|
+
title: "Replace database URL",
|
|
1543
|
+
size: "md",
|
|
1544
|
+
footer: /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2", children: [
|
|
1545
|
+
/* @__PURE__ */ jsx10(Button6, { variant: "outline", onClick: () => setReplacing(false), children: "Cancel" }),
|
|
1546
|
+
/* @__PURE__ */ jsx10(
|
|
1547
|
+
Button6,
|
|
1548
|
+
{
|
|
1549
|
+
disabled: !(report == null ? void 0 : report.ok) || report.state !== "company",
|
|
1550
|
+
loading: busy === "replace",
|
|
1551
|
+
onClick: async () => {
|
|
1552
|
+
setBusy("replace");
|
|
1553
|
+
const ok = await run(() => client.post(`companies/${company.id}/database`, { action: "replace", url: newUrl.trim() }), "Database URL replaced", { stepUp: true });
|
|
1554
|
+
setBusy(null);
|
|
1555
|
+
if (ok) {
|
|
1556
|
+
setReplacing(false);
|
|
1557
|
+
setNewUrl("");
|
|
1558
|
+
setReport(null);
|
|
1559
|
+
}
|
|
1560
|
+
},
|
|
1561
|
+
children: "Replace"
|
|
1562
|
+
}
|
|
1563
|
+
)
|
|
1564
|
+
] }),
|
|
1565
|
+
children: [
|
|
1566
|
+
/* @__PURE__ */ jsx10("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." }),
|
|
1567
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-end gap-2", children: [
|
|
1568
|
+
/* @__PURE__ */ jsx10("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx10(Input4, { label: "New database URL", type: "password", autoComplete: "off", value: newUrl, onChange: (e) => (setNewUrl(e.target.value), setReport(null)) }) }),
|
|
1569
|
+
/* @__PURE__ */ jsx10(
|
|
1570
|
+
Button6,
|
|
1571
|
+
{
|
|
1572
|
+
variant: "outline",
|
|
1573
|
+
disabled: !newUrl.trim(),
|
|
1574
|
+
onClick: async () => {
|
|
1575
|
+
try {
|
|
1576
|
+
setReport(await client.post("databases/inspect", { url: newUrl.trim(), companyId: company.id }));
|
|
1577
|
+
} catch (e) {
|
|
1578
|
+
setReport({ ok: false, reachable: false, error: e.message });
|
|
1579
|
+
}
|
|
1580
|
+
},
|
|
1581
|
+
children: "Test"
|
|
1582
|
+
}
|
|
1583
|
+
)
|
|
1584
|
+
] }),
|
|
1585
|
+
report && /* @__PURE__ */ jsx10(InspectResult, { report })
|
|
1586
|
+
]
|
|
1587
|
+
}
|
|
1588
|
+
),
|
|
1589
|
+
/* @__PURE__ */ jsxs9(
|
|
1590
|
+
Dialog2,
|
|
1591
|
+
{
|
|
1592
|
+
isOpen: revealing,
|
|
1593
|
+
onClose: () => setRevealing(false),
|
|
1594
|
+
title: "Reveal database URL",
|
|
1595
|
+
size: "md",
|
|
1596
|
+
footer: /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2", children: [
|
|
1597
|
+
/* @__PURE__ */ jsx10(Button6, { variant: "outline", onClick: () => setRevealing(false), children: "Cancel" }),
|
|
1598
|
+
/* @__PURE__ */ jsx10(
|
|
1599
|
+
Button6,
|
|
1600
|
+
{
|
|
1601
|
+
variant: "destructive",
|
|
1602
|
+
disabled: reason.trim().length < 8,
|
|
1603
|
+
onClick: async () => {
|
|
1604
|
+
const r = await run(() => client.put(`companies/${company.id}/database`, { action: "reveal", reason: reason.trim() }), null, { stepUp: true });
|
|
1605
|
+
if (r && r.url) {
|
|
1606
|
+
setRevealed(r.url);
|
|
1607
|
+
setRevealing(false);
|
|
1608
|
+
setReason("");
|
|
1609
|
+
}
|
|
1610
|
+
},
|
|
1611
|
+
children: "Reveal"
|
|
1612
|
+
}
|
|
1613
|
+
)
|
|
1614
|
+
] }),
|
|
1615
|
+
children: [
|
|
1616
|
+
/* @__PURE__ */ jsx10("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." }),
|
|
1617
|
+
/* @__PURE__ */ jsx10(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." })
|
|
1618
|
+
]
|
|
1619
|
+
}
|
|
1620
|
+
)
|
|
1621
|
+
] });
|
|
1622
|
+
}
|
|
1623
|
+
function AdminsTab({ company, setSecret, setFlash, client, can }) {
|
|
1624
|
+
var _a, _b, _c;
|
|
1625
|
+
const sensitive = useSensitive();
|
|
1626
|
+
const ready = ((_a = company.database) == null ? void 0 : _a.configured) && ((_b = company.database) == null ? void 0 : _b.status) === "active";
|
|
1627
|
+
const { data, error, reload } = useLoad(ready ? `companies/${company.id}/admins` : null);
|
|
1628
|
+
const [adding, setAdding] = useState7(false);
|
|
1629
|
+
const [fullName, setFullName] = useState7("");
|
|
1630
|
+
const [email, setEmail] = useState7("");
|
|
1631
|
+
const [phone, setPhone] = useState7("");
|
|
1632
|
+
const [busy, setBusy] = useState7(false);
|
|
1633
|
+
const writable = can("companies.write");
|
|
1634
|
+
if (!ready) return /* @__PURE__ */ jsx10(Card, { title: "Company admins", children: /* @__PURE__ */ jsx10("p", { className: "text-sm text-muted", children: "Admins can be added once the company's database is provisioned." }) });
|
|
1635
|
+
const act = async (a, action) => {
|
|
1636
|
+
var _a2;
|
|
1637
|
+
try {
|
|
1638
|
+
const r = await sensitive(() => client.post(`companies/${company.id}/admins/${a.id}`, { action }));
|
|
1639
|
+
if (r.password) setSecret({ email: (_a2 = r.email) != null ? _a2 : a.email, password: r.password, title: `New password for ${a.email}` });
|
|
1640
|
+
else setFlash({ kind: "success", msg: `${a.email} ${action}d` });
|
|
1641
|
+
await reload();
|
|
1642
|
+
} catch (e) {
|
|
1643
|
+
const msg = errorText(e);
|
|
1644
|
+
if (msg) setFlash({ kind: "error", msg });
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
const add = async () => {
|
|
1648
|
+
setBusy(true);
|
|
1649
|
+
try {
|
|
1650
|
+
const r = await client.post(`companies/${company.id}/admins`, { fullName: fullName.trim(), email: email.trim().toLowerCase(), ...phone.trim() ? { phone: phone.trim() } : {} });
|
|
1651
|
+
setSecret({ email: r.email, password: r.password, title: `Sign-in details for ${r.email}` });
|
|
1652
|
+
setAdding(false);
|
|
1653
|
+
setFullName("");
|
|
1654
|
+
setEmail("");
|
|
1655
|
+
setPhone("");
|
|
1656
|
+
await reload();
|
|
1657
|
+
} catch (e) {
|
|
1658
|
+
setFlash({ kind: "error", msg: errorText(e) });
|
|
1659
|
+
} finally {
|
|
1660
|
+
setBusy(false);
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
return /* @__PURE__ */ jsxs9(
|
|
1664
|
+
Card,
|
|
1665
|
+
{
|
|
1666
|
+
title: "Company admins",
|
|
1667
|
+
actions: writable && /* @__PURE__ */ jsx10(Button6, { size: "sm", leftIcon: /* @__PURE__ */ jsx10(IconPlus, {}), onClick: () => setAdding(true), children: "Add admin" }),
|
|
1668
|
+
children: [
|
|
1669
|
+
error && !data ? /* @__PURE__ */ jsx10(ErrorState3, { description: error.message, onRetry: reload }) : !data ? /* @__PURE__ */ jsx10(LoadingState3, { variant: "table", rows: 2 }) : !((_c = data.admins) == null ? void 0 : _c.length) ? /* @__PURE__ */ jsx10(EmptyState2, { title: "No administrators yet", compact: true }) : /* @__PURE__ */ jsx10("ul", { className: "divide-y divide-line", children: data.admins.map((a) => /* @__PURE__ */ jsxs9("li", { className: "flex flex-wrap items-center gap-3 py-2.5", children: [
|
|
1670
|
+
/* @__PURE__ */ jsxs9("div", { className: "min-w-0 flex-1", children: [
|
|
1671
|
+
/* @__PURE__ */ jsx10("div", { className: "truncate text-sm font-medium text-foreground", children: a.fullName }),
|
|
1672
|
+
/* @__PURE__ */ jsx10("div", { className: "truncate text-xs text-muted", children: a.email })
|
|
1673
|
+
] }),
|
|
1674
|
+
/* @__PURE__ */ jsx10(StatusPill, { status: a.status }),
|
|
1675
|
+
writable && /* @__PURE__ */ jsxs9("div", { className: "flex gap-1", children: [
|
|
1676
|
+
/* @__PURE__ */ jsx10(Button6, { size: "sm", variant: "ghost", onClick: () => act(a, "reset-password"), children: "Reset password" }),
|
|
1677
|
+
/* @__PURE__ */ jsx10(Button6, { size: "sm", variant: "ghost", onClick: () => act(a, a.status === "active" ? "disable" : "enable"), children: a.status === "active" ? "Disable" : "Enable" })
|
|
1678
|
+
] })
|
|
1679
|
+
] }, a.id)) }),
|
|
1680
|
+
/* @__PURE__ */ jsx10(
|
|
1681
|
+
Dialog2,
|
|
1682
|
+
{
|
|
1683
|
+
isOpen: adding,
|
|
1684
|
+
onClose: () => setAdding(false),
|
|
1685
|
+
title: "Add company admin",
|
|
1686
|
+
size: "sm",
|
|
1687
|
+
footer: /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2", children: [
|
|
1688
|
+
/* @__PURE__ */ jsx10(Button6, { variant: "outline", onClick: () => setAdding(false), children: "Cancel" }),
|
|
1689
|
+
/* @__PURE__ */ jsx10(Button6, { loading: busy, disabled: !fullName.trim() || !EMAIL_RE.test(email.trim()), onClick: add, children: "Add admin" })
|
|
1690
|
+
] }),
|
|
1691
|
+
children: /* @__PURE__ */ jsxs9("div", { className: "space-y-4", children: [
|
|
1692
|
+
/* @__PURE__ */ jsx10(Input4, { label: "Full name", autoFocus: true, value: fullName, onChange: (e) => setFullName(e.target.value) }),
|
|
1693
|
+
/* @__PURE__ */ jsx10(Input4, { label: "Email", type: "email", value: email, onChange: (e) => setEmail(e.target.value) }),
|
|
1694
|
+
/* @__PURE__ */ jsx10(Input4, { label: "Phone (optional)", type: "tel", value: phone, onChange: (e) => setPhone(e.target.value) })
|
|
1695
|
+
] })
|
|
1696
|
+
}
|
|
1697
|
+
)
|
|
1698
|
+
]
|
|
1699
|
+
}
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
function ActivityTab({ company, client, can }) {
|
|
1703
|
+
var _a, _b;
|
|
1704
|
+
const sensitive = useSensitive();
|
|
1705
|
+
const { data, error, reload } = useLoad(`companies/${company.id}/activity`);
|
|
1706
|
+
const [retryError, setRetryError] = useState7(null);
|
|
1707
|
+
const modeOf = (payload) => {
|
|
1708
|
+
try {
|
|
1709
|
+
const p = typeof payload === "string" ? JSON.parse(payload) : payload;
|
|
1710
|
+
return (p == null ? void 0 : p.mode) === "external" ? "own database" : "managed";
|
|
1711
|
+
} catch {
|
|
1712
|
+
return "managed";
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
if (error && !data) return /* @__PURE__ */ jsx10(ErrorState3, { description: error.message, onRetry: reload });
|
|
1716
|
+
if (!data) return /* @__PURE__ */ jsx10(LoadingState3, { variant: "table", rows: 4 });
|
|
1717
|
+
return /* @__PURE__ */ jsxs9("div", { className: "grid gap-5 [grid-template-columns:repeat(auto-fit,minmax(min(20rem,100%),1fr))]", children: [
|
|
1718
|
+
/* @__PURE__ */ jsxs9(Card, { title: "Provisioning jobs", children: [
|
|
1719
|
+
retryError && /* @__PURE__ */ jsx10(Alert3, { variant: "error", children: retryError }),
|
|
1720
|
+
!((_a = data.jobs) == null ? void 0 : _a.length) ? /* @__PURE__ */ jsx10("p", { className: "text-sm text-muted", children: "No provisioning jobs." }) : /* @__PURE__ */ jsx10("ul", { className: "divide-y divide-line", children: data.jobs.map((j) => {
|
|
1721
|
+
var _a2, _b2;
|
|
1722
|
+
const lastError = (_a2 = j.lastError) != null ? _a2 : j.last_error;
|
|
1723
|
+
return /* @__PURE__ */ jsxs9("li", { className: "py-2.5", children: [
|
|
1724
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1725
|
+
/* @__PURE__ */ jsx10(StatusPill, { status: j.status }),
|
|
1726
|
+
/* @__PURE__ */ jsxs9("span", { className: "text-xs text-muted", children: [
|
|
1727
|
+
modeOf(j.payload),
|
|
1728
|
+
" \xB7 step ",
|
|
1729
|
+
/* @__PURE__ */ jsx10("span", { className: "font-mono", children: j.step }),
|
|
1730
|
+
" \xB7 ",
|
|
1731
|
+
j.attempts,
|
|
1732
|
+
" attempt(s)"
|
|
1733
|
+
] }),
|
|
1734
|
+
j.status === "failed" && can("companies.write") && /* @__PURE__ */ jsx10(
|
|
1735
|
+
Button6,
|
|
1736
|
+
{
|
|
1737
|
+
size: "sm",
|
|
1738
|
+
variant: "ghost",
|
|
1739
|
+
className: "ml-auto",
|
|
1740
|
+
onClick: async () => {
|
|
1741
|
+
setRetryError(null);
|
|
1742
|
+
try {
|
|
1743
|
+
await sensitive(() => client.post(`provisioning/${j.id}`));
|
|
1744
|
+
} catch (e) {
|
|
1745
|
+
setRetryError(errorText(e));
|
|
1746
|
+
}
|
|
1747
|
+
await reload();
|
|
1748
|
+
},
|
|
1749
|
+
children: "Retry"
|
|
1750
|
+
}
|
|
1751
|
+
)
|
|
1752
|
+
] }),
|
|
1753
|
+
lastError && j.status !== "succeeded" && /* @__PURE__ */ jsx10("div", { className: "mt-1 break-words font-mono text-xs text-error-700", children: lastError }),
|
|
1754
|
+
/* @__PURE__ */ jsx10("div", { className: "mt-0.5 text-xs text-muted", children: formatDate((_b2 = j.updatedAt) != null ? _b2 : j.updated_at) })
|
|
1755
|
+
] }, j.id);
|
|
1756
|
+
}) })
|
|
1757
|
+
] }),
|
|
1758
|
+
/* @__PURE__ */ jsx10(Card, { title: "Audit trail", children: !((_b = data.events) == null ? void 0 : _b.length) ? /* @__PURE__ */ jsx10("p", { className: "text-sm text-muted", children: "No events." }) : /* @__PURE__ */ jsx10("ol", { className: "divide-y divide-line", children: data.events.map(normalizeEvent).map((e, i) => {
|
|
1759
|
+
var _a2;
|
|
1760
|
+
return /* @__PURE__ */ jsxs9("li", { className: "py-2.5", children: [
|
|
1761
|
+
/* @__PURE__ */ jsxs9("div", { className: "text-sm text-foreground", children: [
|
|
1762
|
+
/* @__PURE__ */ jsx10("span", { className: "font-medium", children: e.actor }),
|
|
1763
|
+
" ",
|
|
1764
|
+
describeAction(e.action),
|
|
1765
|
+
e.target && /* @__PURE__ */ jsxs9("span", { className: "text-muted", children: [
|
|
1766
|
+
" \xB7 ",
|
|
1767
|
+
e.target
|
|
1768
|
+
] })
|
|
1769
|
+
] }),
|
|
1770
|
+
/* @__PURE__ */ jsx10("div", { className: "text-xs text-muted", children: formatDate(e.at) })
|
|
1771
|
+
] }, (_a2 = e.id) != null ? _a2 : i);
|
|
1772
|
+
}) }) })
|
|
1773
|
+
] });
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// src/platform/views/Admins.jsx
|
|
1777
|
+
import React10, { useState as useState8 } from "react";
|
|
1778
|
+
import { Alert as Alert4, Badge as Badge2, Button as Button7, ConfirmDialog as ConfirmDialog2, Dialog as Dialog3, ErrorState as ErrorState4, Input as Input5, LoadingState as LoadingState4, PageHeader as PageHeader5, Select } from "@dreamtree-org/twreact-ui";
|
|
1779
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1780
|
+
var ROLE_OPTIONS = [
|
|
1781
|
+
{ value: "superadmin", label: "Superadmin: everything" },
|
|
1782
|
+
{ value: "support", label: "Support: read-only" }
|
|
1783
|
+
];
|
|
1784
|
+
function AdminsView() {
|
|
1785
|
+
var _a;
|
|
1786
|
+
const { client, can, loginUrl, Link } = usePlatform();
|
|
1787
|
+
const sensitive = useSensitive();
|
|
1788
|
+
const { data, error, loading, reload } = useLoad("admins");
|
|
1789
|
+
const [flash, setFlash] = useState8(null);
|
|
1790
|
+
const [secret, setSecret] = useState8(null);
|
|
1791
|
+
const [adding, setAdding] = useState8(false);
|
|
1792
|
+
const [email, setEmail] = useState8("");
|
|
1793
|
+
const [role, setRole] = useState8("superadmin");
|
|
1794
|
+
const [busy, setBusy] = useState8(false);
|
|
1795
|
+
const [confirm, setConfirm] = useState8(null);
|
|
1796
|
+
const writable = can("admins.write");
|
|
1797
|
+
const act = async (a, action, extra = {}) => {
|
|
1798
|
+
var _a2;
|
|
1799
|
+
setFlash(null);
|
|
1800
|
+
try {
|
|
1801
|
+
const r = await sensitive(() => client.post(`admins/${a.id}`, { action, ...extra }));
|
|
1802
|
+
if (r.password) setSecret({ email: (_a2 = r.email) != null ? _a2 : a.email, password: r.password, title: `New password for ${a.email}` });
|
|
1803
|
+
else setFlash({ kind: "success", msg: `${a.email}: done` });
|
|
1804
|
+
await reload();
|
|
1805
|
+
} catch (e) {
|
|
1806
|
+
const msg = errorText(e);
|
|
1807
|
+
if (msg) setFlash({ kind: "error", msg });
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
const add = async () => {
|
|
1811
|
+
setBusy(true);
|
|
1812
|
+
setFlash(null);
|
|
1813
|
+
try {
|
|
1814
|
+
const r = await sensitive(() => client.post("admins", { email: email.trim(), roles: [role] }));
|
|
1815
|
+
setSecret({ email: r.email, password: r.password, title: `Console sign-in for ${r.email}` });
|
|
1816
|
+
setAdding(false);
|
|
1817
|
+
setEmail("");
|
|
1818
|
+
await reload();
|
|
1819
|
+
} catch (e) {
|
|
1820
|
+
const msg = errorText(e);
|
|
1821
|
+
if (msg) setFlash({ kind: "error", msg });
|
|
1822
|
+
} finally {
|
|
1823
|
+
setBusy(false);
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
return /* @__PURE__ */ jsxs10("div", { className: "mx-auto max-w-5xl space-y-6 px-4 py-6 sm:px-6", children: [
|
|
1827
|
+
/* @__PURE__ */ jsx11(
|
|
1828
|
+
PageHeader5,
|
|
1829
|
+
{
|
|
1830
|
+
title: "Platform admins",
|
|
1831
|
+
subtitle: "People who can create companies and manage the platform",
|
|
1832
|
+
linkAs: Link,
|
|
1833
|
+
actions: writable && /* @__PURE__ */ jsx11(Button7, { leftIcon: /* @__PURE__ */ jsx11(IconPlus, {}), onClick: () => setAdding(true), children: "Add platform admin" })
|
|
1834
|
+
}
|
|
1835
|
+
),
|
|
1836
|
+
flash && /* @__PURE__ */ jsx11(Alert4, { variant: flash.kind, dismissible: true, onDismiss: () => setFlash(null), children: flash.msg }),
|
|
1837
|
+
secret && /* @__PURE__ */ jsx11(OneTimeSecret, { title: secret.title, loginUrl, email: secret.email, password: secret.password, onDone: () => setSecret(null) }),
|
|
1838
|
+
error && !data ? /* @__PURE__ */ jsx11(ErrorState4, { description: error.message, onRetry: reload }) : loading && !data ? /* @__PURE__ */ jsx11(LoadingState4, { variant: "table", rows: 3 }) : /* @__PURE__ */ jsx11("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) => {
|
|
1839
|
+
var _a2, _b, _c;
|
|
1840
|
+
const me = a.id === data.me;
|
|
1841
|
+
const roles = Array.isArray(a.roles) ? a.roles : [];
|
|
1842
|
+
return /* @__PURE__ */ jsxs10("li", { className: "flex flex-wrap items-center gap-3 px-4 py-3", children: [
|
|
1843
|
+
/* @__PURE__ */ jsxs10("div", { className: "min-w-0 flex-1", children: [
|
|
1844
|
+
/* @__PURE__ */ jsxs10("div", { className: "flex flex-wrap items-center gap-2 text-sm font-medium text-foreground", children: [
|
|
1845
|
+
/* @__PURE__ */ jsx11("span", { className: "truncate", children: a.email }),
|
|
1846
|
+
me && /* @__PURE__ */ jsx11(Badge2, { variant: "primary", size: "sm", children: "you" })
|
|
1847
|
+
] }),
|
|
1848
|
+
/* @__PURE__ */ jsxs10("div", { className: "text-xs text-muted", children: [
|
|
1849
|
+
roles.length ? roles.join(", ") : "No role",
|
|
1850
|
+
" \xB7 last sign-in ",
|
|
1851
|
+
timeAgo((_a2 = a.lastLoginAt) != null ? _a2 : a.last_login_at),
|
|
1852
|
+
!((_b = a.totpEnrolled) != null ? _b : Number(a.strong_factor_enrolled)) && " \xB7 no authenticator yet"
|
|
1853
|
+
] })
|
|
1854
|
+
] }),
|
|
1855
|
+
/* @__PURE__ */ jsx11(StatusPill, { status: a.status }),
|
|
1856
|
+
writable && /* @__PURE__ */ jsxs10("div", { className: "flex flex-wrap gap-1", children: [
|
|
1857
|
+
/* @__PURE__ */ jsx11(Button7, { size: "sm", variant: "ghost", onClick: () => act(a, "reset-password"), children: "Reset password" }),
|
|
1858
|
+
!me && ((_c = a.totpEnrolled) != null ? _c : Number(a.strong_factor_enrolled)) ? /* @__PURE__ */ jsx11(Button7, { size: "sm", variant: "ghost", onClick: () => setConfirm({ admin: a, action: "reset-totp" }), children: "Reset authenticator" }) : null,
|
|
1859
|
+
!me && (a.status === "active" ? /* @__PURE__ */ jsx11(Button7, { size: "sm", variant: "ghost", onClick: () => setConfirm({ admin: a, action: "disable" }), children: "Disable" }) : /* @__PURE__ */ jsx11(Button7, { size: "sm", variant: "ghost", onClick: () => act(a, "enable"), children: "Enable" }))
|
|
1860
|
+
] })
|
|
1861
|
+
] }, a.id);
|
|
1862
|
+
}) }),
|
|
1863
|
+
/* @__PURE__ */ jsx11(
|
|
1864
|
+
Dialog3,
|
|
1865
|
+
{
|
|
1866
|
+
isOpen: adding,
|
|
1867
|
+
onClose: () => setAdding(false),
|
|
1868
|
+
title: "Add platform admin",
|
|
1869
|
+
size: "sm",
|
|
1870
|
+
footer: /* @__PURE__ */ jsxs10("div", { className: "flex justify-end gap-2", children: [
|
|
1871
|
+
/* @__PURE__ */ jsx11(Button7, { variant: "outline", onClick: () => setAdding(false), children: "Cancel" }),
|
|
1872
|
+
/* @__PURE__ */ jsx11(Button7, { loading: busy, disabled: !EMAIL_RE.test(email.trim()), onClick: add, children: "Add admin" })
|
|
1873
|
+
] }),
|
|
1874
|
+
children: /* @__PURE__ */ jsxs10("div", { className: "space-y-4", children: [
|
|
1875
|
+
/* @__PURE__ */ jsx11("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." }),
|
|
1876
|
+
/* @__PURE__ */ jsx11(Input5, { label: "Email", type: "email", autoFocus: true, value: email, onChange: (e) => setEmail(e.target.value) }),
|
|
1877
|
+
/* @__PURE__ */ jsx11(Select, { label: "Role", options: ROLE_OPTIONS, value: role, onChange: (v) => setRole(String(v)) })
|
|
1878
|
+
] })
|
|
1879
|
+
}
|
|
1880
|
+
),
|
|
1881
|
+
/* @__PURE__ */ jsx11(
|
|
1882
|
+
ConfirmDialog2,
|
|
1883
|
+
{
|
|
1884
|
+
isOpen: confirm !== null,
|
|
1885
|
+
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?`,
|
|
1886
|
+
description: (confirm == null ? void 0 : confirm.action) === "disable" ? "They are signed out everywhere and cannot sign in until enabled again." : "They are signed out and must set up a new authenticator app at their next sign-in.",
|
|
1887
|
+
confirmLabel: (confirm == null ? void 0 : confirm.action) === "disable" ? "Disable" : "Reset",
|
|
1888
|
+
destructive: true,
|
|
1889
|
+
onCancel: () => setConfirm(null),
|
|
1890
|
+
onConfirm: () => {
|
|
1891
|
+
const c = confirm;
|
|
1892
|
+
setConfirm(null);
|
|
1893
|
+
void act(c.admin, c.action);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
)
|
|
1897
|
+
] });
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// src/platform/views/Audit.jsx
|
|
1901
|
+
import React11, { useEffect as useEffect5, useState as useState9 } from "react";
|
|
1902
|
+
import { Alert as Alert5, Button as Button8, EmptyState as EmptyState3, ErrorState as ErrorState5, Input as Input6, LoadingState as LoadingState5, PageHeader as PageHeader6 } from "@dreamtree-org/twreact-ui";
|
|
1903
|
+
import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1904
|
+
function AuditView() {
|
|
1905
|
+
const { client, Link, href } = usePlatform();
|
|
1906
|
+
const [q, setQ] = useState9("");
|
|
1907
|
+
const [applied, setApplied] = useState9("");
|
|
1908
|
+
const [events, setEvents] = useState9([]);
|
|
1909
|
+
const [cursor, setCursor] = useState9(null);
|
|
1910
|
+
const [state, setState] = useState9({ loading: true, error: null });
|
|
1911
|
+
const [exportError, setExportError] = useState9(null);
|
|
1912
|
+
const load = async (more = false) => {
|
|
1913
|
+
setState({ loading: true, error: null });
|
|
1914
|
+
try {
|
|
1915
|
+
const r = await client.get("audit", { q: applied, cursor: more ? cursor : void 0 });
|
|
1916
|
+
setEvents((prev) => more ? [...prev, ...r.events] : r.events);
|
|
1917
|
+
setCursor(r.nextCursor);
|
|
1918
|
+
setState({ loading: false, error: null });
|
|
1919
|
+
} catch (e) {
|
|
1920
|
+
setState({ loading: false, error: e });
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
useEffect5(() => {
|
|
1924
|
+
void load(false);
|
|
1925
|
+
}, [applied]);
|
|
1926
|
+
const exportCsv = async () => {
|
|
1927
|
+
setExportError(null);
|
|
1928
|
+
try {
|
|
1929
|
+
const res = await client.raw("audit/export", { query: { q: applied } });
|
|
1930
|
+
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `Export failed (${res.status})`);
|
|
1931
|
+
const url = URL.createObjectURL(await res.blob());
|
|
1932
|
+
const a = document.createElement("a");
|
|
1933
|
+
a.href = url;
|
|
1934
|
+
a.download = "platform-audit.csv";
|
|
1935
|
+
a.click();
|
|
1936
|
+
URL.revokeObjectURL(url);
|
|
1937
|
+
} catch (e) {
|
|
1938
|
+
setExportError(errorText(e));
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
const rows = events.map(normalizeEvent);
|
|
1942
|
+
return /* @__PURE__ */ jsxs11("div", { className: "mx-auto max-w-6xl space-y-6 px-4 py-6 sm:px-6", children: [
|
|
1943
|
+
/* @__PURE__ */ jsx12(
|
|
1944
|
+
PageHeader6,
|
|
1945
|
+
{
|
|
1946
|
+
title: "Audit log",
|
|
1947
|
+
subtitle: "Every platform action: who did what, to which company",
|
|
1948
|
+
linkAs: Link,
|
|
1949
|
+
actions: /* @__PURE__ */ jsx12(Button8, { variant: "outline", leftIcon: /* @__PURE__ */ jsx12(IconDownload, {}), onClick: exportCsv, children: "Export CSV" })
|
|
1950
|
+
}
|
|
1951
|
+
),
|
|
1952
|
+
exportError && /* @__PURE__ */ jsx12(Alert5, { variant: "error", children: exportError }),
|
|
1953
|
+
/* @__PURE__ */ jsxs11(
|
|
1954
|
+
"form",
|
|
1955
|
+
{
|
|
1956
|
+
className: "flex gap-2",
|
|
1957
|
+
onSubmit: (e) => {
|
|
1958
|
+
e.preventDefault();
|
|
1959
|
+
setApplied(q.trim());
|
|
1960
|
+
},
|
|
1961
|
+
children: [
|
|
1962
|
+
/* @__PURE__ */ jsx12("div", { className: "flex-1", children: /* @__PURE__ */ jsx12(Input6, { type: "search", "aria-label": "Search the audit log", placeholder: "Search action, target or company", value: q, onChange: (e) => setQ(e.target.value) }) }),
|
|
1963
|
+
/* @__PURE__ */ jsx12(Button8, { type: "submit", variant: "outline", children: "Search" })
|
|
1964
|
+
]
|
|
1965
|
+
}
|
|
1966
|
+
),
|
|
1967
|
+
state.error && !rows.length ? /* @__PURE__ */ jsx12(ErrorState5, { description: state.error.message, onRetry: () => load(false) }) : state.loading && !rows.length ? /* @__PURE__ */ jsx12(LoadingState5, { variant: "table", rows: 8 }) : !rows.length ? /* @__PURE__ */ jsx12(EmptyState3, { icon: /* @__PURE__ */ jsx12(IconList, { size: 24 }), title: applied ? "Nothing matches" : "No events yet", compact: true }) : /* @__PURE__ */ jsxs11(Fragment5, { children: [
|
|
1968
|
+
/* @__PURE__ */ jsx12("ol", { className: "divide-y divide-line rounded-xl border border-line bg-surface", children: rows.map((e, i) => {
|
|
1969
|
+
var _a;
|
|
1970
|
+
return /* @__PURE__ */ jsxs11("li", { className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 px-4 py-2.5", children: [
|
|
1971
|
+
/* @__PURE__ */ jsx12("span", { className: "w-40 shrink-0 text-xs text-muted", children: formatDate(e.at) }),
|
|
1972
|
+
/* @__PURE__ */ jsxs11("span", { className: "min-w-0 flex-1 text-sm text-foreground", children: [
|
|
1973
|
+
/* @__PURE__ */ jsx12("span", { className: "font-medium", children: e.actor }),
|
|
1974
|
+
" ",
|
|
1975
|
+
describeAction(e.action),
|
|
1976
|
+
e.company && e.companyId && /* @__PURE__ */ jsxs11(Fragment5, { children: [
|
|
1977
|
+
" \xB7 ",
|
|
1978
|
+
/* @__PURE__ */ jsx12(Link, { href: href(`companies/${e.companyId}`), className: "text-primary-600 hover:underline", children: e.company })
|
|
1979
|
+
] }),
|
|
1980
|
+
e.target && /* @__PURE__ */ jsxs11("span", { className: "text-muted", children: [
|
|
1981
|
+
" \xB7 ",
|
|
1982
|
+
e.target
|
|
1983
|
+
] })
|
|
1984
|
+
] }),
|
|
1985
|
+
/* @__PURE__ */ jsx12("code", { className: "text-xs text-muted", children: e.action })
|
|
1986
|
+
] }, (_a = e.id) != null ? _a : i);
|
|
1987
|
+
}) }),
|
|
1988
|
+
cursor && /* @__PURE__ */ jsx12("div", { className: "text-center", children: /* @__PURE__ */ jsx12(Button8, { variant: "outline", loading: state.loading, onClick: () => load(true), children: "Load older" }) })
|
|
1989
|
+
] })
|
|
1990
|
+
] });
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// src/platform/views/Security.jsx
|
|
1994
|
+
import React12, { useState as useState10 } from "react";
|
|
1995
|
+
import { Alert as Alert6, Button as Button9, Input as Input7, PageHeader as PageHeader7 } from "@dreamtree-org/twreact-ui";
|
|
1996
|
+
import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1997
|
+
var grouped = (s) => String(s != null ? s : "").replace(/(.{4})/g, "$1 ").trim();
|
|
1998
|
+
function SecurityView({ gate = false }) {
|
|
1999
|
+
var _a, _b;
|
|
2000
|
+
const { client, me, reloadMe, Link } = usePlatform();
|
|
2001
|
+
const sensitive = useSensitive();
|
|
2002
|
+
const [setup, setSetup] = useState10(null);
|
|
2003
|
+
const [code, setCode] = useState10("");
|
|
2004
|
+
const [busy, setBusy] = useState10(false);
|
|
2005
|
+
const [error, setError] = useState10(null);
|
|
2006
|
+
const enrolled = (_a = me == null ? void 0 : me.admin) == null ? void 0 : _a.totpEnrolled;
|
|
2007
|
+
const begin = async () => {
|
|
2008
|
+
setBusy(true);
|
|
2009
|
+
setError(null);
|
|
2010
|
+
try {
|
|
2011
|
+
setSetup(await sensitive(() => client.post("me/totp", { action: "begin" })));
|
|
2012
|
+
} catch (e) {
|
|
2013
|
+
setError(errorText(e));
|
|
2014
|
+
} finally {
|
|
2015
|
+
setBusy(false);
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
const verify = async (e) => {
|
|
2019
|
+
e == null ? void 0 : e.preventDefault();
|
|
2020
|
+
setBusy(true);
|
|
2021
|
+
setError(null);
|
|
2022
|
+
try {
|
|
2023
|
+
await sensitive(() => client.post("me/totp", { action: "verify", code: code.trim() }));
|
|
2024
|
+
setSetup(null);
|
|
2025
|
+
setCode("");
|
|
2026
|
+
await reloadMe();
|
|
2027
|
+
} catch (err) {
|
|
2028
|
+
setError(errorText(err));
|
|
2029
|
+
} finally {
|
|
2030
|
+
setBusy(false);
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
2033
|
+
return /* @__PURE__ */ jsxs12("div", { className: "mx-auto max-w-2xl space-y-6 px-4 py-6 sm:px-6", children: [
|
|
2034
|
+
/* @__PURE__ */ jsx13(PageHeader7, { title: gate ? "Set up your authenticator" : "Security", subtitle: (_b = me == null ? void 0 : me.admin) == null ? void 0 : _b.email, linkAs: Link }),
|
|
2035
|
+
gate && /* @__PURE__ */ jsx13(Alert6, { 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." }),
|
|
2036
|
+
error && /* @__PURE__ */ jsx13(Alert6, { variant: "error", children: error }),
|
|
2037
|
+
/* @__PURE__ */ jsx13(Card, { title: "Authenticator app", children: enrolled ? /* @__PURE__ */ jsxs12("p", { className: "flex items-center gap-2 text-sm text-success-700", children: [
|
|
2038
|
+
/* @__PURE__ */ jsx13(IconCheck, {}),
|
|
2039
|
+
" 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."
|
|
2040
|
+
] }) : !setup ? /* @__PURE__ */ jsxs12("div", { className: "space-y-3", children: [
|
|
2041
|
+
/* @__PURE__ */ jsx13("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." }),
|
|
2042
|
+
/* @__PURE__ */ jsx13(Button9, { leftIcon: /* @__PURE__ */ jsx13(IconShield, {}), loading: busy, onClick: begin, children: "Start setup" })
|
|
2043
|
+
] }) : /* @__PURE__ */ jsxs12("form", { className: "space-y-4", onSubmit: verify, children: [
|
|
2044
|
+
/* @__PURE__ */ jsxs12("ol", { className: "list-decimal space-y-3 pl-5 text-sm text-foreground", children: [
|
|
2045
|
+
/* @__PURE__ */ jsxs12("li", { children: [
|
|
2046
|
+
"Add an account in your authenticator app with this setup key:",
|
|
2047
|
+
/* @__PURE__ */ jsxs12("div", { className: "mt-2 flex flex-wrap items-center gap-2", children: [
|
|
2048
|
+
/* @__PURE__ */ jsx13("code", { className: "rounded-md bg-background px-2 py-1 font-mono text-sm tracking-wider", children: grouped(setup.secret) }),
|
|
2049
|
+
/* @__PURE__ */ jsx13(CopyButton, { value: setup.secret, label: "Copy key" }),
|
|
2050
|
+
/* @__PURE__ */ jsx13("a", { href: setup.otpauthUrl, className: "text-xs text-primary-600 underline", children: "Open in authenticator app" })
|
|
2051
|
+
] })
|
|
2052
|
+
] }),
|
|
2053
|
+
/* @__PURE__ */ jsx13("li", { children: "Enter the 6-digit code it shows." })
|
|
2054
|
+
] }),
|
|
2055
|
+
/* @__PURE__ */ jsx13("div", { className: "max-w-xs", children: /* @__PURE__ */ jsx13(Input7, { label: "Code", otp: true, autoFocus: true, value: code, onChange: (e) => setCode(e.target.value.replace(/\s/g, "")) }) }),
|
|
2056
|
+
/* @__PURE__ */ jsx13(Button9, { type: "submit", loading: busy, disabled: code.length < 6, children: "Verify and finish" })
|
|
2057
|
+
] }) })
|
|
2058
|
+
] });
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// src/platform/PlatformConsole.jsx
|
|
2062
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
2063
|
+
var trim = (s) => String(s != null ? s : "").replace(/^\/+|\/+$/g, "");
|
|
2064
|
+
function matchConsoleRoute(rel, pages = []) {
|
|
2065
|
+
const parts = trim(rel).split("/").filter(Boolean);
|
|
2066
|
+
if (parts.length === 0) return { view: "overview" };
|
|
2067
|
+
const page = pages.find((p) => trim(rel) === trim(p.path) || trim(rel).startsWith(`${trim(p.path)}/`));
|
|
2068
|
+
if (page) return { view: "page", page, rest: trim(rel).slice(trim(page.path).length + 1) };
|
|
2069
|
+
if (parts[0] === "companies") {
|
|
2070
|
+
if (parts.length === 1) return { view: "companies" };
|
|
2071
|
+
if (parts[1] === "new" && parts.length === 2) return { view: "new-company" };
|
|
2072
|
+
if (parts.length === 2) return { view: "company", id: decodeURIComponent(parts[1]) };
|
|
2073
|
+
}
|
|
2074
|
+
if (parts.length === 1 && ["admins", "audit", "security"].includes(parts[0])) return { view: parts[0] };
|
|
2075
|
+
return { view: "not-found" };
|
|
2076
|
+
}
|
|
2077
|
+
function PlatformConsole({
|
|
2078
|
+
brand,
|
|
2079
|
+
appName,
|
|
2080
|
+
basePath = "/platform",
|
|
2081
|
+
apiBasePath,
|
|
2082
|
+
client: given,
|
|
2083
|
+
pathname: givenPath,
|
|
2084
|
+
navigate: givenNavigate,
|
|
2085
|
+
linkAs,
|
|
2086
|
+
features = PLATFORM_FEATURES,
|
|
2087
|
+
pages = [],
|
|
2088
|
+
companyTabs = [],
|
|
2089
|
+
companyLoginPath: loginPathOpt,
|
|
2090
|
+
adminEmail,
|
|
2091
|
+
initialMe,
|
|
2092
|
+
loginPath,
|
|
2093
|
+
onSignOut
|
|
2094
|
+
}) {
|
|
2095
|
+
var _a, _b, _c, _d, _e;
|
|
2096
|
+
const routed = givenPath !== void 0;
|
|
2097
|
+
const browser = useBrowserLocation(!routed);
|
|
2098
|
+
const pathname = routed ? givenPath : browser.pathname;
|
|
2099
|
+
const navigate = givenNavigate != null ? givenNavigate : browser.navigate;
|
|
2100
|
+
const base2 = `/${trim(basePath)}`.replace(/\/$/, "") || "";
|
|
2101
|
+
const client = useMemo3(
|
|
2102
|
+
() => given != null ? given : createPlatformClient({ basePath: apiBasePath, loginPath: loginPath != null ? loginPath : `${base2}/login` }),
|
|
2103
|
+
[given, apiBasePath, loginPath, base2]
|
|
2104
|
+
);
|
|
2105
|
+
const Link = useMemo3(() => makeLink(linkAs, navigate), [linkAs, navigate]);
|
|
2106
|
+
const [me, setMe] = useState11(initialMe);
|
|
2107
|
+
const reloadMe = useCallback4(async () => {
|
|
2108
|
+
try {
|
|
2109
|
+
setMe(await client.get("me"));
|
|
2110
|
+
} catch (e) {
|
|
2111
|
+
if (e.status !== 401) setMe(null);
|
|
2112
|
+
}
|
|
2113
|
+
}, [client]);
|
|
2114
|
+
useEffect6(() => {
|
|
2115
|
+
void reloadMe();
|
|
2116
|
+
}, [reloadMe]);
|
|
2117
|
+
const href = useCallback4((rel2 = "") => `${base2}/${trim(rel2)}`.replace(/\/$/, "") || "/", [base2]);
|
|
2118
|
+
const companyLoginPath = useCallback4(
|
|
2119
|
+
(slug) => {
|
|
2120
|
+
var _a2, _b2;
|
|
2121
|
+
const pattern = (_b2 = loginPathOpt != null ? loginPathOpt : (_a2 = me == null ? void 0 : me.app) == null ? void 0 : _a2.companyLoginPath) != null ? _b2 : "/c/:slug/login";
|
|
2122
|
+
return typeof pattern === "function" ? pattern(slug) : pattern.replace(":slug", encodeURIComponent(slug != null ? slug : ""));
|
|
2123
|
+
},
|
|
2124
|
+
[loginPathOpt, me]
|
|
2125
|
+
);
|
|
2126
|
+
const origin = typeof window === "undefined" ? "" : window.location.origin;
|
|
2127
|
+
const can = useCallback4((perm) => (me == null ? void 0 : me.permissions) ? me.permissions.includes(perm) : true, [me]);
|
|
2128
|
+
const enabled = enabledFeatures(features, me);
|
|
2129
|
+
const visiblePages = pages.filter((p) => (!p.feature || enabled.includes(p.feature)) && (!p.permission || can(p.permission)));
|
|
2130
|
+
const signOut = async () => {
|
|
2131
|
+
try {
|
|
2132
|
+
await client.post("auth/logout", {}, { skipAuthRedirect: true });
|
|
2133
|
+
} finally {
|
|
2134
|
+
if (onSignOut) onSignOut();
|
|
2135
|
+
else if (typeof window !== "undefined") window.location.assign(loginPath != null ? loginPath : `${base2}/login`);
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
const rel = pathname.startsWith(base2) ? pathname.slice(base2.length) : pathname;
|
|
2139
|
+
const route = matchConsoleRoute(rel, visiblePages);
|
|
2140
|
+
const gate = me && me.access && me.access.console === false;
|
|
2141
|
+
const active = (prefix) => prefix === "" ? route.view === "overview" : trim(rel) === prefix || trim(rel).startsWith(`${prefix}/`);
|
|
2142
|
+
const nav = gate ? [{ id: "security", label: "Set up authenticator", href: href("security"), icon: /* @__PURE__ */ jsx14(IconShield, {}), active: true }] : [
|
|
2143
|
+
enabled.includes("overview") && { id: "overview", label: "Overview", href: href(""), icon: /* @__PURE__ */ jsx14(IconDashboard, {}), active: active("") },
|
|
2144
|
+
enabled.includes("companies") && { id: "companies", label: "Companies", href: href("companies"), icon: /* @__PURE__ */ jsx14(IconBuilding, {}), active: active("companies") },
|
|
2145
|
+
...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)) })),
|
|
2146
|
+
enabled.includes("audit") && can("audit.read") && { id: "audit", label: "Audit log", href: href("audit"), icon: /* @__PURE__ */ jsx14(IconList, {}), active: active("audit"), group: "Platform" },
|
|
2147
|
+
enabled.includes("admins") && can("admins.read") && { id: "admins", label: "Platform admins", href: href("admins"), icon: /* @__PURE__ */ jsx14(IconUsers, {}), active: active("admins"), group: "Platform" },
|
|
2148
|
+
{ id: "security", label: "Security", href: href("security"), icon: /* @__PURE__ */ jsx14(IconShield, {}), active: active("security"), group: "Platform" }
|
|
2149
|
+
].filter(Boolean);
|
|
2150
|
+
const value = {
|
|
2151
|
+
client,
|
|
2152
|
+
me,
|
|
2153
|
+
reloadMe,
|
|
2154
|
+
Link,
|
|
2155
|
+
href,
|
|
2156
|
+
navigate,
|
|
2157
|
+
can,
|
|
2158
|
+
companyTabs,
|
|
2159
|
+
companyLoginPath,
|
|
2160
|
+
companyLoginUrl: (slug) => `${origin}${companyLoginPath(slug)}`,
|
|
2161
|
+
loginUrl: `${origin}${loginPath != null ? loginPath : `${base2}/login`}`,
|
|
2162
|
+
brand,
|
|
2163
|
+
appName: appName != null ? appName : (_a = me == null ? void 0 : me.app) == null ? void 0 : _a.name
|
|
2164
|
+
};
|
|
2165
|
+
let body;
|
|
2166
|
+
if (me === void 0) body = /* @__PURE__ */ jsx14("div", { className: "p-6", children: /* @__PURE__ */ jsx14(LoadingState6, { variant: "stats" }) });
|
|
2167
|
+
else if (gate) body = /* @__PURE__ */ jsx14(SecurityView, { gate: true });
|
|
2168
|
+
else if (route.view === "overview") body = /* @__PURE__ */ jsx14(OverviewView, {});
|
|
2169
|
+
else if (route.view === "companies") body = /* @__PURE__ */ jsx14(CompaniesView, {});
|
|
2170
|
+
else if (route.view === "new-company") body = /* @__PURE__ */ jsx14(NewCompanyView, {});
|
|
2171
|
+
else if (route.view === "company") body = /* @__PURE__ */ jsx14(CompanyDetailView, { companyId: route.id }, route.id);
|
|
2172
|
+
else if (route.view === "admins") body = /* @__PURE__ */ jsx14(AdminsView, {});
|
|
2173
|
+
else if (route.view === "audit") body = /* @__PURE__ */ jsx14(AuditView, {});
|
|
2174
|
+
else if (route.view === "security") body = /* @__PURE__ */ jsx14(SecurityView, {});
|
|
2175
|
+
else if (route.view === "page") body = route.page.render({ client, path: route.rest, navigate, href, me });
|
|
2176
|
+
else body = /* @__PURE__ */ jsx14("div", { className: "p-6", children: /* @__PURE__ */ jsx14(EmptyState4, { title: "Page not found", description: "This console page does not exist." }) });
|
|
2177
|
+
return /* @__PURE__ */ jsx14(PlatformProvider, { value, children: /* @__PURE__ */ jsx14(StepUpProvider, { client, totpEnrolled: Boolean((_b = me == null ? void 0 : me.admin) == null ? void 0 : _b.totpEnrolled), children: /* @__PURE__ */ jsx14(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 }) }) });
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
// src/platform/PlatformLogin.jsx
|
|
2181
|
+
import React14, { useMemo as useMemo4, useState as useState14 } from "react";
|
|
2182
|
+
import { Alert as Alert7, Button as Button10, Input as Input8 } from "@dreamtree-org/twreact-ui";
|
|
2183
|
+
|
|
2184
|
+
// src/AuthShell.jsx
|
|
2185
|
+
import { useId } from "react";
|
|
2186
|
+
|
|
2187
|
+
// src/DemoAccounts.jsx
|
|
2188
|
+
import { useEffect as useEffect7, useRef as useRef3, useState as useState12 } from "react";
|
|
2189
|
+
|
|
2190
|
+
// src/demoFill.js
|
|
2191
|
+
function normalizeDemoAccounts(data) {
|
|
2192
|
+
const list = Array.isArray(data) ? data : Array.isArray(data == null ? void 0 : data.accounts) ? data.accounts : [];
|
|
2193
|
+
const accounts = list.filter((a) => a && typeof a.label === "string" && typeof a.identifier === "string");
|
|
2194
|
+
let sharedPassword = typeof (data == null ? void 0 : data.sharedPassword) === "string" ? data.sharedPassword : void 0;
|
|
2195
|
+
if (!sharedPassword && accounts.length > 1 && accounts.every((a) => a.password && a.password === accounts[0].password)) {
|
|
2196
|
+
sharedPassword = accounts[0].password;
|
|
2197
|
+
}
|
|
2198
|
+
return sharedPassword ? { accounts, sharedPassword } : { accounts };
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// src/DemoAccounts.jsx
|
|
2202
|
+
import { Fragment as Fragment6, jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2203
|
+
function DemoAccounts({ load, onSelect, className = "" }) {
|
|
2204
|
+
const [state, setState] = useState12({ status: "loading", accounts: [] });
|
|
2205
|
+
const [picked, setPicked] = useState12(null);
|
|
2206
|
+
const loadRef = useRef3(load);
|
|
2207
|
+
useEffect7(() => {
|
|
2208
|
+
let live = true;
|
|
2209
|
+
Promise.resolve().then(() => loadRef.current()).then((data) => live && setState({ status: "ready", ...normalizeDemoAccounts(data) })).catch(() => live && setState({ status: "ready", accounts: [] }));
|
|
2210
|
+
return () => {
|
|
2211
|
+
live = false;
|
|
2212
|
+
};
|
|
2213
|
+
}, []);
|
|
2214
|
+
if (state.status === "ready" && !state.accounts.length) return null;
|
|
2215
|
+
const pick = (account, i) => {
|
|
2216
|
+
setPicked(i);
|
|
2217
|
+
onSelect == null ? void 0 : onSelect(account);
|
|
2218
|
+
};
|
|
2219
|
+
return /* @__PURE__ */ jsxs13("div", { className: `dt-auth-demo ${className}`, "aria-busy": state.status === "loading" || void 0, children: [
|
|
2220
|
+
/* @__PURE__ */ jsx15("p", { className: "dt-auth-demo-label", children: "Demo accounts" }),
|
|
2221
|
+
state.status === "loading" ? /* @__PURE__ */ jsx15("div", { className: "dt-auth-demo-chips", "aria-hidden": "true", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx15("span", { className: "dt-auth-demo-chip dt-auth-demo-chip--skeleton" }, i)) }) : /* @__PURE__ */ jsxs13(Fragment6, { children: [
|
|
2222
|
+
/* @__PURE__ */ jsx15("div", { className: "dt-auth-demo-chips", children: state.accounts.map((a, i) => /* @__PURE__ */ jsx15(
|
|
2223
|
+
"button",
|
|
2224
|
+
{
|
|
2225
|
+
type: "button",
|
|
2226
|
+
className: "dt-auth-demo-chip",
|
|
2227
|
+
"aria-pressed": picked === i,
|
|
2228
|
+
title: a.company ? `${a.identifier} \xB7 ${a.company}` : a.identifier,
|
|
2229
|
+
onClick: () => pick(a, i),
|
|
2230
|
+
children: a.label
|
|
2231
|
+
},
|
|
2232
|
+
`${a.label}-${a.identifier}`
|
|
2233
|
+
)) }),
|
|
2234
|
+
/* @__PURE__ */ jsx15("p", { className: "dt-auth-demo-note", children: state.sharedPassword ? /* @__PURE__ */ jsxs13(Fragment6, { children: [
|
|
2235
|
+
"Shared password ",
|
|
2236
|
+
/* @__PURE__ */ jsx15("code", { children: state.sharedPassword }),
|
|
2237
|
+
" \u2014 tap a role to fill."
|
|
2238
|
+
] }) : "Tap a role to fill." })
|
|
2239
|
+
] })
|
|
2240
|
+
] });
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
// src/useTapReveal.js
|
|
2244
|
+
import { useCallback as useCallback5, useRef as useRef4, useState as useState13 } from "react";
|
|
2245
|
+
function useTapReveal({ taps = 4, windowMs = 1500 } = {}) {
|
|
2246
|
+
const [revealed, setRevealed] = useState13(false);
|
|
2247
|
+
const count = useRef4(0);
|
|
2248
|
+
const last = useRef4(0);
|
|
2249
|
+
const onClick = useCallback5(() => {
|
|
2250
|
+
if (revealed) return;
|
|
2251
|
+
const now = Date.now();
|
|
2252
|
+
count.current = now - last.current <= windowMs ? count.current + 1 : 1;
|
|
2253
|
+
last.current = now;
|
|
2254
|
+
if (count.current >= taps) setRevealed(true);
|
|
2255
|
+
}, [revealed, taps, windowMs]);
|
|
2256
|
+
return { revealed, tapProps: { onClick, style: { userSelect: "none", WebkitUserSelect: "none" } } };
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
// src/AuthShell.jsx
|
|
2260
|
+
import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2261
|
+
var DEFAULT_LEGAL = [
|
|
2262
|
+
{ label: "Privacy Policy", href: "/privacy" },
|
|
2263
|
+
{ label: "Terms", href: "/terms" },
|
|
2264
|
+
{ label: "Data Policy", href: "/data-policy" }
|
|
2265
|
+
];
|
|
2266
|
+
function BrandMark({ brand, size }) {
|
|
2267
|
+
if (brand.logoSrc) {
|
|
2268
|
+
return /* @__PURE__ */ jsx16("img", { className: "dt-auth-mark-img", src: brand.logoSrc, alt: "", width: size, height: size });
|
|
2269
|
+
}
|
|
2270
|
+
if (brand.mark) return /* @__PURE__ */ jsx16("span", { className: "dt-auth-mark-node", children: brand.mark });
|
|
2271
|
+
return /* @__PURE__ */ jsx16("span", { className: "dt-auth-monogram", style: { width: size, height: size }, "aria-hidden": "true", children: brand.name.charAt(0) });
|
|
2272
|
+
}
|
|
2273
|
+
function brandStyle(brand) {
|
|
2274
|
+
return {
|
|
2275
|
+
"--dt-brand": brand.primary,
|
|
2276
|
+
"--dt-brand-accent": brand.accent || brand.primary,
|
|
2277
|
+
...brand.panel ? { "--dt-brand-panel": brand.panel } : {}
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
function AuthShell({
|
|
2281
|
+
brand,
|
|
2282
|
+
title,
|
|
2283
|
+
subtitle,
|
|
2284
|
+
children,
|
|
2285
|
+
footer,
|
|
2286
|
+
variant = "tenant",
|
|
2287
|
+
legalLinks = DEFAULT_LEGAL,
|
|
2288
|
+
linkComponent: LinkComponent,
|
|
2289
|
+
className = "",
|
|
2290
|
+
demoAccounts
|
|
2291
|
+
}) {
|
|
2292
|
+
var _a, _b;
|
|
2293
|
+
const headingId = useId();
|
|
2294
|
+
const { revealed, tapProps } = useTapReveal({ taps: (_a = demoAccounts == null ? void 0 : demoAccounts.taps) != null ? _a : 4 });
|
|
2295
|
+
const isPlatform = variant === "platform";
|
|
2296
|
+
const headline = isPlatform ? brand.platformHeadline || "Platform administration" : brand.headline;
|
|
2297
|
+
const pitch = isPlatform ? brand.platformPitch || `Super administrator console for ${brand.name}. Every action here is audited.` : brand.pitch;
|
|
2298
|
+
const renderLink = (l) => LinkComponent ? /* @__PURE__ */ jsx16(LinkComponent, { to: l.href, href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href) : /* @__PURE__ */ jsx16("a", { href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href);
|
|
2299
|
+
return /* @__PURE__ */ jsxs14(
|
|
2300
|
+
"div",
|
|
2301
|
+
{
|
|
2302
|
+
className: `dt-auth ${isPlatform ? "dt-auth--platform" : ""} ${className}`,
|
|
2303
|
+
style: brandStyle(brand),
|
|
2304
|
+
"data-product": brand.id,
|
|
2305
|
+
children: [
|
|
2306
|
+
/* @__PURE__ */ jsxs14("aside", { className: "dt-auth-panel", children: [
|
|
2307
|
+
/* @__PURE__ */ jsxs14("a", { href: "/", className: "dt-auth-brand", children: [
|
|
2308
|
+
/* @__PURE__ */ jsx16(BrandMark, { brand, size: 28 }),
|
|
2309
|
+
/* @__PURE__ */ jsx16("span", { className: "dt-auth-brand-name", children: brand.name }),
|
|
2310
|
+
isPlatform ? /* @__PURE__ */ jsx16("span", { className: "dt-auth-badge", children: "Platform" }) : null
|
|
2311
|
+
] }),
|
|
2312
|
+
/* @__PURE__ */ jsxs14("div", { className: "dt-auth-pitch", children: [
|
|
2313
|
+
/* @__PURE__ */ jsx16("h2", { children: headline }),
|
|
2314
|
+
pitch ? /* @__PURE__ */ jsx16("p", { children: pitch }) : null,
|
|
2315
|
+
!isPlatform && ((_b = brand.points) == null ? void 0 : _b.length) ? /* @__PURE__ */ jsx16("ul", { children: brand.points.map((p) => /* @__PURE__ */ jsx16("li", { children: p }, p)) }) : null
|
|
2316
|
+
] }),
|
|
2317
|
+
/* @__PURE__ */ jsx16("p", { className: "dt-auth-panel-foot", children: brand.category || "by Dreamtree-Org" })
|
|
2318
|
+
] }),
|
|
2319
|
+
/* @__PURE__ */ jsx16("main", { className: "dt-auth-main", children: /* @__PURE__ */ jsxs14("div", { className: "dt-auth-column", children: [
|
|
2320
|
+
/* @__PURE__ */ jsxs14("a", { href: "/", className: "dt-auth-brand dt-auth-brand--mobile", children: [
|
|
2321
|
+
/* @__PURE__ */ jsx16(BrandMark, { brand, size: 24 }),
|
|
2322
|
+
/* @__PURE__ */ jsx16("span", { className: "dt-auth-brand-name", children: brand.name }),
|
|
2323
|
+
isPlatform ? /* @__PURE__ */ jsx16("span", { className: "dt-auth-badge", children: "Platform" }) : null
|
|
2324
|
+
] }),
|
|
2325
|
+
/* @__PURE__ */ jsxs14("section", { className: "dt-auth-card", "aria-labelledby": headingId, children: [
|
|
2326
|
+
/* @__PURE__ */ jsxs14("header", { className: "dt-auth-head", children: [
|
|
2327
|
+
/* @__PURE__ */ jsx16("h1", { id: headingId, ...demoAccounts ? tapProps : {}, children: title }),
|
|
2328
|
+
subtitle ? /* @__PURE__ */ jsx16("p", { children: subtitle }) : null
|
|
2329
|
+
] }),
|
|
2330
|
+
children,
|
|
2331
|
+
demoAccounts && revealed ? /* @__PURE__ */ jsx16(DemoAccounts, { load: demoAccounts.load, onSelect: demoAccounts.onSelect }) : null
|
|
2332
|
+
] }),
|
|
2333
|
+
footer ? /* @__PURE__ */ jsx16("div", { className: "dt-auth-footer", children: footer }) : null,
|
|
2334
|
+
(legalLinks == null ? void 0 : legalLinks.length) ? /* @__PURE__ */ jsx16("nav", { className: "dt-auth-legal", "aria-label": "Legal", children: legalLinks.map((l, i) => /* @__PURE__ */ jsxs14("span", { className: "dt-auth-legal-item", children: [
|
|
2335
|
+
i > 0 ? /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", children: "\xB7" }) : null,
|
|
2336
|
+
renderLink(l)
|
|
2337
|
+
] }, l.href)) }) : null
|
|
2338
|
+
] }) })
|
|
2339
|
+
]
|
|
2340
|
+
}
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
// src/platform/PlatformLogin.jsx
|
|
2345
|
+
import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
2346
|
+
function PlatformLogin({ brand, basePath, client: given, onSignedIn, redirectTo = "/platform", title = "Platform administration", subtitle = "Administrator sign-in", linkComponent, legalLinks }) {
|
|
2347
|
+
const client = useMemo4(() => given != null ? given : createPlatformClient({ basePath, onUnauthorized: () => {
|
|
2348
|
+
} }), [given, basePath]);
|
|
2349
|
+
const [step, setStep] = useState14("password");
|
|
2350
|
+
const [email, setEmail] = useState14("");
|
|
2351
|
+
const [password, setPassword] = useState14("");
|
|
2352
|
+
const [ticket, setTicket] = useState14(null);
|
|
2353
|
+
const [code, setCode] = useState14("");
|
|
2354
|
+
const [busy, setBusy] = useState14(false);
|
|
2355
|
+
const [error, setError] = useState14(null);
|
|
2356
|
+
const done = (res) => {
|
|
2357
|
+
if (onSignedIn) onSignedIn(res);
|
|
2358
|
+
else if (typeof window !== "undefined") window.location.assign(redirectTo);
|
|
2359
|
+
};
|
|
2360
|
+
const submitPassword = async (e) => {
|
|
2361
|
+
e.preventDefault();
|
|
2362
|
+
setBusy(true);
|
|
2363
|
+
setError(null);
|
|
2364
|
+
try {
|
|
2365
|
+
const res = await client.post("auth/login", { email: email.trim(), password }, { skipAuthRedirect: true });
|
|
2366
|
+
setPassword("");
|
|
2367
|
+
if (res.mfa === "required") {
|
|
2368
|
+
setTicket(res.ticket);
|
|
2369
|
+
setStep("code");
|
|
2370
|
+
} else {
|
|
2371
|
+
done(res);
|
|
2372
|
+
}
|
|
2373
|
+
} catch (err) {
|
|
2374
|
+
setError(err.message);
|
|
2375
|
+
} finally {
|
|
2376
|
+
setBusy(false);
|
|
2377
|
+
}
|
|
2378
|
+
};
|
|
2379
|
+
const submitCode = async (e) => {
|
|
2380
|
+
e.preventDefault();
|
|
2381
|
+
setBusy(true);
|
|
2382
|
+
setError(null);
|
|
2383
|
+
try {
|
|
2384
|
+
done(await client.post("auth/totp", { ticket, code: code.trim() }, { skipAuthRedirect: true }));
|
|
2385
|
+
} catch (err) {
|
|
2386
|
+
if (err.code === "RESTART") {
|
|
2387
|
+
setStep("password");
|
|
2388
|
+
setTicket(null);
|
|
2389
|
+
}
|
|
2390
|
+
setError(err.message);
|
|
2391
|
+
} finally {
|
|
2392
|
+
setBusy(false);
|
|
2393
|
+
}
|
|
2394
|
+
};
|
|
2395
|
+
return /* @__PURE__ */ jsx17(AuthShell, { brand, variant: "platform", title, subtitle, linkComponent, legalLinks, children: step === "password" ? /* @__PURE__ */ jsxs15("form", { onSubmit: submitPassword, className: "flex flex-col gap-4", children: [
|
|
2396
|
+
/* @__PURE__ */ jsx17(Input8, { label: "Admin email", type: "email", autoComplete: "username", autoFocus: true, value: email, onChange: (e) => setEmail(e.target.value) }),
|
|
2397
|
+
/* @__PURE__ */ jsx17(Input8, { label: "Password", type: "password", autoComplete: "current-password", value: password, onChange: (e) => setPassword(e.target.value) }),
|
|
2398
|
+
error && /* @__PURE__ */ jsx17(Alert7, { variant: "error", children: error }),
|
|
2399
|
+
/* @__PURE__ */ jsx17(Button10, { type: "submit", loading: busy, disabled: !email || !password, fullWidth: true, children: "Sign in" })
|
|
2400
|
+
] }) : /* @__PURE__ */ jsxs15("form", { onSubmit: submitCode, className: "flex flex-col gap-4", children: [
|
|
2401
|
+
/* @__PURE__ */ jsx17("p", { className: "text-sm", children: "Enter the 6-digit code from your authenticator app." }),
|
|
2402
|
+
/* @__PURE__ */ jsx17(Input8, { label: "Authenticator code", otp: true, autoFocus: true, value: code, onChange: (e) => setCode(e.target.value.replace(/\s/g, "")) }),
|
|
2403
|
+
error && /* @__PURE__ */ jsx17(Alert7, { variant: "error", children: error }),
|
|
2404
|
+
/* @__PURE__ */ jsx17(Button10, { type: "submit", loading: busy, disabled: code.length < 6, fullWidth: true, children: "Verify" }),
|
|
2405
|
+
/* @__PURE__ */ jsx17(
|
|
2406
|
+
Button10,
|
|
2407
|
+
{
|
|
2408
|
+
variant: "ghost",
|
|
2409
|
+
onClick: () => {
|
|
2410
|
+
setStep("password");
|
|
2411
|
+
setCode("");
|
|
2412
|
+
setError(null);
|
|
2413
|
+
},
|
|
2414
|
+
children: "Use a different account"
|
|
2415
|
+
}
|
|
2416
|
+
)
|
|
2417
|
+
] }) });
|
|
2418
|
+
}
|
|
2419
|
+
export {
|
|
2420
|
+
Card,
|
|
2421
|
+
CopyButton,
|
|
2422
|
+
Facts,
|
|
2423
|
+
HealthDot,
|
|
2424
|
+
InspectResult,
|
|
2425
|
+
ModePill,
|
|
2426
|
+
OneTimeSecret,
|
|
2427
|
+
PLATFORM_API_BASE,
|
|
2428
|
+
PLATFORM_FEATURES,
|
|
2429
|
+
PLATFORM_LOGIN_PATH,
|
|
2430
|
+
PlatformApiError,
|
|
2431
|
+
PlatformConsole,
|
|
2432
|
+
PlatformLogin,
|
|
2433
|
+
PlatformShell,
|
|
2434
|
+
SHIPPED_FEATURES,
|
|
2435
|
+
STEP_UP_REQUIRED,
|
|
2436
|
+
StatusPill,
|
|
2437
|
+
StepUpProvider,
|
|
2438
|
+
createPlatformClient,
|
|
2439
|
+
createStepUp,
|
|
2440
|
+
describeAction,
|
|
2441
|
+
enabledFeatures,
|
|
2442
|
+
formatDate,
|
|
2443
|
+
isStepUpRequired,
|
|
2444
|
+
matchConsoleRoute,
|
|
2445
|
+
readPlatformCsrf,
|
|
2446
|
+
timeAgo,
|
|
2447
|
+
useLoad,
|
|
2448
|
+
usePlatform,
|
|
2449
|
+
useSensitive
|
|
2450
|
+
};
|