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