@fonderie/vue-admin-screens 1.1.0 → 1.3.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/dist/index.cjs +845 -404
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +111 -4
- package/dist/index.d.ts +111 -4
- package/dist/index.js +830 -391
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/screens/AdminShell.ts
|
|
2
2
|
import { ConfigEditorScreen, ConfigListScreen } from "@fonderie/vue-config-admin-screens";
|
|
3
3
|
import { TemplateEditorScreen, TemplateListScreen } from "@fonderie/vue-courier-admin-screens";
|
|
4
|
-
import { computed as
|
|
4
|
+
import { computed as computed3, defineComponent as defineComponent15, h as h17, onBeforeUnmount, onMounted, ref as ref9 } from "vue";
|
|
5
5
|
|
|
6
6
|
// src/styles.ts
|
|
7
7
|
var t = {
|
|
@@ -557,6 +557,12 @@ function empty(title, body, iconName = "check") {
|
|
|
557
557
|
body ? h("div", body) : null
|
|
558
558
|
]);
|
|
559
559
|
}
|
|
560
|
+
function refreshButton(onClick, busy, label = "Refresh") {
|
|
561
|
+
return h("button", { type: "button", style: styles.button, onClick, disabled: busy }, [
|
|
562
|
+
icon("refresh", 14),
|
|
563
|
+
busy ? "Working\u2026" : label
|
|
564
|
+
]);
|
|
565
|
+
}
|
|
560
566
|
function methodChip(method) {
|
|
561
567
|
const tone = method === "GET" ? "ok" : method === "DELETE" ? "bad" : method === "POST" ? "info" : "warn";
|
|
562
568
|
const color = toneColor[tone];
|
|
@@ -608,7 +614,7 @@ function table(headers, rows) {
|
|
|
608
614
|
]);
|
|
609
615
|
}
|
|
610
616
|
var td = (children, extra = {}) => h2("td", { style: { ...styles.td, ...extra } }, children);
|
|
611
|
-
var
|
|
617
|
+
var refreshButton2 = (label, disabled, onClick) => h2("button", { type: "button", style: styles.button, disabled, onClick }, [
|
|
612
618
|
icon("refresh", 14),
|
|
613
619
|
disabled ? "Working\u2026" : label
|
|
614
620
|
]);
|
|
@@ -634,7 +640,7 @@ var AdminLogScreen = defineComponent({
|
|
|
634
640
|
pageHeader(
|
|
635
641
|
"Admin log",
|
|
636
642
|
"Every request made through this surface, newest first \u2014 refused ones included.",
|
|
637
|
-
[
|
|
643
|
+
[refreshButton2("Refresh", isLoading.value, () => void refresh())]
|
|
638
644
|
),
|
|
639
645
|
error.value ? h3(
|
|
640
646
|
"p",
|
|
@@ -775,7 +781,7 @@ var AttentionScreen = defineComponent2({
|
|
|
775
781
|
)
|
|
776
782
|
];
|
|
777
783
|
},
|
|
778
|
-
[
|
|
784
|
+
[refreshButton2("Refresh", isLoading.value, () => void refresh())],
|
|
779
785
|
{
|
|
780
786
|
lead: a ? `What needs you on this deployment \xB7 checked ${new Date(a.generatedAt).toLocaleTimeString()}` : "What needs you on this deployment"
|
|
781
787
|
}
|
|
@@ -955,7 +961,7 @@ var DoctorScreen = defineComponent4({
|
|
|
955
961
|
report.value.ok ? "ok" : "bad",
|
|
956
962
|
report.value.ok ? "all checks pass" : "a check failed"
|
|
957
963
|
) : null,
|
|
958
|
-
|
|
964
|
+
refreshButton2("Run again", isLoading.value, () => void refresh())
|
|
959
965
|
],
|
|
960
966
|
{
|
|
961
967
|
loadingText: "Running the checks\u2026",
|
|
@@ -1158,7 +1164,7 @@ var MigrationsScreen = defineComponent7({
|
|
|
1158
1164
|
...behind.map((m) => section(m, r.everApplied))
|
|
1159
1165
|
].filter(Boolean);
|
|
1160
1166
|
},
|
|
1161
|
-
[
|
|
1167
|
+
[refreshButton2("Refresh", isLoading.value, () => void refresh())],
|
|
1162
1168
|
{
|
|
1163
1169
|
lead: "Schema changes each module ships, and whether this database has them.",
|
|
1164
1170
|
errorText: (e) => e.status === 403 ? "Applying migrations needs a token with the write scope." : e.explanation
|
|
@@ -1167,42 +1173,319 @@ var MigrationsScreen = defineComponent7({
|
|
|
1167
1173
|
}
|
|
1168
1174
|
});
|
|
1169
1175
|
|
|
1176
|
+
// src/screens/OperatorsScreen.ts
|
|
1177
|
+
import { useAdminOperators } from "@fonderie/vue-admin";
|
|
1178
|
+
import { defineComponent as defineComponent8, h as h10, ref as ref2 } from "vue";
|
|
1179
|
+
var LEVELS = [
|
|
1180
|
+
{ label: "Read only", scopes: ["read"], hint: "look around, change nothing" },
|
|
1181
|
+
{ label: "Editor", scopes: ["read", "write"], hint: "change config, templates, users" },
|
|
1182
|
+
{
|
|
1183
|
+
label: "Owner",
|
|
1184
|
+
scopes: ["read", "write", "secrets"],
|
|
1185
|
+
hint: "secrets, tokens and operators too"
|
|
1186
|
+
}
|
|
1187
|
+
];
|
|
1188
|
+
var levelOf = (scopes) => scopes.includes("secrets") ? "Owner" : scopes.includes("write") ? "Editor" : "Read only";
|
|
1189
|
+
var scopesFor = (label) => LEVELS.find((l) => l.label === label)?.scopes ?? ["read"];
|
|
1190
|
+
var small = { height: "28px" };
|
|
1191
|
+
var OperatorsScreen = defineComponent8({
|
|
1192
|
+
name: "FonderieOperatorsScreen",
|
|
1193
|
+
props: {
|
|
1194
|
+
client: { type: Object, required: true },
|
|
1195
|
+
// The signed-in operator's email: their own row offers no disable or
|
|
1196
|
+
// recovery (the server refuses both for yourself anyway).
|
|
1197
|
+
me: { type: String, default: void 0 }
|
|
1198
|
+
},
|
|
1199
|
+
setup(props) {
|
|
1200
|
+
const { report, isLoading, error, invite, recover, update, revokeLink } = useAdminOperators(
|
|
1201
|
+
props.client
|
|
1202
|
+
);
|
|
1203
|
+
const email = ref2("");
|
|
1204
|
+
const level = ref2("Editor");
|
|
1205
|
+
const minted = ref2(null);
|
|
1206
|
+
const copied = ref2(false);
|
|
1207
|
+
const status = (o) => o.disabledAt ? pill("neutral", "disabled") : o.locked ? pill("warn", "locked") : !o.enrolled ? pill("warn", "setting up") : pill("ok", "active");
|
|
1208
|
+
const mintedNotice = (link) => {
|
|
1209
|
+
const url = `${window.location.origin}${link.url}`;
|
|
1210
|
+
return h10(
|
|
1211
|
+
"div",
|
|
1212
|
+
{
|
|
1213
|
+
style: {
|
|
1214
|
+
...styles.notice,
|
|
1215
|
+
background: "color-mix(in srgb, var(--fonderie-accent,#00d294) 10%, transparent)",
|
|
1216
|
+
borderColor: "color-mix(in srgb, var(--fonderie-accent,#00d294) 35%, transparent)"
|
|
1217
|
+
}
|
|
1218
|
+
},
|
|
1219
|
+
[
|
|
1220
|
+
h10("strong", `Send this link to ${link.email}. It is shown once.`),
|
|
1221
|
+
` It works once and expires ${new Date(link.expiresAt).toLocaleString()}.`,
|
|
1222
|
+
h10(
|
|
1223
|
+
"div",
|
|
1224
|
+
{ style: { display: "flex", gap: "8px", marginTop: "8px", alignItems: "center" } },
|
|
1225
|
+
[
|
|
1226
|
+
h10(
|
|
1227
|
+
"code",
|
|
1228
|
+
{ style: { ...styles.code, flex: 1, padding: "7px 10px", wordBreak: "break-all" } },
|
|
1229
|
+
url
|
|
1230
|
+
),
|
|
1231
|
+
h10(
|
|
1232
|
+
"button",
|
|
1233
|
+
{
|
|
1234
|
+
type: "button",
|
|
1235
|
+
style: styles.button,
|
|
1236
|
+
onClick: () => {
|
|
1237
|
+
void navigator.clipboard?.writeText(url);
|
|
1238
|
+
copied.value = true;
|
|
1239
|
+
}
|
|
1240
|
+
},
|
|
1241
|
+
copied.value ? "Copied" : "Copy"
|
|
1242
|
+
),
|
|
1243
|
+
h10(
|
|
1244
|
+
"button",
|
|
1245
|
+
{
|
|
1246
|
+
type: "button",
|
|
1247
|
+
style: styles.buttonGhost,
|
|
1248
|
+
"aria-label": "Dismiss",
|
|
1249
|
+
onClick: () => {
|
|
1250
|
+
minted.value = null;
|
|
1251
|
+
copied.value = false;
|
|
1252
|
+
}
|
|
1253
|
+
},
|
|
1254
|
+
[icon("close", 14)]
|
|
1255
|
+
)
|
|
1256
|
+
]
|
|
1257
|
+
)
|
|
1258
|
+
]
|
|
1259
|
+
);
|
|
1260
|
+
};
|
|
1261
|
+
const setMinted = (l) => {
|
|
1262
|
+
copied.value = false;
|
|
1263
|
+
minted.value = l;
|
|
1264
|
+
};
|
|
1265
|
+
return () => {
|
|
1266
|
+
const r = report.value;
|
|
1267
|
+
const e = error.value;
|
|
1268
|
+
return h10("div", { style: styles.container }, [
|
|
1269
|
+
pageHeader(
|
|
1270
|
+
"Operators",
|
|
1271
|
+
"The people who can sign in to this console. Each one uses a password and an authenticator app; there is no sign-up."
|
|
1272
|
+
),
|
|
1273
|
+
e ? h10(
|
|
1274
|
+
"p",
|
|
1275
|
+
{ style: styles.error, role: "alert" },
|
|
1276
|
+
e.status === 403 && e.reason === "FORBIDDEN" ? "Managing operators needs the Owner level." : e.explanation
|
|
1277
|
+
) : null,
|
|
1278
|
+
minted.value ? mintedNotice(minted.value) : null,
|
|
1279
|
+
h10(
|
|
1280
|
+
"form",
|
|
1281
|
+
{
|
|
1282
|
+
style: {
|
|
1283
|
+
...styles.card,
|
|
1284
|
+
display: "flex",
|
|
1285
|
+
gap: "8px",
|
|
1286
|
+
alignItems: "center",
|
|
1287
|
+
flexWrap: "wrap",
|
|
1288
|
+
marginBottom: "20px"
|
|
1289
|
+
},
|
|
1290
|
+
onSubmit: (ev) => {
|
|
1291
|
+
ev.preventDefault();
|
|
1292
|
+
void invite({ email: email.value.trim(), scopes: scopesFor(level.value) }).then((l) => {
|
|
1293
|
+
setMinted(l);
|
|
1294
|
+
email.value = "";
|
|
1295
|
+
}).catch(() => {
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
},
|
|
1299
|
+
[
|
|
1300
|
+
icon("users", 16),
|
|
1301
|
+
h10("strong", { style: { fontSize: "13.5px", marginRight: "4px" } }, "Invite"),
|
|
1302
|
+
h10("input", {
|
|
1303
|
+
type: "email",
|
|
1304
|
+
required: true,
|
|
1305
|
+
value: email.value,
|
|
1306
|
+
onInput: (ev) => email.value = ev.target.value,
|
|
1307
|
+
placeholder: "teammate@company.com",
|
|
1308
|
+
style: { ...styles.input, flex: 1, minWidth: "220px" },
|
|
1309
|
+
"aria-label": "Email to invite"
|
|
1310
|
+
}),
|
|
1311
|
+
h10(
|
|
1312
|
+
"select",
|
|
1313
|
+
{
|
|
1314
|
+
value: level.value,
|
|
1315
|
+
onChange: (ev) => level.value = ev.target.value,
|
|
1316
|
+
style: styles.input,
|
|
1317
|
+
"aria-label": "Access level"
|
|
1318
|
+
},
|
|
1319
|
+
LEVELS.map(
|
|
1320
|
+
(l) => h10("option", { key: l.label, value: l.label }, `${l.label} \u2014 ${l.hint}`)
|
|
1321
|
+
)
|
|
1322
|
+
),
|
|
1323
|
+
h10(
|
|
1324
|
+
"button",
|
|
1325
|
+
{ type: "submit", style: styles.buttonPrimary, disabled: !email.value.trim() },
|
|
1326
|
+
[icon("plus", 14), "Create invite link"]
|
|
1327
|
+
)
|
|
1328
|
+
]
|
|
1329
|
+
),
|
|
1330
|
+
isLoading.value && !r ? h10("p", { style: styles.status }, "Loading\u2026") : r && r.operators.length === 0 ? empty("No operators yet", void 0, "users") : r ? table(
|
|
1331
|
+
["Operator", "Access", "Status", "Last sign-in", ""],
|
|
1332
|
+
r.operators.map((o) => {
|
|
1333
|
+
const mine = o.email === props.me;
|
|
1334
|
+
return h10("tr", { key: o.id }, [
|
|
1335
|
+
td([
|
|
1336
|
+
h10("div", { style: { fontWeight: 600 } }, o.name || o.email),
|
|
1337
|
+
o.name ? h10("div", { style: styles.muted }, o.email) : null
|
|
1338
|
+
]),
|
|
1339
|
+
td(
|
|
1340
|
+
h10(
|
|
1341
|
+
"select",
|
|
1342
|
+
{
|
|
1343
|
+
value: levelOf(o.scopes),
|
|
1344
|
+
disabled: mine,
|
|
1345
|
+
onChange: (ev) => void update(o.id, {
|
|
1346
|
+
scopes: scopesFor(ev.target.value)
|
|
1347
|
+
}).catch(() => {
|
|
1348
|
+
}),
|
|
1349
|
+
style: { ...styles.input, ...small, fontSize: "12.5px" },
|
|
1350
|
+
"aria-label": `Access level for ${o.email}`
|
|
1351
|
+
},
|
|
1352
|
+
LEVELS.map((l) => h10("option", { key: l.label, value: l.label }, l.label))
|
|
1353
|
+
)
|
|
1354
|
+
),
|
|
1355
|
+
td([
|
|
1356
|
+
status(o),
|
|
1357
|
+
o.enrolled && o.backupCodesLeft <= 2 && !o.disabledAt ? h10(
|
|
1358
|
+
"div",
|
|
1359
|
+
{ style: { ...styles.muted, marginTop: "4px" } },
|
|
1360
|
+
`${o.backupCodesLeft} backup code(s) left`
|
|
1361
|
+
) : null
|
|
1362
|
+
]),
|
|
1363
|
+
td(o.lastLoginAt ? new Date(o.lastLoginAt).toLocaleString() : "never", {
|
|
1364
|
+
...styles.muted,
|
|
1365
|
+
whiteSpace: "nowrap"
|
|
1366
|
+
}),
|
|
1367
|
+
td(
|
|
1368
|
+
mine ? h10("span", { style: styles.muted }, "you") : [
|
|
1369
|
+
h10(
|
|
1370
|
+
"button",
|
|
1371
|
+
{
|
|
1372
|
+
type: "button",
|
|
1373
|
+
style: { ...styles.button, ...small, marginRight: "6px" },
|
|
1374
|
+
onClick: () => {
|
|
1375
|
+
if (window.confirm(
|
|
1376
|
+
`Create a recovery link for ${o.email}? It signs them out everywhere; the link sets a new password and a new authenticator.`
|
|
1377
|
+
))
|
|
1378
|
+
void recover(o.id).then(setMinted).catch(() => {
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
},
|
|
1382
|
+
"Recovery link"
|
|
1383
|
+
),
|
|
1384
|
+
h10(
|
|
1385
|
+
"button",
|
|
1386
|
+
{
|
|
1387
|
+
type: "button",
|
|
1388
|
+
style: {
|
|
1389
|
+
...o.disabledAt ? styles.button : styles.buttonDanger,
|
|
1390
|
+
...small
|
|
1391
|
+
},
|
|
1392
|
+
onClick: () => {
|
|
1393
|
+
if (o.disabledAt || window.confirm(
|
|
1394
|
+
`Disable ${o.email}? They are signed out immediately.`
|
|
1395
|
+
))
|
|
1396
|
+
void update(o.id, { disabled: !o.disabledAt }).catch(
|
|
1397
|
+
() => {
|
|
1398
|
+
}
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
},
|
|
1402
|
+
o.disabledAt ? "Enable" : "Disable"
|
|
1403
|
+
)
|
|
1404
|
+
],
|
|
1405
|
+
{ textAlign: "right", whiteSpace: "nowrap" }
|
|
1406
|
+
)
|
|
1407
|
+
]);
|
|
1408
|
+
})
|
|
1409
|
+
) : null,
|
|
1410
|
+
r && r.links.length > 0 ? [
|
|
1411
|
+
h10("h2", { style: styles.subtitle }, "Pending links"),
|
|
1412
|
+
h10(
|
|
1413
|
+
"ul",
|
|
1414
|
+
{ style: styles.list },
|
|
1415
|
+
r.links.map(
|
|
1416
|
+
(l) => h10(
|
|
1417
|
+
"li",
|
|
1418
|
+
{
|
|
1419
|
+
key: l.id,
|
|
1420
|
+
style: { ...styles.row, display: "flex", gap: "12px", alignItems: "center" }
|
|
1421
|
+
},
|
|
1422
|
+
[
|
|
1423
|
+
pill(l.kind === "invite" ? "info" : "warn", l.kind, false),
|
|
1424
|
+
h10("span", { style: { flex: 1 } }, [
|
|
1425
|
+
l.email,
|
|
1426
|
+
l.kind === "invite" ? h10("span", { style: styles.muted }, ` \xB7 ${levelOf(l.scopes)}`) : null
|
|
1427
|
+
]),
|
|
1428
|
+
h10(
|
|
1429
|
+
"span",
|
|
1430
|
+
{ style: styles.muted },
|
|
1431
|
+
`expires ${new Date(l.expiresAt).toLocaleString()}`
|
|
1432
|
+
),
|
|
1433
|
+
h10(
|
|
1434
|
+
"button",
|
|
1435
|
+
{
|
|
1436
|
+
type: "button",
|
|
1437
|
+
style: { ...styles.buttonDanger, ...small },
|
|
1438
|
+
onClick: () => void revokeLink(l.id).catch(() => {
|
|
1439
|
+
})
|
|
1440
|
+
},
|
|
1441
|
+
"Revoke"
|
|
1442
|
+
)
|
|
1443
|
+
]
|
|
1444
|
+
)
|
|
1445
|
+
)
|
|
1446
|
+
)
|
|
1447
|
+
] : null
|
|
1448
|
+
]);
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1170
1453
|
// src/screens/TokensScreen.ts
|
|
1171
1454
|
import { useAdminTokens } from "@fonderie/vue-admin";
|
|
1172
|
-
import { defineComponent as
|
|
1455
|
+
import { defineComponent as defineComponent9, h as h11, ref as ref3 } from "vue";
|
|
1173
1456
|
var ALL = ["read", "write", "secrets"];
|
|
1174
|
-
var TokensScreen =
|
|
1457
|
+
var TokensScreen = defineComponent9({
|
|
1175
1458
|
name: "FonderieTokensScreen",
|
|
1176
1459
|
props: { client: { type: Object, required: true } },
|
|
1177
1460
|
setup(props) {
|
|
1178
1461
|
const { report, isLoading, error, issue, revoke } = useAdminTokens(props.client);
|
|
1179
|
-
const name =
|
|
1180
|
-
const scopes =
|
|
1181
|
-
const days =
|
|
1182
|
-
const minted =
|
|
1462
|
+
const name = ref3("");
|
|
1463
|
+
const scopes = ref3(["read"]);
|
|
1464
|
+
const days = ref3("");
|
|
1465
|
+
const minted = ref3(null);
|
|
1183
1466
|
const toggle = (s) => scopes.value = scopes.value.includes(s) ? scopes.value.filter((x) => x !== s) : [...scopes.value, s];
|
|
1184
1467
|
return () => {
|
|
1185
1468
|
const r = report.value;
|
|
1186
|
-
return
|
|
1469
|
+
return h11("div", { style: styles.container }, [
|
|
1187
1470
|
pageHeader(
|
|
1188
|
-
"
|
|
1189
|
-
"
|
|
1471
|
+
"Tokens",
|
|
1472
|
+
"Credentials for machines: the root token's strength, scoped tokens for the CLI and CI, and any legacy per-module tokens. People sign in as operators."
|
|
1190
1473
|
),
|
|
1191
|
-
error.value ?
|
|
1474
|
+
error.value ? h11(
|
|
1192
1475
|
"p",
|
|
1193
1476
|
{ style: styles.error, role: "alert" },
|
|
1194
1477
|
error.value.status === 401 ? "Issuing and revoking need the root token (the one in your deployment config)." : error.value.explanation
|
|
1195
1478
|
) : null,
|
|
1196
|
-
|
|
1197
|
-
r ?
|
|
1479
|
+
h11("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Root token"),
|
|
1480
|
+
r ? h11(
|
|
1198
1481
|
"div",
|
|
1199
1482
|
{ style: { ...styles.card, display: "flex", gap: "12px", alignItems: "flex-start" } },
|
|
1200
1483
|
[
|
|
1201
1484
|
icon("lock", 18, { marginTop: "2px" }),
|
|
1202
|
-
|
|
1485
|
+
h11("div", [
|
|
1203
1486
|
pill(r.admin.ok ? "ok" : "bad", r.admin.ok ? "strong" : "weak"),
|
|
1204
1487
|
...r.admin.problems.map(
|
|
1205
|
-
(p) =>
|
|
1488
|
+
(p) => h11(
|
|
1206
1489
|
"div",
|
|
1207
1490
|
{
|
|
1208
1491
|
key: p.message,
|
|
@@ -1218,13 +1501,13 @@ var TokensScreen = defineComponent8({
|
|
|
1218
1501
|
])
|
|
1219
1502
|
]
|
|
1220
1503
|
) : null,
|
|
1221
|
-
|
|
1222
|
-
r?.issued === null ?
|
|
1504
|
+
h11("h2", { style: styles.subtitle }, "Issued tokens"),
|
|
1505
|
+
r?.issued === null ? h11(
|
|
1223
1506
|
"p",
|
|
1224
1507
|
{ style: styles.muted },
|
|
1225
1508
|
"Issuing is off \u2014 give AdminModule a store and run its migrations."
|
|
1226
1509
|
) : [
|
|
1227
|
-
minted.value ?
|
|
1510
|
+
minted.value ? h11(
|
|
1228
1511
|
"div",
|
|
1229
1512
|
{
|
|
1230
1513
|
style: {
|
|
@@ -1234,9 +1517,9 @@ var TokensScreen = defineComponent8({
|
|
|
1234
1517
|
}
|
|
1235
1518
|
},
|
|
1236
1519
|
[
|
|
1237
|
-
|
|
1520
|
+
h11("strong", "Copy this now \u2014 it is never shown again."),
|
|
1238
1521
|
` (${minted.value.name})`,
|
|
1239
|
-
|
|
1522
|
+
h11(
|
|
1240
1523
|
"div",
|
|
1241
1524
|
{
|
|
1242
1525
|
style: {
|
|
@@ -1251,7 +1534,7 @@ var TokensScreen = defineComponent8({
|
|
|
1251
1534
|
)
|
|
1252
1535
|
]
|
|
1253
1536
|
) : null,
|
|
1254
|
-
|
|
1537
|
+
h11(
|
|
1255
1538
|
"form",
|
|
1256
1539
|
{
|
|
1257
1540
|
style: styles.toolbar,
|
|
@@ -1271,7 +1554,7 @@ var TokensScreen = defineComponent8({
|
|
|
1271
1554
|
}
|
|
1272
1555
|
},
|
|
1273
1556
|
[
|
|
1274
|
-
|
|
1557
|
+
h11("input", {
|
|
1275
1558
|
value: name.value,
|
|
1276
1559
|
onInput: (e) => name.value = e.target.value,
|
|
1277
1560
|
placeholder: "name",
|
|
@@ -1279,7 +1562,7 @@ var TokensScreen = defineComponent8({
|
|
|
1279
1562
|
"aria-label": "Name"
|
|
1280
1563
|
}),
|
|
1281
1564
|
...ALL.map(
|
|
1282
|
-
(s) =>
|
|
1565
|
+
(s) => h11(
|
|
1283
1566
|
"label",
|
|
1284
1567
|
{
|
|
1285
1568
|
key: s,
|
|
@@ -1293,7 +1576,7 @@ var TokensScreen = defineComponent8({
|
|
|
1293
1576
|
}
|
|
1294
1577
|
},
|
|
1295
1578
|
[
|
|
1296
|
-
|
|
1579
|
+
h11("input", {
|
|
1297
1580
|
type: "checkbox",
|
|
1298
1581
|
checked: scopes.value.includes(s),
|
|
1299
1582
|
onChange: () => toggle(s)
|
|
@@ -1302,14 +1585,14 @@ var TokensScreen = defineComponent8({
|
|
|
1302
1585
|
]
|
|
1303
1586
|
)
|
|
1304
1587
|
),
|
|
1305
|
-
|
|
1588
|
+
h11("input", {
|
|
1306
1589
|
value: days.value,
|
|
1307
1590
|
onInput: (e) => days.value = e.target.value,
|
|
1308
1591
|
placeholder: "days (optional)",
|
|
1309
1592
|
style: { ...styles.input, width: "140px" },
|
|
1310
1593
|
"aria-label": "Days"
|
|
1311
1594
|
}),
|
|
1312
|
-
|
|
1595
|
+
h11(
|
|
1313
1596
|
"button",
|
|
1314
1597
|
{
|
|
1315
1598
|
type: "submit",
|
|
@@ -1323,7 +1606,7 @@ var TokensScreen = defineComponent8({
|
|
|
1323
1606
|
r?.issued && r.issued.length > 0 ? table(
|
|
1324
1607
|
["Name", "Scopes", "Created", "Expires", "Last used", ""],
|
|
1325
1608
|
r.issued.map(
|
|
1326
|
-
(t2) =>
|
|
1609
|
+
(t2) => h11("tr", { key: t2.id }, [
|
|
1327
1610
|
td([t2.name, t2.revokedAt ? [" ", pill("neutral", "revoked")] : ""]),
|
|
1328
1611
|
td(t2.scopes.join(", "), styles.mono),
|
|
1329
1612
|
td(
|
|
@@ -1339,7 +1622,7 @@ var TokensScreen = defineComponent8({
|
|
|
1339
1622
|
styles.muted
|
|
1340
1623
|
),
|
|
1341
1624
|
td(
|
|
1342
|
-
t2.revokedAt ? "" :
|
|
1625
|
+
t2.revokedAt ? "" : h11(
|
|
1343
1626
|
"button",
|
|
1344
1627
|
{
|
|
1345
1628
|
type: "button",
|
|
@@ -1363,19 +1646,19 @@ var TokensScreen = defineComponent8({
|
|
|
1363
1646
|
"tokens"
|
|
1364
1647
|
)
|
|
1365
1648
|
],
|
|
1366
|
-
|
|
1367
|
-
r && r.legacy.length === 0 ?
|
|
1649
|
+
h11("h2", { style: styles.subtitle }, "Legacy per-brick tokens"),
|
|
1650
|
+
r && r.legacy.length === 0 ? h11(
|
|
1368
1651
|
"p",
|
|
1369
1652
|
{ style: styles.muted },
|
|
1370
1653
|
"None \u2014 every brick's admin surface goes through this token."
|
|
1371
|
-
) :
|
|
1654
|
+
) : h11(
|
|
1372
1655
|
"ul",
|
|
1373
1656
|
{ style: styles.list },
|
|
1374
1657
|
(r?.legacy ?? []).map(
|
|
1375
|
-
(l) =>
|
|
1376
|
-
|
|
1658
|
+
(l) => h11("li", { key: l.module, style: styles.row }, [
|
|
1659
|
+
h11("span", { style: styles.mono }, l.module),
|
|
1377
1660
|
" ",
|
|
1378
|
-
|
|
1661
|
+
h11(
|
|
1379
1662
|
"span",
|
|
1380
1663
|
{ style: styles.muted },
|
|
1381
1664
|
"still registers its standalone routes with its own token (deprecated)"
|
|
@@ -1396,28 +1679,233 @@ import {
|
|
|
1396
1679
|
useAdminUserSessions,
|
|
1397
1680
|
useAdminUsers
|
|
1398
1681
|
} from "@fonderie/vue-admin";
|
|
1399
|
-
import { computed, defineComponent as
|
|
1400
|
-
|
|
1682
|
+
import { computed as computed2, defineComponent as defineComponent11, h as h13, ref as ref6 } from "vue";
|
|
1683
|
+
|
|
1684
|
+
// src/billing.ts
|
|
1685
|
+
import { ref as ref4 } from "vue";
|
|
1686
|
+
function periodEnd(s) {
|
|
1687
|
+
if (!s.currentPeriodEnd) return null;
|
|
1688
|
+
const date = new Date(s.currentPeriodEnd).toLocaleDateString();
|
|
1689
|
+
if (s.status === "canceled")
|
|
1690
|
+
return { label: new Date(s.currentPeriodEnd) < /* @__PURE__ */ new Date() ? "Ended" : "Ends", date };
|
|
1691
|
+
if (s.cancelAtPeriodEnd) return { label: "Ends", date };
|
|
1692
|
+
return { label: "Renews", date };
|
|
1693
|
+
}
|
|
1694
|
+
var statusTone = (status) => status === "active" ? "ok" : status === "trialing" ? "info" : status === "past_due" || status === "unpaid" ? "warn" : "neutral";
|
|
1695
|
+
function useSubscriptionIndex(client) {
|
|
1696
|
+
const index = ref4(null);
|
|
1697
|
+
if (!client) return index;
|
|
1698
|
+
void (async () => {
|
|
1699
|
+
const map = /* @__PURE__ */ new Map();
|
|
1700
|
+
let cursor;
|
|
1701
|
+
for (let page2 = 0; page2 < 20; page2++) {
|
|
1702
|
+
const { result } = await client.listSubscriptions({
|
|
1703
|
+
limit: 100,
|
|
1704
|
+
...cursor ? { cursor } : {}
|
|
1705
|
+
});
|
|
1706
|
+
for (const s of result.subscriptions) {
|
|
1707
|
+
const key = `${s.subscriberType}/${s.subscriberId}`;
|
|
1708
|
+
if (!map.has(key)) map.set(key, s);
|
|
1709
|
+
}
|
|
1710
|
+
if (!result.nextCursor) break;
|
|
1711
|
+
cursor = result.nextCursor;
|
|
1712
|
+
}
|
|
1713
|
+
index.value = map;
|
|
1714
|
+
})().catch(() => {
|
|
1715
|
+
index.value = /* @__PURE__ */ new Map();
|
|
1716
|
+
});
|
|
1717
|
+
return index;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// src/screens/SubscriberBilling.ts
|
|
1721
|
+
import { useAdminSubscriber } from "@fonderie/vue-admin";
|
|
1722
|
+
import { computed, defineComponent as defineComponent10, h as h12, ref as ref5 } from "vue";
|
|
1723
|
+
var SubscriberBilling = defineComponent10({
|
|
1724
|
+
name: "FonderieSubscriberBilling",
|
|
1725
|
+
props: {
|
|
1726
|
+
client: { type: Object, required: true },
|
|
1727
|
+
subscriberType: { type: String, required: true },
|
|
1728
|
+
subscriberId: { type: String, required: true }
|
|
1729
|
+
},
|
|
1730
|
+
setup(props) {
|
|
1731
|
+
const subscriber = computed(() => ({ type: props.subscriberType, id: props.subscriberId }));
|
|
1732
|
+
const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(props.client, subscriber, { limit: 20 });
|
|
1733
|
+
const amount = ref5("");
|
|
1734
|
+
const note = ref5("");
|
|
1735
|
+
const granted = ref5(null);
|
|
1736
|
+
return () => {
|
|
1737
|
+
const s = subscription.value;
|
|
1738
|
+
const w = wallet.value;
|
|
1739
|
+
if (isLoading.value && !w && !s) return h12("p", { style: styles.status }, "Loading billing\u2026");
|
|
1740
|
+
const end = s ? periodEnd(s) : null;
|
|
1741
|
+
return h12("div", [
|
|
1742
|
+
error.value ? h12("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
|
|
1743
|
+
h12("div", { style: { ...styles.grid, marginBottom: "16px" } }, [
|
|
1744
|
+
h12("div", { style: styles.card }, [
|
|
1745
|
+
h12("div", { style: styles.statLabel }, "Plan"),
|
|
1746
|
+
h12(
|
|
1747
|
+
"div",
|
|
1748
|
+
{
|
|
1749
|
+
style: {
|
|
1750
|
+
display: "flex",
|
|
1751
|
+
alignItems: "center",
|
|
1752
|
+
gap: "8px",
|
|
1753
|
+
marginTop: "6px",
|
|
1754
|
+
flexWrap: "wrap"
|
|
1755
|
+
}
|
|
1756
|
+
},
|
|
1757
|
+
[
|
|
1758
|
+
h12("strong", { style: { fontSize: "18px" } }, s ? s.plan : "Free"),
|
|
1759
|
+
...s ? [
|
|
1760
|
+
h12("span", { style: styles.muted }, `\xB7 ${s.interval}`),
|
|
1761
|
+
pill(statusTone(s.status), s.status)
|
|
1762
|
+
] : [h12("span", { style: styles.muted }, "no subscription")]
|
|
1763
|
+
]
|
|
1764
|
+
),
|
|
1765
|
+
end ? h12("div", { style: styles.statHint }, `${end.label} ${end.date}`) : null,
|
|
1766
|
+
s?.providerSubscriptionId ? h12(
|
|
1767
|
+
"div",
|
|
1768
|
+
{ style: { ...styles.statHint, fontFamily: "var(--fonderie-mono,monospace)" } },
|
|
1769
|
+
s.providerSubscriptionId
|
|
1770
|
+
) : null
|
|
1771
|
+
]),
|
|
1772
|
+
h12("div", { style: styles.card }, [
|
|
1773
|
+
h12("div", { style: styles.statLabel }, "Credits"),
|
|
1774
|
+
...w ? [
|
|
1775
|
+
h12("div", { style: styles.statValue }, [
|
|
1776
|
+
`${w.balance} `,
|
|
1777
|
+
h12("span", { style: { fontSize: "14px", fontWeight: 500 } }, w.currency)
|
|
1778
|
+
]),
|
|
1779
|
+
h12(
|
|
1780
|
+
"div",
|
|
1781
|
+
{ style: styles.statHint },
|
|
1782
|
+
`${w.purchased ?? "0"} permanent \xB7 ${w.granted ?? "0"} plan allowance${w.grantedExpiresAt ? ` (expires ${new Date(w.grantedExpiresAt).toLocaleDateString()})` : ""}`
|
|
1783
|
+
)
|
|
1784
|
+
] : [
|
|
1785
|
+
h12(
|
|
1786
|
+
"div",
|
|
1787
|
+
{ style: styles.statHint },
|
|
1788
|
+
"No wallet \u2014 billing has no wallet configuration."
|
|
1789
|
+
)
|
|
1790
|
+
]
|
|
1791
|
+
])
|
|
1792
|
+
]),
|
|
1793
|
+
w ? h12(
|
|
1794
|
+
"form",
|
|
1795
|
+
{
|
|
1796
|
+
style: {
|
|
1797
|
+
...styles.card,
|
|
1798
|
+
display: "flex",
|
|
1799
|
+
gap: "8px",
|
|
1800
|
+
alignItems: "center",
|
|
1801
|
+
flexWrap: "wrap",
|
|
1802
|
+
marginBottom: "16px"
|
|
1803
|
+
},
|
|
1804
|
+
onSubmit: (e) => {
|
|
1805
|
+
e.preventDefault();
|
|
1806
|
+
const key = `admin-grant-${props.subscriberType}-${props.subscriberId}-${Date.now()}`;
|
|
1807
|
+
const a = amount.value.trim();
|
|
1808
|
+
void grant({
|
|
1809
|
+
amount: a,
|
|
1810
|
+
...note.value ? { description: note.value } : {},
|
|
1811
|
+
idempotencyKey: key
|
|
1812
|
+
}).then(() => {
|
|
1813
|
+
granted.value = `Granted ${a} ${w.currency}.`;
|
|
1814
|
+
amount.value = "";
|
|
1815
|
+
note.value = "";
|
|
1816
|
+
}).catch(() => {
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
},
|
|
1820
|
+
[
|
|
1821
|
+
h12("strong", { style: { fontSize: "13.5px", marginRight: "4px" } }, "Grant credits"),
|
|
1822
|
+
h12("input", {
|
|
1823
|
+
value: amount.value,
|
|
1824
|
+
onInput: (e) => amount.value = e.target.value,
|
|
1825
|
+
placeholder: `amount (${w.currency})`,
|
|
1826
|
+
inputmode: "numeric",
|
|
1827
|
+
style: { ...styles.input, width: "150px" },
|
|
1828
|
+
"aria-label": "Amount"
|
|
1829
|
+
}),
|
|
1830
|
+
h12("input", {
|
|
1831
|
+
value: note.value,
|
|
1832
|
+
onInput: (e) => note.value = e.target.value,
|
|
1833
|
+
placeholder: "reason (optional)",
|
|
1834
|
+
style: { ...styles.input, flex: 1, minWidth: "180px" },
|
|
1835
|
+
"aria-label": "Reason"
|
|
1836
|
+
}),
|
|
1837
|
+
h12(
|
|
1838
|
+
"button",
|
|
1839
|
+
{
|
|
1840
|
+
type: "submit",
|
|
1841
|
+
style: styles.buttonPrimary,
|
|
1842
|
+
disabled: !/^\d+$/.test(amount.value.trim())
|
|
1843
|
+
},
|
|
1844
|
+
[icon("plus", 14), "Grant"]
|
|
1845
|
+
),
|
|
1846
|
+
granted.value ? h12("span", { style: styles.ok }, granted.value) : null
|
|
1847
|
+
]
|
|
1848
|
+
) : null,
|
|
1849
|
+
w ? ledger.value.length === 0 ? empty("No credit movements yet", void 0, "subscriber") : h12("div", [
|
|
1850
|
+
table(
|
|
1851
|
+
["When", "Kind", "Amount", "Note"],
|
|
1852
|
+
ledger.value.map(
|
|
1853
|
+
(t2) => h12("tr", { key: t2.id }, [
|
|
1854
|
+
td(new Date(t2.createdAt).toLocaleString(), {
|
|
1855
|
+
...styles.muted,
|
|
1856
|
+
whiteSpace: "nowrap"
|
|
1857
|
+
}),
|
|
1858
|
+
td(t2.type),
|
|
1859
|
+
td(t2.amount, styles.mono),
|
|
1860
|
+
td(t2.description ?? "", styles.muted)
|
|
1861
|
+
])
|
|
1862
|
+
)
|
|
1863
|
+
),
|
|
1864
|
+
hasMoreLedger.value ? loadMoreButton(() => void loadMoreLedger()) : null
|
|
1865
|
+
]) : null
|
|
1866
|
+
]);
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
// src/screens/UsersScreen.ts
|
|
1872
|
+
var UsersScreen = defineComponent11({
|
|
1401
1873
|
name: "FonderieUsersScreen",
|
|
1402
1874
|
props: {
|
|
1403
1875
|
client: { type: Object, required: true },
|
|
1404
|
-
pageSize: { type: Number, default: 50 }
|
|
1876
|
+
pageSize: { type: Number, default: 50 },
|
|
1877
|
+
// Given ⇒ a Plan column, and each user's plan, credits and grant form on
|
|
1878
|
+
// their page. Everyone has a wallet; no subscription means the free tier.
|
|
1879
|
+
billingClient: { type: Object, default: void 0 },
|
|
1880
|
+
// Open this user on arrival (e.g. from the Subscriptions list).
|
|
1881
|
+
openUserId: { type: String, default: void 0 }
|
|
1405
1882
|
},
|
|
1406
1883
|
setup(props) {
|
|
1407
|
-
const input =
|
|
1408
|
-
const email =
|
|
1409
|
-
const selectedId =
|
|
1884
|
+
const input = ref6("");
|
|
1885
|
+
const email = ref6("");
|
|
1886
|
+
const selectedId = ref6(props.openUserId ?? "");
|
|
1887
|
+
const plans = useSubscriptionIndex(props.billingClient);
|
|
1410
1888
|
const list = useAdminUsers(props.client, { limit: props.pageSize });
|
|
1411
1889
|
const { user, isLoading, error, suspend, unsuspend, revokeSessions } = useAdminUser(
|
|
1412
1890
|
props.client,
|
|
1413
1891
|
{ email, id: selectedId }
|
|
1414
1892
|
);
|
|
1415
|
-
const userId =
|
|
1893
|
+
const userId = computed2(() => user.value?.id ?? null);
|
|
1416
1894
|
const sessions = useAdminUserSessions(props.client, userId);
|
|
1417
1895
|
const history = useAdminLoginHistory(props.client, userId, { limit: 20 });
|
|
1418
|
-
const showList =
|
|
1896
|
+
const showList = computed2(() => !email.value && !selectedId.value);
|
|
1419
1897
|
const yesNo = (v) => v ? pill("ok", "yes") : pill("neutral", "no");
|
|
1420
|
-
const row = (k, v) =>
|
|
1898
|
+
const row = (k, v) => h13("tr", [td(k), td(v)]);
|
|
1899
|
+
const planCell = (id) => {
|
|
1900
|
+
if (!plans.value) return h13("span", { style: styles.muted }, "\u2026");
|
|
1901
|
+
const sub = plans.value.get(`user/${id}`);
|
|
1902
|
+
if (!sub || sub.status === "canceled") return h13("span", { style: styles.muted }, "free");
|
|
1903
|
+
return [
|
|
1904
|
+
h13("strong", sub.plan),
|
|
1905
|
+
" ",
|
|
1906
|
+
sub.status !== "active" ? pill(statusTone(sub.status), sub.status) : null
|
|
1907
|
+
];
|
|
1908
|
+
};
|
|
1421
1909
|
const clear = () => {
|
|
1422
1910
|
email.value = "";
|
|
1423
1911
|
selectedId.value = "";
|
|
@@ -1425,12 +1913,12 @@ var UsersScreen = defineComponent9({
|
|
|
1425
1913
|
};
|
|
1426
1914
|
return () => {
|
|
1427
1915
|
const u = user.value;
|
|
1428
|
-
return
|
|
1916
|
+
return h13("div", { style: styles.container }, [
|
|
1429
1917
|
pageHeader(
|
|
1430
1918
|
"Users",
|
|
1431
1919
|
"Everyone signed up. Open an account for its sessions, sign-ins and the suspend and sign-out controls."
|
|
1432
1920
|
),
|
|
1433
|
-
|
|
1921
|
+
h13(
|
|
1434
1922
|
"form",
|
|
1435
1923
|
{
|
|
1436
1924
|
style: styles.toolbar,
|
|
@@ -1441,7 +1929,7 @@ var UsersScreen = defineComponent9({
|
|
|
1441
1929
|
}
|
|
1442
1930
|
},
|
|
1443
1931
|
[
|
|
1444
|
-
|
|
1932
|
+
h13("input", {
|
|
1445
1933
|
type: "email",
|
|
1446
1934
|
value: input.value,
|
|
1447
1935
|
onInput: (e) => input.value = e.target.value,
|
|
@@ -1449,22 +1937,22 @@ var UsersScreen = defineComponent9({
|
|
|
1449
1937
|
style: { ...styles.input, minWidth: "280px" },
|
|
1450
1938
|
"aria-label": "Email"
|
|
1451
1939
|
}),
|
|
1452
|
-
|
|
1940
|
+
h13(
|
|
1453
1941
|
"button",
|
|
1454
1942
|
{ type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
|
|
1455
1943
|
[icon("search", 14), "Look up"]
|
|
1456
1944
|
),
|
|
1457
|
-
showList.value ?
|
|
1945
|
+
showList.value ? refreshButton2("Refresh", list.isLoading.value, () => void list.refresh()) : actionButton([icon("back", 14), "All users"], false, clear, styles.buttonGhost)
|
|
1458
1946
|
]
|
|
1459
1947
|
),
|
|
1460
|
-
showList.value ?
|
|
1461
|
-
list.error.value ?
|
|
1948
|
+
showList.value ? h13("div", [
|
|
1949
|
+
list.error.value ? h13("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
|
|
1462
1950
|
list.users.value.length === 0 && !list.isLoading.value ? empty("No users yet", "Sign-ups appear here as they happen.", "users") : table(
|
|
1463
|
-
["Email", "Name", "Created", "Status"],
|
|
1951
|
+
props.billingClient ? ["Email", "Name", "Plan", "Created", "Status"] : ["Email", "Name", "Created", "Status"],
|
|
1464
1952
|
list.users.value.map(
|
|
1465
|
-
(u2) =>
|
|
1953
|
+
(u2) => h13("tr", { key: u2.id }, [
|
|
1466
1954
|
td(
|
|
1467
|
-
|
|
1955
|
+
h13(
|
|
1468
1956
|
"button",
|
|
1469
1957
|
{
|
|
1470
1958
|
type: "button",
|
|
@@ -1475,8 +1963,9 @@ var UsersScreen = defineComponent9({
|
|
|
1475
1963
|
)
|
|
1476
1964
|
),
|
|
1477
1965
|
td(
|
|
1478
|
-
`${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() ||
|
|
1966
|
+
`${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || h13("span", { style: styles.muted }, "\u2014")
|
|
1479
1967
|
),
|
|
1968
|
+
props.billingClient ? td(planCell(u2.id)) : null,
|
|
1480
1969
|
td(new Date(u2.createdAt).toLocaleDateString()),
|
|
1481
1970
|
td(
|
|
1482
1971
|
u2.suspended ? pill("warn", "suspended") : u2.deletedAt ? pill("neutral", "deleted") : pill("ok", "active")
|
|
@@ -1484,24 +1973,24 @@ var UsersScreen = defineComponent9({
|
|
|
1484
1973
|
])
|
|
1485
1974
|
)
|
|
1486
1975
|
),
|
|
1487
|
-
list.isLoading.value ?
|
|
1976
|
+
list.isLoading.value ? h13("p", { style: styles.status }, "Loading\u2026") : null,
|
|
1488
1977
|
list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
|
|
1489
1978
|
]) : null,
|
|
1490
|
-
error.value ?
|
|
1979
|
+
error.value ? h13(
|
|
1491
1980
|
"p",
|
|
1492
1981
|
{ style: styles.error, role: "alert" },
|
|
1493
1982
|
error.value.status === 404 ? "No user with that email." : error.value.explanation
|
|
1494
1983
|
) : null,
|
|
1495
1984
|
u ? [
|
|
1496
|
-
|
|
1985
|
+
h13("h2", { style: styles.subtitle }, [
|
|
1497
1986
|
u.firstName || u.lastName ? `${u.firstName} ${u.lastName}`.trim() : u.email,
|
|
1498
1987
|
" ",
|
|
1499
1988
|
u.suspended ? pill("warn", "suspended") : null,
|
|
1500
1989
|
u.deletedAt ? pill("neutral", "deleted") : null
|
|
1501
1990
|
]),
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
row("id",
|
|
1991
|
+
h13("table", { style: styles.table }, [
|
|
1992
|
+
h13("tbody", [
|
|
1993
|
+
row("id", h13("span", { style: styles.mono }, u.id)),
|
|
1505
1994
|
row("email", [u.email, " \xB7 verified ", yesNo(u.isEmailVerified)]),
|
|
1506
1995
|
row("MFA", yesNo(u.mfaEnabled)),
|
|
1507
1996
|
row(
|
|
@@ -1510,12 +1999,12 @@ var UsersScreen = defineComponent9({
|
|
|
1510
1999
|
),
|
|
1511
2000
|
row(
|
|
1512
2001
|
"last login",
|
|
1513
|
-
u.lastLogin ? new Date(u.lastLogin).toLocaleString() :
|
|
2002
|
+
u.lastLogin ? new Date(u.lastLogin).toLocaleString() : h13("span", { style: styles.muted }, "never")
|
|
1514
2003
|
),
|
|
1515
2004
|
row("created", new Date(u.createdAt).toLocaleString())
|
|
1516
2005
|
])
|
|
1517
2006
|
]),
|
|
1518
|
-
|
|
2007
|
+
h13("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
|
|
1519
2008
|
u.suspended ? actionButton("Unsuspend", isLoading.value, () => void unsuspend()) : actionButton(
|
|
1520
2009
|
"Suspend",
|
|
1521
2010
|
isLoading.value,
|
|
@@ -1528,19 +2017,26 @@ var UsersScreen = defineComponent9({
|
|
|
1528
2017
|
() => void revokeSessions().then(() => sessions.refresh())
|
|
1529
2018
|
)
|
|
1530
2019
|
]),
|
|
1531
|
-
|
|
1532
|
-
|
|
2020
|
+
props.billingClient ? h13("h2", { style: styles.subtitle }, "Plan & credits") : null,
|
|
2021
|
+
props.billingClient ? h13(SubscriberBilling, {
|
|
2022
|
+
key: u.id,
|
|
2023
|
+
client: props.billingClient,
|
|
2024
|
+
subscriberType: "user",
|
|
2025
|
+
subscriberId: u.id
|
|
2026
|
+
}) : null,
|
|
2027
|
+
h13("h2", { style: styles.subtitle }, "Live sessions"),
|
|
2028
|
+
sessions.sessions.value.length === 0 ? h13("p", { style: styles.muted }, "None.") : h13(
|
|
1533
2029
|
"ul",
|
|
1534
2030
|
{ style: styles.list },
|
|
1535
2031
|
sessions.sessions.value.map(
|
|
1536
|
-
(s) =>
|
|
1537
|
-
|
|
2032
|
+
(s) => h13("li", { key: s.id, style: styles.row }, [
|
|
2033
|
+
h13("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
|
|
1538
2034
|
" ",
|
|
1539
|
-
|
|
2035
|
+
h13("span", { style: styles.muted }, describeLocation(s.location) ?? ""),
|
|
1540
2036
|
" ",
|
|
1541
|
-
|
|
2037
|
+
h13("span", { style: styles.muted }, s.userAgent ?? ""),
|
|
1542
2038
|
" ",
|
|
1543
|
-
|
|
2039
|
+
h13(
|
|
1544
2040
|
"span",
|
|
1545
2041
|
{ style: styles.muted },
|
|
1546
2042
|
`since ${new Date(s.createdAt).toLocaleString()}`
|
|
@@ -1548,22 +2044,22 @@ var UsersScreen = defineComponent9({
|
|
|
1548
2044
|
])
|
|
1549
2045
|
)
|
|
1550
2046
|
),
|
|
1551
|
-
|
|
1552
|
-
history.events.value.length === 0 && !history.isLoading.value ?
|
|
2047
|
+
h13("h2", { style: styles.subtitle }, "Recent sign-ins"),
|
|
2048
|
+
history.events.value.length === 0 && !history.isLoading.value ? h13("p", { style: styles.muted }, "None recorded.") : h13(
|
|
1553
2049
|
"ul",
|
|
1554
2050
|
{ style: styles.list },
|
|
1555
2051
|
history.events.value.map(
|
|
1556
|
-
(e) =>
|
|
2052
|
+
(e) => h13("li", { key: e.id, style: styles.row }, [
|
|
1557
2053
|
pill(e.outcome === "success" ? "ok" : "bad", e.outcome),
|
|
1558
2054
|
" ",
|
|
1559
|
-
|
|
2055
|
+
h13("span", { style: styles.muted }, e.method),
|
|
1560
2056
|
" ",
|
|
1561
|
-
|
|
2057
|
+
h13("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
|
|
1562
2058
|
" ",
|
|
1563
|
-
|
|
2059
|
+
h13("span", { style: styles.muted }, describeLocation(e.location) ?? ""),
|
|
1564
2060
|
e.location?.proxy || e.location?.hosting ? [" ", pill("warn", e.location.proxy ? "proxy/VPN" : "hosting")] : "",
|
|
1565
2061
|
" ",
|
|
1566
|
-
|
|
2062
|
+
h13("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
|
|
1567
2063
|
])
|
|
1568
2064
|
)
|
|
1569
2065
|
),
|
|
@@ -1576,8 +2072,8 @@ var UsersScreen = defineComponent9({
|
|
|
1576
2072
|
|
|
1577
2073
|
// src/screens/CatalogScreen.ts
|
|
1578
2074
|
import { useAdminCatalog } from "@fonderie/vue-admin";
|
|
1579
|
-
import { defineComponent as
|
|
1580
|
-
var CatalogScreen =
|
|
2075
|
+
import { defineComponent as defineComponent12, h as h14 } from "vue";
|
|
2076
|
+
var CatalogScreen = defineComponent12({
|
|
1581
2077
|
name: "FonderieCatalogScreen",
|
|
1582
2078
|
props: { client: { type: Object, required: true } },
|
|
1583
2079
|
setup(props) {
|
|
@@ -1585,17 +2081,17 @@ var CatalogScreen = defineComponent10({
|
|
|
1585
2081
|
const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
|
|
1586
2082
|
return () => {
|
|
1587
2083
|
const c = catalog.value;
|
|
1588
|
-
return
|
|
2084
|
+
return h14("div", { style: styles.container }, [
|
|
1589
2085
|
pageHeader(
|
|
1590
2086
|
"Catalog",
|
|
1591
2087
|
"What you sell: plans as configured in code, and as stored in the database, side by side.",
|
|
1592
|
-
[
|
|
2088
|
+
[refreshButton2("Refresh", isLoading.value, () => void refresh())]
|
|
1593
2089
|
),
|
|
1594
|
-
error.value ?
|
|
1595
|
-
isLoading.value && !c ?
|
|
2090
|
+
error.value ? h14("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
|
|
2091
|
+
isLoading.value && !c ? h14("p", { style: styles.status }, "Loading\u2026") : null,
|
|
1596
2092
|
c ? [
|
|
1597
|
-
|
|
1598
|
-
|
|
2093
|
+
h14("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
|
|
2094
|
+
h14(
|
|
1599
2095
|
"pre",
|
|
1600
2096
|
{
|
|
1601
2097
|
style: {
|
|
@@ -1609,7 +2105,7 @@ var CatalogScreen = defineComponent10({
|
|
|
1609
2105
|
},
|
|
1610
2106
|
JSON.stringify(c.configured, null, 2)
|
|
1611
2107
|
),
|
|
1612
|
-
|
|
2108
|
+
h14("h2", { style: styles.subtitle }, "Stored (database)"),
|
|
1613
2109
|
c.stored.length === 0 ? empty(
|
|
1614
2110
|
"No stored plans",
|
|
1615
2111
|
"Plans are configured in code; nothing has been written to the database.",
|
|
@@ -1617,10 +2113,10 @@ var CatalogScreen = defineComponent10({
|
|
|
1617
2113
|
) : table(
|
|
1618
2114
|
["Plan", "Tier", "Seats", "Monthly", "Yearly", "Trial", ""],
|
|
1619
2115
|
c.stored.map(
|
|
1620
|
-
(p) =>
|
|
2116
|
+
(p) => h14("tr", { key: p.id }, [
|
|
1621
2117
|
td([
|
|
1622
|
-
|
|
1623
|
-
p.description ?
|
|
2118
|
+
h14("strong", p.name),
|
|
2119
|
+
p.description ? h14("div", { style: styles.muted }, p.description) : ""
|
|
1624
2120
|
]),
|
|
1625
2121
|
td(String(p.tier)),
|
|
1626
2122
|
td(p.seats == null ? "\u221E" : String(p.seats)),
|
|
@@ -1628,7 +2124,7 @@ var CatalogScreen = defineComponent10({
|
|
|
1628
2124
|
td(money(p.pricing?.yearly)),
|
|
1629
2125
|
td(p.trialDays ? `${p.trialDays} d` : "\u2014"),
|
|
1630
2126
|
td(
|
|
1631
|
-
|
|
2127
|
+
h14(
|
|
1632
2128
|
"button",
|
|
1633
2129
|
{
|
|
1634
2130
|
type: "button",
|
|
@@ -1652,259 +2148,171 @@ var CatalogScreen = defineComponent10({
|
|
|
1652
2148
|
});
|
|
1653
2149
|
|
|
1654
2150
|
// src/screens/SubscriberScreen.ts
|
|
1655
|
-
import {
|
|
1656
|
-
import { defineComponent as
|
|
1657
|
-
var
|
|
2151
|
+
import { useAdminSubscribers } from "@fonderie/vue-admin";
|
|
2152
|
+
import { defineComponent as defineComponent13, h as h15, ref as ref7 } from "vue";
|
|
2153
|
+
var FILTERS = [
|
|
2154
|
+
{ label: "All", match: () => true },
|
|
2155
|
+
{ label: "Active", match: (s) => s === "active" },
|
|
2156
|
+
{ label: "Trialing", match: (s) => s === "trialing" },
|
|
2157
|
+
{ label: "Past due", match: (s) => s === "past_due" || s === "unpaid" },
|
|
2158
|
+
{ label: "Canceled", match: (s) => s === "canceled" }
|
|
2159
|
+
];
|
|
2160
|
+
var SubscriberScreen = defineComponent13({
|
|
1658
2161
|
name: "FonderieSubscriberScreen",
|
|
1659
2162
|
props: {
|
|
1660
2163
|
client: { type: Object, required: true },
|
|
1661
|
-
pageSize: { type: Number, default: 50 }
|
|
2164
|
+
pageSize: { type: Number, default: 50 },
|
|
2165
|
+
/**
|
|
2166
|
+
* A user subscriber opens on the Users page, where their plan and credits
|
|
2167
|
+
* live beside their account. Omit to open everything here.
|
|
2168
|
+
*/
|
|
2169
|
+
onOpenUser: { type: Function, default: void 0 }
|
|
1662
2170
|
},
|
|
1663
2171
|
setup(props) {
|
|
1664
|
-
const type = ref4("user");
|
|
1665
|
-
const idInput = ref4("");
|
|
1666
|
-
const subscriber = ref4(null);
|
|
1667
2172
|
const list = useAdminSubscribers(props.client, { limit: props.pageSize });
|
|
1668
|
-
const
|
|
1669
|
-
const open = (
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
2173
|
+
const filter = ref7("All");
|
|
2174
|
+
const open = ref7(null);
|
|
2175
|
+
const type = ref7("workspace");
|
|
2176
|
+
const idInput = ref7("");
|
|
2177
|
+
const openRow = (t2, id) => {
|
|
2178
|
+
if (t2 === "user" && props.onOpenUser) props.onOpenUser(id);
|
|
2179
|
+
else open.value = { type: t2, id };
|
|
1674
2180
|
};
|
|
1675
|
-
const amount = ref4("");
|
|
1676
|
-
const note = ref4("");
|
|
1677
|
-
const granted = ref4(null);
|
|
1678
|
-
const input = (model, placeholder, extra = {}) => h13("input", {
|
|
1679
|
-
value: model.value,
|
|
1680
|
-
onInput: (e) => model.value = e.target.value,
|
|
1681
|
-
placeholder,
|
|
1682
|
-
style: { ...styles.input, ...extra },
|
|
1683
|
-
"aria-label": placeholder
|
|
1684
|
-
});
|
|
1685
2181
|
return () => {
|
|
1686
|
-
const
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
"Who is subscribed. Open one for their plan, wallet and ledger, and to grant credit."
|
|
1692
|
-
),
|
|
1693
|
-
h13(
|
|
1694
|
-
"form",
|
|
1695
|
-
{
|
|
1696
|
-
style: styles.toolbar,
|
|
1697
|
-
onSubmit: (e) => {
|
|
1698
|
-
e.preventDefault();
|
|
1699
|
-
granted.value = null;
|
|
1700
|
-
subscriber.value = idInput.value.trim() ? { type: type.value, id: idInput.value.trim() } : null;
|
|
1701
|
-
}
|
|
1702
|
-
},
|
|
1703
|
-
[
|
|
1704
|
-
h13(
|
|
1705
|
-
"select",
|
|
1706
|
-
{
|
|
1707
|
-
value: type.value,
|
|
1708
|
-
onChange: (e) => type.value = e.target.value,
|
|
1709
|
-
style: styles.input,
|
|
1710
|
-
"aria-label": "Type"
|
|
1711
|
-
},
|
|
1712
|
-
[
|
|
1713
|
-
h13("option", { value: "user" }, "user"),
|
|
1714
|
-
h13("option", { value: "workspace" }, "workspace")
|
|
1715
|
-
]
|
|
1716
|
-
),
|
|
1717
|
-
input(idInput, "subscriber id", { minWidth: "280px" }),
|
|
1718
|
-
h13(
|
|
2182
|
+
const o = open.value;
|
|
2183
|
+
if (o) {
|
|
2184
|
+
return h15("div", { style: styles.container }, [
|
|
2185
|
+
pageHeader(`${o.type}/${o.id}`, "Plan, credits and what moved.", [
|
|
2186
|
+
h15(
|
|
1719
2187
|
"button",
|
|
1720
|
-
{ type: "
|
|
1721
|
-
[icon("
|
|
1722
|
-
),
|
|
1723
|
-
subscriber.value ? h13(
|
|
1724
|
-
"button",
|
|
1725
|
-
{
|
|
1726
|
-
type: "button",
|
|
1727
|
-
style: styles.button,
|
|
1728
|
-
onClick: () => {
|
|
1729
|
-
subscriber.value = null;
|
|
1730
|
-
idInput.value = "";
|
|
1731
|
-
granted.value = null;
|
|
1732
|
-
}
|
|
1733
|
-
},
|
|
1734
|
-
[icon("back", 14), "All subscribers"]
|
|
1735
|
-
) : h13(
|
|
1736
|
-
"button",
|
|
1737
|
-
{
|
|
1738
|
-
type: "button",
|
|
1739
|
-
style: styles.button,
|
|
1740
|
-
disabled: list.isLoading.value,
|
|
1741
|
-
onClick: () => void list.refresh()
|
|
1742
|
-
},
|
|
1743
|
-
"Refresh"
|
|
2188
|
+
{ type: "button", style: styles.buttonGhost, onClick: () => open.value = null },
|
|
2189
|
+
[icon("back", 14), "All subscriptions"]
|
|
1744
2190
|
)
|
|
1745
|
-
]
|
|
2191
|
+
]),
|
|
2192
|
+
h15(SubscriberBilling, {
|
|
2193
|
+
key: `${o.type}/${o.id}`,
|
|
2194
|
+
client: props.client,
|
|
2195
|
+
subscriberType: o.type,
|
|
2196
|
+
subscriberId: o.id
|
|
2197
|
+
})
|
|
2198
|
+
]);
|
|
2199
|
+
}
|
|
2200
|
+
const match = FILTERS.find((f) => f.label === filter.value)?.match ?? (() => true);
|
|
2201
|
+
const rows = list.subscriptions.value.filter((s) => match(s.status));
|
|
2202
|
+
return h15("div", { style: styles.container }, [
|
|
2203
|
+
pageHeader(
|
|
2204
|
+
"Subscriptions",
|
|
2205
|
+
"Who pays, and who is behind. A user's plan and credits are also on their page under Users \u2014 free users appear only there.",
|
|
2206
|
+
[refreshButton(() => void list.refresh(), list.isLoading.value)]
|
|
1746
2207
|
),
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd).toLocaleDateString() : h13("span", { style: styles.muted }, "\u2014")
|
|
1778
|
-
)
|
|
1779
|
-
])
|
|
1780
|
-
)
|
|
2208
|
+
h15("div", { style: { ...styles.toolbar, justifyContent: "space-between" } }, [
|
|
2209
|
+
h15(
|
|
2210
|
+
"fieldset",
|
|
2211
|
+
{
|
|
2212
|
+
style: {
|
|
2213
|
+
display: "flex",
|
|
2214
|
+
gap: "6px",
|
|
2215
|
+
flexWrap: "wrap",
|
|
2216
|
+
border: "none",
|
|
2217
|
+
margin: 0,
|
|
2218
|
+
padding: 0,
|
|
2219
|
+
minWidth: 0
|
|
2220
|
+
},
|
|
2221
|
+
"aria-label": "Filter by status"
|
|
2222
|
+
},
|
|
2223
|
+
FILTERS.map((f) => {
|
|
2224
|
+
const n = list.subscriptions.value.filter((s) => f.match(s.status)).length;
|
|
2225
|
+
const on = filter.value === f.label;
|
|
2226
|
+
return h15(
|
|
2227
|
+
"button",
|
|
2228
|
+
{
|
|
2229
|
+
key: f.label,
|
|
2230
|
+
type: "button",
|
|
2231
|
+
"aria-pressed": on,
|
|
2232
|
+
onClick: () => filter.value = f.label,
|
|
2233
|
+
style: { ...on ? styles.buttonPrimary : styles.button, height: "28px" }
|
|
2234
|
+
},
|
|
2235
|
+
[f.label, h15("span", { style: { opacity: 0.7 } }, String(n))]
|
|
2236
|
+
);
|
|
2237
|
+
})
|
|
1781
2238
|
),
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
"button",
|
|
2239
|
+
h15(
|
|
2240
|
+
"form",
|
|
1785
2241
|
{
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
2242
|
+
style: { display: "flex", gap: "6px" },
|
|
2243
|
+
onSubmit: (e) => {
|
|
2244
|
+
e.preventDefault();
|
|
2245
|
+
if (idInput.value.trim()) openRow(type.value, idInput.value.trim());
|
|
2246
|
+
}
|
|
1789
2247
|
},
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
letterSpacing: "var(--fonderie-tracking-display,-0.05em)"
|
|
1832
|
-
}
|
|
1833
|
-
},
|
|
1834
|
-
w.balance
|
|
1835
|
-
),
|
|
1836
|
-
` ${w.currency} `,
|
|
1837
|
-
h13(
|
|
1838
|
-
"span",
|
|
1839
|
-
{ style: styles.muted },
|
|
1840
|
-
`(granted ${w.granted ?? "0"} \xB7 purchased ${w.purchased ?? "0"}) \xB7 minor units, precision ${w.precision}`
|
|
1841
|
-
)
|
|
1842
|
-
]
|
|
1843
|
-
),
|
|
1844
|
-
h13(
|
|
1845
|
-
"form",
|
|
1846
|
-
{
|
|
1847
|
-
style: styles.toolbar,
|
|
1848
|
-
onSubmit: (e) => {
|
|
1849
|
-
e.preventDefault();
|
|
1850
|
-
const sub = subscriber.value;
|
|
1851
|
-
if (!sub) return;
|
|
1852
|
-
const key = `admin-grant-${sub.type}-${sub.id}-${Date.now()}`;
|
|
1853
|
-
const amt = amount.value.trim();
|
|
1854
|
-
void grant({
|
|
1855
|
-
amount: amt,
|
|
1856
|
-
...note.value ? { description: note.value } : {},
|
|
1857
|
-
idempotencyKey: key
|
|
1858
|
-
}).then(() => {
|
|
1859
|
-
granted.value = `Granted ${amt} ${w.currency}.`;
|
|
1860
|
-
amount.value = "";
|
|
1861
|
-
note.value = "";
|
|
1862
|
-
}).catch(() => {
|
|
1863
|
-
});
|
|
1864
|
-
}
|
|
1865
|
-
},
|
|
1866
|
-
[
|
|
1867
|
-
input(amount, "amount (minor units)"),
|
|
1868
|
-
input(note, "reason (optional)", { minWidth: "220px" }),
|
|
1869
|
-
h13(
|
|
2248
|
+
[
|
|
2249
|
+
h15(
|
|
2250
|
+
"select",
|
|
2251
|
+
{
|
|
2252
|
+
value: type.value,
|
|
2253
|
+
onChange: (e) => type.value = e.target.value,
|
|
2254
|
+
style: styles.input,
|
|
2255
|
+
"aria-label": "Type"
|
|
2256
|
+
},
|
|
2257
|
+
[
|
|
2258
|
+
h15("option", { value: "workspace" }, "workspace"),
|
|
2259
|
+
h15("option", { value: "user" }, "user")
|
|
2260
|
+
]
|
|
2261
|
+
),
|
|
2262
|
+
h15("input", {
|
|
2263
|
+
value: idInput.value,
|
|
2264
|
+
onInput: (e) => idInput.value = e.target.value,
|
|
2265
|
+
placeholder: "subscriber id",
|
|
2266
|
+
style: { ...styles.input, width: "220px" },
|
|
2267
|
+
"aria-label": "Subscriber id"
|
|
2268
|
+
}),
|
|
2269
|
+
h15(
|
|
2270
|
+
"button",
|
|
2271
|
+
{ type: "submit", style: styles.button, disabled: !idInput.value.trim() },
|
|
2272
|
+
[icon("search", 14), "Open"]
|
|
2273
|
+
)
|
|
2274
|
+
]
|
|
2275
|
+
)
|
|
2276
|
+
]),
|
|
2277
|
+
list.error.value ? h15("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
|
|
2278
|
+
rows.length === 0 && !list.isLoading.value ? empty(
|
|
2279
|
+
filter.value === "All" ? "No subscriptions yet" : `No ${filter.value.toLowerCase()} subscriptions`,
|
|
2280
|
+
"Subscriptions appear here once someone checks out.",
|
|
2281
|
+
"subscriber"
|
|
2282
|
+
) : table(
|
|
2283
|
+
["Subscriber", "Plan", "Status", "Renews / ends"],
|
|
2284
|
+
rows.map((s) => {
|
|
2285
|
+
const end = periodEnd(s);
|
|
2286
|
+
return h15("tr", { key: s.id }, [
|
|
2287
|
+
td(
|
|
2288
|
+
h15(
|
|
1870
2289
|
"button",
|
|
1871
2290
|
{
|
|
1872
|
-
type: "
|
|
1873
|
-
style: styles.
|
|
1874
|
-
|
|
2291
|
+
type: "button",
|
|
2292
|
+
style: { ...styles.link, ...styles.mono },
|
|
2293
|
+
onClick: () => openRow(s.subscriberType, s.subscriberId)
|
|
1875
2294
|
},
|
|
1876
|
-
|
|
1877
|
-
)
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
(
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2295
|
+
`${s.subscriberType}/${s.subscriberId}`
|
|
2296
|
+
)
|
|
2297
|
+
),
|
|
2298
|
+
td([
|
|
2299
|
+
h15("strong", s.plan),
|
|
2300
|
+
" ",
|
|
2301
|
+
h15("span", { style: styles.muted }, `\xB7 ${s.interval}`)
|
|
2302
|
+
]),
|
|
2303
|
+
td([
|
|
2304
|
+
pill(statusTone(s.status), s.status),
|
|
2305
|
+
s.cancelAtPeriodEnd && s.status !== "canceled" ? h15("span", [" ", pill("warn", "cancels", false)]) : null
|
|
2306
|
+
]),
|
|
2307
|
+
td(
|
|
2308
|
+
end ? [h15("span", { style: styles.muted }, end.label), ` ${end.date}`] : h15("span", { style: styles.muted }, "\u2014"),
|
|
2309
|
+
{ whiteSpace: "nowrap" }
|
|
1891
2310
|
)
|
|
1892
|
-
)
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
style: { ...styles.button, marginTop: "8px" },
|
|
1898
|
-
onClick: () => void loadMoreLedger()
|
|
1899
|
-
},
|
|
1900
|
-
"Load more"
|
|
1901
|
-
) : null
|
|
1902
|
-
] : h13(
|
|
1903
|
-
"p",
|
|
1904
|
-
{ style: styles.muted },
|
|
1905
|
-
"No wallet \u2014 billing has no wallet configuration."
|
|
1906
|
-
)
|
|
1907
|
-
] : null
|
|
2311
|
+
]);
|
|
2312
|
+
})
|
|
2313
|
+
),
|
|
2314
|
+
list.isLoading.value ? h15("p", { style: styles.status }, "Loading\u2026") : null,
|
|
2315
|
+
list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
|
|
1908
2316
|
]);
|
|
1909
2317
|
};
|
|
1910
2318
|
}
|
|
@@ -1912,33 +2320,33 @@ var SubscriberScreen = defineComponent11({
|
|
|
1912
2320
|
|
|
1913
2321
|
// src/screens/AuditScreen.ts
|
|
1914
2322
|
import { useAdminAudit } from "@fonderie/vue-admin";
|
|
1915
|
-
import { defineComponent as
|
|
1916
|
-
var AuditScreen =
|
|
2323
|
+
import { defineComponent as defineComponent14, h as h16, ref as ref8 } from "vue";
|
|
2324
|
+
var AuditScreen = defineComponent14({
|
|
1917
2325
|
name: "FonderieAuditScreen",
|
|
1918
2326
|
props: {
|
|
1919
2327
|
client: { type: Object, required: true },
|
|
1920
2328
|
pageSize: { type: Number, default: 50 }
|
|
1921
2329
|
},
|
|
1922
2330
|
setup(props) {
|
|
1923
|
-
const draft =
|
|
1924
|
-
const query =
|
|
2331
|
+
const draft = ref8({ workspaceId: "", type: "", actorId: "" });
|
|
2332
|
+
const query = ref8({ limit: props.pageSize });
|
|
1925
2333
|
const { events, hasMore, isLoading, error, refresh, loadMore } = useAdminAudit(
|
|
1926
2334
|
props.client,
|
|
1927
2335
|
query
|
|
1928
2336
|
);
|
|
1929
|
-
const field = (key, placeholder) =>
|
|
2337
|
+
const field = (key, placeholder) => h16("input", {
|
|
1930
2338
|
value: draft.value[key],
|
|
1931
2339
|
onInput: (e) => draft.value = { ...draft.value, [key]: e.target.value },
|
|
1932
2340
|
placeholder,
|
|
1933
2341
|
style: styles.input,
|
|
1934
2342
|
"aria-label": placeholder
|
|
1935
2343
|
});
|
|
1936
|
-
return () =>
|
|
2344
|
+
return () => h16("div", { style: styles.container }, [
|
|
1937
2345
|
pageHeader(
|
|
1938
2346
|
"Audit",
|
|
1939
2347
|
"What happened, across every workspace unless you name one. The chain's integrity verdict is on the Doctor page."
|
|
1940
2348
|
),
|
|
1941
|
-
|
|
2349
|
+
h16(
|
|
1942
2350
|
"form",
|
|
1943
2351
|
{
|
|
1944
2352
|
style: styles.toolbar,
|
|
@@ -1957,19 +2365,19 @@ var AuditScreen = defineComponent12({
|
|
|
1957
2365
|
field("workspaceId", "workspace id (all if empty)"),
|
|
1958
2366
|
field("type", "event type"),
|
|
1959
2367
|
field("actorId", "actor id"),
|
|
1960
|
-
|
|
2368
|
+
h16(
|
|
1961
2369
|
"button",
|
|
1962
2370
|
{ type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
|
|
1963
2371
|
[icon("search", 14), "Filter"]
|
|
1964
2372
|
),
|
|
1965
|
-
|
|
2373
|
+
refreshButton2("Refresh", isLoading.value, () => void refresh())
|
|
1966
2374
|
]
|
|
1967
2375
|
),
|
|
1968
|
-
error.value ?
|
|
2376
|
+
error.value ? h16("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
|
|
1969
2377
|
events.value.length === 0 && !isLoading.value ? empty("Nothing recorded for this filter", void 0, "audit") : table(
|
|
1970
2378
|
["When", "Type", "Workspace", "Actor", "Request"],
|
|
1971
2379
|
events.value.map(
|
|
1972
|
-
(e) =>
|
|
2380
|
+
(e) => h16("tr", { key: e.id }, [
|
|
1973
2381
|
td(new Date(e.createdAt).toLocaleString(), styles.muted),
|
|
1974
2382
|
td(e.type, styles.mono),
|
|
1975
2383
|
td(String(e.payload["workspaceId"] ?? "\u2014"), styles.mono),
|
|
@@ -1978,7 +2386,7 @@ var AuditScreen = defineComponent12({
|
|
|
1978
2386
|
])
|
|
1979
2387
|
)
|
|
1980
2388
|
),
|
|
1981
|
-
isLoading.value ?
|
|
2389
|
+
isLoading.value ? h16("p", { style: styles.status }, "Loading\u2026") : null,
|
|
1982
2390
|
hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
|
|
1983
2391
|
]);
|
|
1984
2392
|
}
|
|
@@ -2001,7 +2409,7 @@ var NAV = [
|
|
|
2001
2409
|
group: "Money",
|
|
2002
2410
|
items: [
|
|
2003
2411
|
{ page: "catalog", label: "Catalog", needs: "billing" },
|
|
2004
|
-
{ page: "subscriber", label: "
|
|
2412
|
+
{ page: "subscriber", label: "Subscriptions", needs: "billing" }
|
|
2005
2413
|
]
|
|
2006
2414
|
},
|
|
2007
2415
|
{ group: "Settings", items: [{ page: "settings", label: "Config & secrets", needs: "config" }] },
|
|
@@ -2011,7 +2419,8 @@ var NAV = [
|
|
|
2011
2419
|
items: [
|
|
2012
2420
|
{ page: "audit", label: "Audit", needs: "audit" },
|
|
2013
2421
|
{ page: "log", label: "Admin log" },
|
|
2014
|
-
{ page: "
|
|
2422
|
+
{ page: "operators", label: "Operators", needs: "operators" },
|
|
2423
|
+
{ page: "tokens", label: "Tokens" },
|
|
2015
2424
|
{ page: "migrations", label: "Migrations" }
|
|
2016
2425
|
]
|
|
2017
2426
|
}
|
|
@@ -2030,10 +2439,11 @@ var ICON = {
|
|
|
2030
2439
|
audit: "audit",
|
|
2031
2440
|
log: "log",
|
|
2032
2441
|
tokens: "tokens",
|
|
2442
|
+
operators: "users",
|
|
2033
2443
|
migrations: "migrations"
|
|
2034
2444
|
};
|
|
2035
2445
|
var NARROW = "(max-width: 820px)";
|
|
2036
|
-
var AdminShell =
|
|
2446
|
+
var AdminShell = defineComponent15({
|
|
2037
2447
|
name: "FonderieAdminShell",
|
|
2038
2448
|
props: {
|
|
2039
2449
|
client: { type: Object, required: true },
|
|
@@ -2055,62 +2465,86 @@ var AdminShell = defineComponent13({
|
|
|
2055
2465
|
appName: { type: String, default: "Admin" },
|
|
2056
2466
|
// A badge beside the name — the deployment's environment ("production").
|
|
2057
2467
|
// Production is tinted so an operator always knows where their clicks land.
|
|
2058
|
-
envLabel: { type: String, default: void 0 }
|
|
2468
|
+
envLabel: { type: String, default: void 0 },
|
|
2469
|
+
// The deployment has operator accounts (@fonderie/admin with a store):
|
|
2470
|
+
// shows the Operators page.
|
|
2471
|
+
operators: { type: Boolean, default: false },
|
|
2472
|
+
// The signed-in operator's email, when a person (not a token) is signed in.
|
|
2473
|
+
currentOperator: { type: String, default: void 0 }
|
|
2059
2474
|
},
|
|
2060
2475
|
// `footer` slot: pinned to the bottom of the sidebar — session controls
|
|
2061
2476
|
// (theme, sign out). The React shell takes the same thing as a `footer` prop.
|
|
2062
2477
|
emits: { navigate: (_page) => true },
|
|
2063
2478
|
setup(props, { emit, slots }) {
|
|
2064
2479
|
const query = typeof window !== "undefined" && window.matchMedia ? window.matchMedia(NARROW) : null;
|
|
2065
|
-
const narrow =
|
|
2066
|
-
const drawer =
|
|
2480
|
+
const narrow = ref9(query?.matches ?? false);
|
|
2481
|
+
const drawer = ref9(false);
|
|
2067
2482
|
const onMedia = (e) => narrow.value = e.matches;
|
|
2068
2483
|
onMounted(() => query?.addEventListener("change", onMedia));
|
|
2069
2484
|
onBeforeUnmount(() => query?.removeEventListener("change", onMedia));
|
|
2070
|
-
const own =
|
|
2071
|
-
const current =
|
|
2072
|
-
const editing =
|
|
2485
|
+
const own = ref9("attention");
|
|
2486
|
+
const current = computed3(() => props.page ?? own.value);
|
|
2487
|
+
const editing = ref9(null);
|
|
2488
|
+
const openUser = ref9(void 0);
|
|
2073
2489
|
const go = (p) => {
|
|
2074
2490
|
editing.value = null;
|
|
2075
2491
|
emit("navigate", p);
|
|
2076
2492
|
if (props.page === void 0) own.value = p;
|
|
2077
2493
|
drawer.value = false;
|
|
2078
2494
|
};
|
|
2079
|
-
const back = (label) =>
|
|
2080
|
-
|
|
2495
|
+
const back = (label) => h17("div", { style: { padding: "24px 40px 0" } }, [
|
|
2496
|
+
h17(
|
|
2081
2497
|
"button",
|
|
2082
2498
|
{ type: "button", style: styles.buttonGhost, onClick: () => editing.value = null },
|
|
2083
2499
|
[icon("back", 14), label]
|
|
2084
2500
|
)
|
|
2085
2501
|
]);
|
|
2086
|
-
const missing = (text) =>
|
|
2502
|
+
const missing = (text) => h17("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
|
|
2087
2503
|
const body = () => {
|
|
2088
2504
|
const c = props.client;
|
|
2089
2505
|
switch (current.value) {
|
|
2090
2506
|
case "attention":
|
|
2091
|
-
return
|
|
2507
|
+
return h17(AttentionScreen, { client: c });
|
|
2092
2508
|
case "modules":
|
|
2093
|
-
return
|
|
2509
|
+
return h17(ModulesScreen, { client: c });
|
|
2094
2510
|
case "doctor":
|
|
2095
|
-
return
|
|
2511
|
+
return h17(DoctorScreen, { client: c });
|
|
2096
2512
|
case "environment":
|
|
2097
|
-
return
|
|
2513
|
+
return h17(EnvironmentScreen, { client: c });
|
|
2098
2514
|
case "routes":
|
|
2099
|
-
return
|
|
2515
|
+
return h17(RoutesScreen, { client: c });
|
|
2100
2516
|
case "tokens":
|
|
2101
|
-
return
|
|
2517
|
+
return h17(TokensScreen, { client: c });
|
|
2518
|
+
case "operators":
|
|
2519
|
+
return h17(OperatorsScreen, {
|
|
2520
|
+
client: c,
|
|
2521
|
+
...props.currentOperator ? { me: props.currentOperator } : {}
|
|
2522
|
+
});
|
|
2102
2523
|
case "migrations":
|
|
2103
|
-
return
|
|
2524
|
+
return h17(MigrationsScreen, { client: c });
|
|
2104
2525
|
case "log":
|
|
2105
|
-
return
|
|
2526
|
+
return h17(AdminLogScreen, { client: c });
|
|
2106
2527
|
case "catalog":
|
|
2107
|
-
return props.billingClient ?
|
|
2528
|
+
return props.billingClient ? h17(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
|
|
2108
2529
|
case "subscriber":
|
|
2109
|
-
return props.billingClient ?
|
|
2530
|
+
return props.billingClient ? h17(SubscriberScreen, {
|
|
2531
|
+
client: props.billingClient,
|
|
2532
|
+
...props.authClient ? {
|
|
2533
|
+
onOpenUser: (id) => {
|
|
2534
|
+
openUser.value = id;
|
|
2535
|
+
go("users");
|
|
2536
|
+
}
|
|
2537
|
+
} : {}
|
|
2538
|
+
}) : missing("Pass a BillingAdminClient to look up subscribers here.");
|
|
2110
2539
|
case "audit":
|
|
2111
|
-
return props.auditClient ?
|
|
2540
|
+
return props.auditClient ? h17(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
|
|
2112
2541
|
case "users":
|
|
2113
|
-
return props.authClient ?
|
|
2542
|
+
return props.authClient ? h17(UsersScreen, {
|
|
2543
|
+
key: openUser.value ?? "list",
|
|
2544
|
+
client: props.authClient,
|
|
2545
|
+
...props.billingClient ? { billingClient: props.billingClient } : {},
|
|
2546
|
+
...openUser.value ? { openUserId: openUser.value } : {}
|
|
2547
|
+
}) : missing("Pass an AuthAdminClient to look up users here.");
|
|
2114
2548
|
case "settings": {
|
|
2115
2549
|
const cc = props.configClient;
|
|
2116
2550
|
if (!cc) return missing("Pass a ConfigAdminClient to manage config and secrets here.");
|
|
@@ -2118,7 +2552,7 @@ var AdminShell = defineComponent13({
|
|
|
2118
2552
|
if (e && e.kind !== "template")
|
|
2119
2553
|
return [
|
|
2120
2554
|
back("Config & secrets"),
|
|
2121
|
-
|
|
2555
|
+
h17(ConfigEditorScreen, {
|
|
2122
2556
|
client: cc,
|
|
2123
2557
|
kind: e.kind,
|
|
2124
2558
|
configKey: e.key,
|
|
@@ -2126,7 +2560,7 @@ var AdminShell = defineComponent13({
|
|
|
2126
2560
|
onSaved: () => editing.value = null
|
|
2127
2561
|
})
|
|
2128
2562
|
];
|
|
2129
|
-
return
|
|
2563
|
+
return h17(ConfigListScreen, {
|
|
2130
2564
|
client: cc,
|
|
2131
2565
|
...props.environment !== void 0 ? { environment: props.environment } : {},
|
|
2132
2566
|
"onSelect-config": (key) => editing.value = { kind: "config", key },
|
|
@@ -2143,27 +2577,27 @@ var AdminShell = defineComponent13({
|
|
|
2143
2577
|
if (e?.kind === "template")
|
|
2144
2578
|
return [
|
|
2145
2579
|
back("Templates"),
|
|
2146
|
-
|
|
2580
|
+
h17(TemplateEditorScreen, {
|
|
2147
2581
|
client: kc,
|
|
2148
2582
|
type: e.type,
|
|
2149
2583
|
onSaved: () => editing.value = null
|
|
2150
2584
|
})
|
|
2151
2585
|
];
|
|
2152
|
-
return
|
|
2586
|
+
return h17(TemplateListScreen, {
|
|
2153
2587
|
client: kc,
|
|
2154
2588
|
"onSelect-template": (t2) => editing.value = { kind: "template", type: t2.type }
|
|
2155
2589
|
});
|
|
2156
2590
|
}
|
|
2157
2591
|
}
|
|
2158
2592
|
};
|
|
2159
|
-
const visible = (i) => !i.needs || (i.needs === "config" ? props.configClient : i.needs === "courier" ? props.courierClient : i.needs === "auth" ? props.authClient : i.needs === "billing" ? props.billingClient : props.auditClient);
|
|
2593
|
+
const visible = (i) => !i.needs || (i.needs === "operators" ? props.operators : i.needs === "config" ? props.configClient : i.needs === "courier" ? props.courierClient : i.needs === "auth" ? props.authClient : i.needs === "billing" ? props.billingClient : props.auditClient);
|
|
2160
2594
|
const brand = () => {
|
|
2161
2595
|
const production = props.envLabel ? /^prod/i.test(props.envLabel) : false;
|
|
2162
|
-
return
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
props.envLabel ?
|
|
2596
|
+
return h17("div", { style: styles.navBrand }, [
|
|
2597
|
+
h17("div", { style: styles.navMark }, [icon("mark", 15)]),
|
|
2598
|
+
h17("div", { style: { minWidth: 0, flex: 1 } }, [
|
|
2599
|
+
h17("div", { style: styles.navBrandTitle }, props.appName),
|
|
2600
|
+
props.envLabel ? h17(
|
|
2167
2601
|
"span",
|
|
2168
2602
|
{
|
|
2169
2603
|
style: {
|
|
@@ -2180,7 +2614,7 @@ var AdminShell = defineComponent13({
|
|
|
2180
2614
|
props.envLabel
|
|
2181
2615
|
) : null
|
|
2182
2616
|
]),
|
|
2183
|
-
narrow.value ?
|
|
2617
|
+
narrow.value ? h17(
|
|
2184
2618
|
"button",
|
|
2185
2619
|
{
|
|
2186
2620
|
type: "button",
|
|
@@ -2192,7 +2626,7 @@ var AdminShell = defineComponent13({
|
|
|
2192
2626
|
) : null
|
|
2193
2627
|
]);
|
|
2194
2628
|
};
|
|
2195
|
-
const nav = () =>
|
|
2629
|
+
const nav = () => h17(
|
|
2196
2630
|
"nav",
|
|
2197
2631
|
{
|
|
2198
2632
|
style: {
|
|
@@ -2203,17 +2637,17 @@ var AdminShell = defineComponent13({
|
|
|
2203
2637
|
},
|
|
2204
2638
|
[
|
|
2205
2639
|
brand(),
|
|
2206
|
-
|
|
2640
|
+
h17(
|
|
2207
2641
|
"div",
|
|
2208
2642
|
{ style: styles.navScroll },
|
|
2209
2643
|
NAV.map((g) => {
|
|
2210
2644
|
const items = g.items.filter(visible);
|
|
2211
2645
|
if (items.length === 0) return null;
|
|
2212
|
-
return
|
|
2213
|
-
|
|
2646
|
+
return h17("div", { key: g.group }, [
|
|
2647
|
+
h17("div", { style: styles.navGroup }, g.group),
|
|
2214
2648
|
...items.map((i) => {
|
|
2215
2649
|
const active = current.value === i.page;
|
|
2216
|
-
return
|
|
2650
|
+
return h17(
|
|
2217
2651
|
"button",
|
|
2218
2652
|
{
|
|
2219
2653
|
key: i.page,
|
|
@@ -2221,7 +2655,10 @@ var AdminShell = defineComponent13({
|
|
|
2221
2655
|
class: "fonderie-admin-nav-item",
|
|
2222
2656
|
style: { ...styles.navItem, ...active ? styles.navItemActive : {} },
|
|
2223
2657
|
"aria-current": active ? "page" : void 0,
|
|
2224
|
-
onClick: () =>
|
|
2658
|
+
onClick: () => {
|
|
2659
|
+
openUser.value = void 0;
|
|
2660
|
+
go(i.page);
|
|
2661
|
+
}
|
|
2225
2662
|
},
|
|
2226
2663
|
[icon(ICON[i.page], 16, active ? styles.navIconActive : void 0), i.label]
|
|
2227
2664
|
);
|
|
@@ -2229,18 +2666,18 @@ var AdminShell = defineComponent13({
|
|
|
2229
2666
|
]);
|
|
2230
2667
|
})
|
|
2231
2668
|
),
|
|
2232
|
-
slots["footer"] ?
|
|
2669
|
+
slots["footer"] ? h17("div", { style: styles.navFooter }, slots["footer"]()) : null
|
|
2233
2670
|
]
|
|
2234
2671
|
);
|
|
2235
2672
|
return () => {
|
|
2236
2673
|
const title = NAV.flatMap((g) => g.items).find((i) => i.page === current.value)?.label ?? "";
|
|
2237
|
-
return
|
|
2674
|
+
return h17(
|
|
2238
2675
|
"div",
|
|
2239
2676
|
{ style: { ...styles.shell, ...narrow.value ? { flexDirection: "column" } : {} } },
|
|
2240
2677
|
[
|
|
2241
2678
|
...narrow.value ? [
|
|
2242
|
-
|
|
2243
|
-
|
|
2679
|
+
h17("div", { style: styles.topbar }, [
|
|
2680
|
+
h17(
|
|
2244
2681
|
"button",
|
|
2245
2682
|
{
|
|
2246
2683
|
type: "button",
|
|
@@ -2251,19 +2688,19 @@ var AdminShell = defineComponent13({
|
|
|
2251
2688
|
},
|
|
2252
2689
|
[icon("menu")]
|
|
2253
2690
|
),
|
|
2254
|
-
|
|
2255
|
-
props.envLabel ?
|
|
2691
|
+
h17("span", { style: { fontWeight: 600, fontSize: "14px" } }, title),
|
|
2692
|
+
props.envLabel ? h17("span", { style: { ...styles.badge, marginLeft: "auto" } }, props.envLabel) : null
|
|
2256
2693
|
]),
|
|
2257
|
-
drawer.value ?
|
|
2694
|
+
drawer.value ? h17("div", { style: styles.drawer }, [
|
|
2258
2695
|
nav(),
|
|
2259
|
-
|
|
2696
|
+
h17("div", {
|
|
2260
2697
|
style: styles.scrim,
|
|
2261
2698
|
"aria-hidden": "true",
|
|
2262
2699
|
onClick: () => drawer.value = false
|
|
2263
2700
|
})
|
|
2264
2701
|
]) : null
|
|
2265
2702
|
] : [nav()],
|
|
2266
|
-
|
|
2703
|
+
h17("main", { class: "fonderie-admin-main", style: styles.main }, body())
|
|
2267
2704
|
]
|
|
2268
2705
|
);
|
|
2269
2706
|
};
|
|
@@ -2279,7 +2716,9 @@ export {
|
|
|
2279
2716
|
EnvironmentScreen,
|
|
2280
2717
|
MigrationsScreen,
|
|
2281
2718
|
ModulesScreen,
|
|
2719
|
+
OperatorsScreen,
|
|
2282
2720
|
RoutesScreen,
|
|
2721
|
+
SubscriberBilling,
|
|
2283
2722
|
SubscriberScreen,
|
|
2284
2723
|
TokensScreen,
|
|
2285
2724
|
UsersScreen
|