@crvouga/mockingbird-service-step-functions 1.4.0 → 1.5.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/CHANGELOG.md +10 -0
- package/README.md +2 -2
- package/dist/{chunk-4S65JRDA.js → chunk-FWF7NYHK.js} +1399 -595
- package/dist/chunk-FWF7NYHK.js.map +7 -0
- package/dist/{chunk-OZLOD3CN.js → chunk-UC7X4N5K.js} +2 -2
- package/dist/cli.js +2 -2
- package/dist/index.js +1 -1
- package/dist/server.js +2 -2
- package/package.json +3 -3
- package/dist/chunk-4S65JRDA.js.map +0 -7
- /package/dist/{chunk-OZLOD3CN.js.map → chunk-UC7X4N5K.js.map} +0 -0
|
@@ -541,10 +541,10 @@ var OpenAPIReferenceError = class extends Error {
|
|
|
541
541
|
};
|
|
542
542
|
var unescapePointer = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
543
543
|
var isReference = (value) => typeof value === "object" && value !== null && typeof value.$ref === "string";
|
|
544
|
-
var resolveRef = (
|
|
544
|
+
var resolveRef = (document3, ref) => {
|
|
545
545
|
if (!ref.startsWith("#/"))
|
|
546
546
|
throw new OpenAPIReferenceError(ref);
|
|
547
|
-
let cursor =
|
|
547
|
+
let cursor = document3;
|
|
548
548
|
for (const raw of ref.slice(2).split("/")) {
|
|
549
549
|
const segment = unescapePointer(raw);
|
|
550
550
|
if (typeof cursor !== "object" || cursor === null || !(segment in cursor)) {
|
|
@@ -556,14 +556,14 @@ var resolveRef = (document2, ref) => {
|
|
|
556
556
|
throw new OpenAPIReferenceError(ref);
|
|
557
557
|
return cursor;
|
|
558
558
|
};
|
|
559
|
-
var deref = (
|
|
559
|
+
var deref = (document3, value) => {
|
|
560
560
|
let current = value;
|
|
561
561
|
const seen = /* @__PURE__ */ new Set();
|
|
562
562
|
while (isReference(current)) {
|
|
563
563
|
if (seen.has(current.$ref))
|
|
564
564
|
throw new OpenAPIReferenceError(`${current.$ref} (cycle)`);
|
|
565
565
|
seen.add(current.$ref);
|
|
566
|
-
current = resolveRef(
|
|
566
|
+
current = resolveRef(document3, current.$ref);
|
|
567
567
|
}
|
|
568
568
|
return current;
|
|
569
569
|
};
|
|
@@ -581,36 +581,36 @@ var HTTP_METHODS = [
|
|
|
581
581
|
];
|
|
582
582
|
|
|
583
583
|
// ../../openapi/core/dist/document.js
|
|
584
|
-
var mergeParameters = (
|
|
584
|
+
var mergeParameters = (document3, item, own) => {
|
|
585
585
|
const merged = /* @__PURE__ */ new Map();
|
|
586
586
|
for (const raw of item.parameters ?? []) {
|
|
587
|
-
const parameter = deref(
|
|
587
|
+
const parameter = deref(document3, raw);
|
|
588
588
|
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
589
589
|
}
|
|
590
590
|
for (const raw of own ?? []) {
|
|
591
|
-
const parameter = deref(
|
|
591
|
+
const parameter = deref(document3, raw);
|
|
592
592
|
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
593
593
|
}
|
|
594
594
|
return [...merged.values()];
|
|
595
595
|
};
|
|
596
|
-
var listOperations = (
|
|
596
|
+
var listOperations = (document3) => {
|
|
597
597
|
const operations = [];
|
|
598
|
-
for (const [path, item] of Object.entries(
|
|
598
|
+
for (const [path, item] of Object.entries(document3.paths)) {
|
|
599
599
|
for (const method of HTTP_METHODS) {
|
|
600
600
|
const operation = item[method];
|
|
601
601
|
if (operation?.operationId === void 0)
|
|
602
602
|
continue;
|
|
603
603
|
const responses = {};
|
|
604
604
|
for (const [status, response] of Object.entries(operation.responses)) {
|
|
605
|
-
responses[status] = deref(
|
|
605
|
+
responses[status] = deref(document3, response);
|
|
606
606
|
}
|
|
607
607
|
operations.push({
|
|
608
608
|
operationId: operation.operationId,
|
|
609
609
|
method,
|
|
610
610
|
path,
|
|
611
611
|
operation,
|
|
612
|
-
parameters: mergeParameters(
|
|
613
|
-
requestBody: operation.requestBody === void 0 ? void 0 : deref(
|
|
612
|
+
parameters: mergeParameters(document3, item, operation.parameters),
|
|
613
|
+
requestBody: operation.requestBody === void 0 ? void 0 : deref(document3, operation.requestBody),
|
|
614
614
|
responses
|
|
615
615
|
});
|
|
616
616
|
}
|
|
@@ -971,6 +971,1232 @@ var clearNamespace = (sqlite, namespace) => {
|
|
|
971
971
|
});
|
|
972
972
|
};
|
|
973
973
|
|
|
974
|
+
// ../../ui/dist/reset.js
|
|
975
|
+
var CSS_RESET = `*,
|
|
976
|
+
*::before,
|
|
977
|
+
*::after {
|
|
978
|
+
box-sizing: border-box;
|
|
979
|
+
margin: 0;
|
|
980
|
+
padding: 0;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
* {
|
|
984
|
+
scrollbar-width: thin;
|
|
985
|
+
scrollbar-color: color-mix(in srgb, CanvasText 35%, Canvas) Canvas;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
*::-webkit-scrollbar {
|
|
989
|
+
width: 12px;
|
|
990
|
+
height: 12px;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
*::-webkit-scrollbar-track,
|
|
994
|
+
*::-webkit-scrollbar-corner {
|
|
995
|
+
background: Canvas;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
*::-webkit-scrollbar-thumb {
|
|
999
|
+
background: color-mix(in srgb, CanvasText 35%, Canvas);
|
|
1000
|
+
border: 3px solid Canvas;
|
|
1001
|
+
border-radius: 999px;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
html {
|
|
1005
|
+
hanging-punctuation: first allow-end last;
|
|
1006
|
+
-webkit-text-size-adjust: none;
|
|
1007
|
+
text-size-adjust: none;
|
|
1008
|
+
tab-size: 4;
|
|
1009
|
+
font-family:
|
|
1010
|
+
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI",
|
|
1011
|
+
"Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
|
1012
|
+
font-size: 16px;
|
|
1013
|
+
line-height: 1.5;
|
|
1014
|
+
-webkit-font-smoothing: antialiased;
|
|
1015
|
+
-moz-osx-font-smoothing: grayscale;
|
|
1016
|
+
text-rendering: optimizeLegibility;
|
|
1017
|
+
-webkit-tap-highlight-color: transparent;
|
|
1018
|
+
scroll-behavior: smooth;
|
|
1019
|
+
background: Canvas;
|
|
1020
|
+
color: CanvasText;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
body {
|
|
1024
|
+
min-height: 100svh;
|
|
1025
|
+
line-height: 1.5;
|
|
1026
|
+
background: Canvas;
|
|
1027
|
+
color: CanvasText;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
h1 {
|
|
1031
|
+
font-size: 2em;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
h2 {
|
|
1035
|
+
font-size: 1.5em;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
h3 {
|
|
1039
|
+
font-size: 1.17em;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
h4 {
|
|
1043
|
+
font-size: 1em;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
h5 {
|
|
1047
|
+
font-size: 0.83em;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
h6 {
|
|
1051
|
+
font-size: 0.67em;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
h1,
|
|
1055
|
+
h2,
|
|
1056
|
+
h3,
|
|
1057
|
+
h4,
|
|
1058
|
+
h5,
|
|
1059
|
+
h6 {
|
|
1060
|
+
font-weight: 700;
|
|
1061
|
+
line-height: 1.2;
|
|
1062
|
+
text-wrap: balance;
|
|
1063
|
+
overflow-wrap: break-word;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
p,
|
|
1067
|
+
li,
|
|
1068
|
+
figcaption,
|
|
1069
|
+
dd,
|
|
1070
|
+
dt,
|
|
1071
|
+
blockquote {
|
|
1072
|
+
overflow-wrap: break-word;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
p,
|
|
1076
|
+
li,
|
|
1077
|
+
figcaption,
|
|
1078
|
+
blockquote {
|
|
1079
|
+
text-wrap: pretty;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
ul,
|
|
1083
|
+
ol,
|
|
1084
|
+
menu {
|
|
1085
|
+
list-style: none;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
a {
|
|
1089
|
+
color: inherit;
|
|
1090
|
+
text-decoration: underline;
|
|
1091
|
+
text-decoration-thickness: 1px;
|
|
1092
|
+
text-underline-offset: 0.15em;
|
|
1093
|
+
text-decoration-skip-ink: auto;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
b,
|
|
1097
|
+
strong {
|
|
1098
|
+
font-weight: 700;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
small {
|
|
1102
|
+
font-size: 0.875em;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
code,
|
|
1106
|
+
kbd,
|
|
1107
|
+
samp,
|
|
1108
|
+
pre {
|
|
1109
|
+
font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
1110
|
+
font-size: 1em;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
sub,
|
|
1114
|
+
sup {
|
|
1115
|
+
font-size: 75%;
|
|
1116
|
+
line-height: 0;
|
|
1117
|
+
position: relative;
|
|
1118
|
+
vertical-align: baseline;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
sub {
|
|
1122
|
+
bottom: -0.25em;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
sup {
|
|
1126
|
+
top: -0.5em;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
abbr[title] {
|
|
1130
|
+
text-decoration: underline dotted;
|
|
1131
|
+
text-underline-offset: 0.15em;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
mark {
|
|
1135
|
+
background: Mark;
|
|
1136
|
+
color: MarkText;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
hr {
|
|
1140
|
+
height: 0;
|
|
1141
|
+
border: 0;
|
|
1142
|
+
border-top: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
|
|
1143
|
+
color: inherit;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
img,
|
|
1147
|
+
picture,
|
|
1148
|
+
video,
|
|
1149
|
+
canvas {
|
|
1150
|
+
display: block;
|
|
1151
|
+
max-width: 100%;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
img,
|
|
1155
|
+
video {
|
|
1156
|
+
height: auto;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
svg {
|
|
1160
|
+
overflow: hidden;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
iframe {
|
|
1164
|
+
border: 0;
|
|
1165
|
+
max-width: 100%;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
table {
|
|
1169
|
+
border-collapse: collapse;
|
|
1170
|
+
border-spacing: 0;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
fieldset {
|
|
1174
|
+
border: 0;
|
|
1175
|
+
min-inline-size: 0;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
legend {
|
|
1179
|
+
padding: 0;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
button,
|
|
1183
|
+
input,
|
|
1184
|
+
select,
|
|
1185
|
+
textarea,
|
|
1186
|
+
optgroup {
|
|
1187
|
+
font: inherit;
|
|
1188
|
+
letter-spacing: inherit;
|
|
1189
|
+
color: inherit;
|
|
1190
|
+
margin: 0;
|
|
1191
|
+
vertical-align: middle;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
button,
|
|
1195
|
+
input[type="button"],
|
|
1196
|
+
input[type="submit"],
|
|
1197
|
+
input[type="reset"] {
|
|
1198
|
+
appearance: none;
|
|
1199
|
+
background: none;
|
|
1200
|
+
border: 0;
|
|
1201
|
+
padding: 0;
|
|
1202
|
+
cursor: pointer;
|
|
1203
|
+
text-align: center;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
input,
|
|
1207
|
+
textarea,
|
|
1208
|
+
select {
|
|
1209
|
+
background-color: Canvas;
|
|
1210
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1211
|
+
border-radius: 0;
|
|
1212
|
+
padding: 0.5em 0.65em;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
input[type="checkbox"],
|
|
1216
|
+
input[type="radio"],
|
|
1217
|
+
input[type="range"],
|
|
1218
|
+
input[type="file"],
|
|
1219
|
+
input[type="color"],
|
|
1220
|
+
input[type="button"],
|
|
1221
|
+
input[type="submit"],
|
|
1222
|
+
input[type="reset"],
|
|
1223
|
+
input[type="image"],
|
|
1224
|
+
input[type="hidden"] {
|
|
1225
|
+
background: none;
|
|
1226
|
+
border: 0;
|
|
1227
|
+
padding: 0;
|
|
1228
|
+
border-radius: 0;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
input[type="checkbox"],
|
|
1232
|
+
input[type="radio"] {
|
|
1233
|
+
accent-color: CanvasText;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
input[type="search"] {
|
|
1237
|
+
appearance: none;
|
|
1238
|
+
background-color: Canvas;
|
|
1239
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
input[type="search"]::-webkit-search-decoration,
|
|
1243
|
+
input[type="search"]::-webkit-search-cancel-button,
|
|
1244
|
+
input[type="search"]::-webkit-search-results-button,
|
|
1245
|
+
input[type="search"]::-webkit-search-results-decoration {
|
|
1246
|
+
-webkit-appearance: none;
|
|
1247
|
+
display: none;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
input[type="number"] {
|
|
1251
|
+
appearance: textfield;
|
|
1252
|
+
background-color: Canvas;
|
|
1253
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
input[type="number"]::-webkit-inner-spin-button,
|
|
1257
|
+
input[type="number"]::-webkit-outer-spin-button {
|
|
1258
|
+
-webkit-appearance: none;
|
|
1259
|
+
margin: 0;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
textarea {
|
|
1263
|
+
resize: vertical;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
::placeholder {
|
|
1267
|
+
opacity: 1;
|
|
1268
|
+
color: color-mix(in srgb, CanvasText 45%, transparent);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
:focus-visible {
|
|
1272
|
+
outline: 2px solid CanvasText;
|
|
1273
|
+
outline-offset: 2px;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
:focus:not(:focus-visible) {
|
|
1277
|
+
outline: none;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
:disabled,
|
|
1281
|
+
[disabled] {
|
|
1282
|
+
cursor: default;
|
|
1283
|
+
opacity: 0.6;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
label,
|
|
1287
|
+
summary,
|
|
1288
|
+
select,
|
|
1289
|
+
[role="button"] {
|
|
1290
|
+
cursor: pointer;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
summary {
|
|
1294
|
+
display: list-item;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
details {
|
|
1298
|
+
display: block;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
dialog {
|
|
1302
|
+
margin: auto;
|
|
1303
|
+
padding: 1rem;
|
|
1304
|
+
border: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
|
|
1305
|
+
background: Canvas;
|
|
1306
|
+
color: CanvasText;
|
|
1307
|
+
max-width: calc(100vw - 2rem);
|
|
1308
|
+
max-height: calc(100svh - 2rem);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
dialog:not([open]) {
|
|
1312
|
+
display: none;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
::backdrop {
|
|
1316
|
+
background: color-mix(in srgb, CanvasText 40%, transparent);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
[hidden] {
|
|
1320
|
+
display: none !important;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
::selection {
|
|
1324
|
+
background: Highlight;
|
|
1325
|
+
color: HighlightText;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1329
|
+
*,
|
|
1330
|
+
*::before,
|
|
1331
|
+
*::after {
|
|
1332
|
+
animation-duration: 0.01ms !important;
|
|
1333
|
+
animation-iteration-count: 1 !important;
|
|
1334
|
+
transition-duration: 0.01ms !important;
|
|
1335
|
+
scroll-behavior: auto !important;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
@media (pointer: coarse) {
|
|
1340
|
+
input,
|
|
1341
|
+
select,
|
|
1342
|
+
textarea {
|
|
1343
|
+
font-size: max(16px, 1em);
|
|
1344
|
+
}
|
|
1345
|
+
}`;
|
|
1346
|
+
|
|
1347
|
+
// ../core/dist/admin-client.js
|
|
1348
|
+
function isRecord2(value) {
|
|
1349
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1350
|
+
}
|
|
1351
|
+
function mountAdminBrand(doc, host, service, catalog) {
|
|
1352
|
+
const clip = (value, max) => {
|
|
1353
|
+
if (typeof value !== "string")
|
|
1354
|
+
return null;
|
|
1355
|
+
const trimmed = value.trim();
|
|
1356
|
+
if (trimmed === "")
|
|
1357
|
+
return null;
|
|
1358
|
+
return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
|
|
1359
|
+
};
|
|
1360
|
+
const httpUrl = (value) => {
|
|
1361
|
+
if (typeof value !== "string" || value.length > 2e3)
|
|
1362
|
+
return null;
|
|
1363
|
+
try {
|
|
1364
|
+
const url = new URL(value);
|
|
1365
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
1366
|
+
return null;
|
|
1367
|
+
return url.href;
|
|
1368
|
+
} catch {
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
const hexColor = (value) => {
|
|
1373
|
+
if (typeof value !== "string")
|
|
1374
|
+
return null;
|
|
1375
|
+
const raw = value.trim();
|
|
1376
|
+
return /^#[0-9a-fA-F]{3,8}$/.test(raw) ? raw : null;
|
|
1377
|
+
};
|
|
1378
|
+
const hostname = (value) => {
|
|
1379
|
+
try {
|
|
1380
|
+
return new URL(value).hostname.replace(/^www\./, "");
|
|
1381
|
+
} catch {
|
|
1382
|
+
return value;
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
host.replaceChildren();
|
|
1386
|
+
host.hidden = true;
|
|
1387
|
+
if (!isRecord2(catalog) || !Object.hasOwn(catalog, service))
|
|
1388
|
+
return;
|
|
1389
|
+
const row = catalog[service];
|
|
1390
|
+
if (!isRecord2(row))
|
|
1391
|
+
return;
|
|
1392
|
+
const vendor = clip(row.vendor, 80);
|
|
1393
|
+
const website = httpUrl(row.website);
|
|
1394
|
+
const guide = httpUrl(row.guide);
|
|
1395
|
+
const docs = httpUrl(row.docs);
|
|
1396
|
+
const logo = httpUrl(row.logo);
|
|
1397
|
+
const color = hexColor(row.color);
|
|
1398
|
+
const description = clip(row.description, 280);
|
|
1399
|
+
if (vendor === null || website === null && guide === null && docs === null)
|
|
1400
|
+
return;
|
|
1401
|
+
const add = (node) => {
|
|
1402
|
+
host.append(node);
|
|
1403
|
+
return node;
|
|
1404
|
+
};
|
|
1405
|
+
if (logo !== null) {
|
|
1406
|
+
const img = doc.createElement("img");
|
|
1407
|
+
img.alt = "";
|
|
1408
|
+
img.src = logo;
|
|
1409
|
+
img.width = 16;
|
|
1410
|
+
img.height = 16;
|
|
1411
|
+
img.decoding = "async";
|
|
1412
|
+
img.referrerPolicy = "no-referrer";
|
|
1413
|
+
if (color !== null)
|
|
1414
|
+
img.style.borderColor = color;
|
|
1415
|
+
add(img);
|
|
1416
|
+
}
|
|
1417
|
+
const name = doc.createElement("span");
|
|
1418
|
+
name.className = "vendor-name";
|
|
1419
|
+
name.textContent = vendor;
|
|
1420
|
+
if (description !== null)
|
|
1421
|
+
name.title = description;
|
|
1422
|
+
add(name);
|
|
1423
|
+
const sep = () => {
|
|
1424
|
+
const node = doc.createElement("span");
|
|
1425
|
+
node.className = "vendor-sep";
|
|
1426
|
+
node.setAttribute("aria-hidden", "true");
|
|
1427
|
+
node.textContent = "\xB7";
|
|
1428
|
+
return node;
|
|
1429
|
+
};
|
|
1430
|
+
const link = (href, label, title) => {
|
|
1431
|
+
const node = doc.createElement("a");
|
|
1432
|
+
node.className = "vendor-link";
|
|
1433
|
+
node.href = href;
|
|
1434
|
+
node.target = "_blank";
|
|
1435
|
+
node.rel = "noopener noreferrer";
|
|
1436
|
+
node.referrerPolicy = "no-referrer";
|
|
1437
|
+
node.textContent = label;
|
|
1438
|
+
node.title = title;
|
|
1439
|
+
return node;
|
|
1440
|
+
};
|
|
1441
|
+
if (website !== null) {
|
|
1442
|
+
add(sep());
|
|
1443
|
+
add(link(website, hostname(website), `${vendor} website`));
|
|
1444
|
+
}
|
|
1445
|
+
if (guide !== null) {
|
|
1446
|
+
add(sep());
|
|
1447
|
+
add(link(guide, "Docs", `Docs for ${vendor}`));
|
|
1448
|
+
}
|
|
1449
|
+
if (docs !== null) {
|
|
1450
|
+
add(sep());
|
|
1451
|
+
add(link(docs, "API", `${vendor} API reference`));
|
|
1452
|
+
}
|
|
1453
|
+
host.removeAttribute("hidden");
|
|
1454
|
+
host.hidden = false;
|
|
1455
|
+
}
|
|
1456
|
+
function startAdminBrand(env) {
|
|
1457
|
+
const requested = env.params.get("brands");
|
|
1458
|
+
let brandsUrl = env.brandsUrl;
|
|
1459
|
+
if (requested) {
|
|
1460
|
+
try {
|
|
1461
|
+
const url = new URL(requested, env.location.href);
|
|
1462
|
+
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]" || url.origin === env.location.origin;
|
|
1463
|
+
if ((url.protocol === "https:" || url.protocol === "http:") && local)
|
|
1464
|
+
brandsUrl = url.href;
|
|
1465
|
+
} catch {
|
|
1466
|
+
brandsUrl = env.brandsUrl;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
const vendorHost = env.$("vendor");
|
|
1470
|
+
const brandRoot = env.document.querySelector("[data-mockingbird-admin]");
|
|
1471
|
+
const brandService = brandRoot?.getAttribute("data-service") || "";
|
|
1472
|
+
if (vendorHost === null)
|
|
1473
|
+
return;
|
|
1474
|
+
env.fetch(brandsUrl, {
|
|
1475
|
+
headers: { accept: "application/json" },
|
|
1476
|
+
signal: AbortSignal.timeout(5e3)
|
|
1477
|
+
}).then(async (response) => {
|
|
1478
|
+
if (!response.ok)
|
|
1479
|
+
return null;
|
|
1480
|
+
const catalog = await response.json();
|
|
1481
|
+
return catalog;
|
|
1482
|
+
}).then((catalog) => {
|
|
1483
|
+
mountAdminBrand(env.document, vendorHost, brandService, catalog);
|
|
1484
|
+
}).catch(() => {
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
function faultPresetList(value) {
|
|
1488
|
+
if (!isRecord2(value))
|
|
1489
|
+
throw new Error("Fault presets response was not an object");
|
|
1490
|
+
if (!Object.hasOwn(value, "presets") || value.presets === void 0 || value.presets === null) {
|
|
1491
|
+
return { presets: [] };
|
|
1492
|
+
}
|
|
1493
|
+
if (!Array.isArray(value.presets))
|
|
1494
|
+
throw new Error("Fault presets response was not a list");
|
|
1495
|
+
return {
|
|
1496
|
+
presets: value.presets.flatMap((item) => {
|
|
1497
|
+
if (!isRecord2(item) || typeof item.name !== "string")
|
|
1498
|
+
return [];
|
|
1499
|
+
return [{ name: item.name }];
|
|
1500
|
+
})
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
function bootAdmin(config) {
|
|
1504
|
+
const fieldKinds = ["string", "number", "boolean", "null", "object", "array", "unknown"];
|
|
1505
|
+
const sources = ["declared", "inferred", "mixed"];
|
|
1506
|
+
const isFieldKind = (value) => typeof value === "string" && fieldKinds.some((kind) => kind === value);
|
|
1507
|
+
const isSource = (value) => typeof value === "string" && sources.some((source) => source === value);
|
|
1508
|
+
const readJson2 = (text) => {
|
|
1509
|
+
const value = JSON.parse(text);
|
|
1510
|
+
return value;
|
|
1511
|
+
};
|
|
1512
|
+
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => {
|
|
1513
|
+
switch (char) {
|
|
1514
|
+
case "&":
|
|
1515
|
+
return "&";
|
|
1516
|
+
case "<":
|
|
1517
|
+
return "<";
|
|
1518
|
+
case ">":
|
|
1519
|
+
return ">";
|
|
1520
|
+
case '"':
|
|
1521
|
+
return """;
|
|
1522
|
+
default:
|
|
1523
|
+
return "'";
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
const el = (id, kind) => {
|
|
1527
|
+
const node = document.getElementById(id);
|
|
1528
|
+
if (!(node instanceof kind))
|
|
1529
|
+
throw new Error(`admin shell is missing #${id}`);
|
|
1530
|
+
return node;
|
|
1531
|
+
};
|
|
1532
|
+
const errorMessage = (data, fallback) => {
|
|
1533
|
+
if (!isRecord2(data))
|
|
1534
|
+
return fallback;
|
|
1535
|
+
const error = data.error;
|
|
1536
|
+
if (!isRecord2(error) || typeof error.message !== "string" || error.message === "") {
|
|
1537
|
+
return fallback;
|
|
1538
|
+
}
|
|
1539
|
+
return error.message;
|
|
1540
|
+
};
|
|
1541
|
+
const requireList = (value, label) => {
|
|
1542
|
+
if (value === void 0 || value === null)
|
|
1543
|
+
return [];
|
|
1544
|
+
if (!Array.isArray(value))
|
|
1545
|
+
throw new Error(`${label} was not a list`);
|
|
1546
|
+
return value;
|
|
1547
|
+
};
|
|
1548
|
+
const state = {
|
|
1549
|
+
namespace: "default",
|
|
1550
|
+
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1551
|
+
view: "overview",
|
|
1552
|
+
collection: "",
|
|
1553
|
+
shape: null,
|
|
1554
|
+
route: null,
|
|
1555
|
+
editing: null
|
|
1556
|
+
};
|
|
1557
|
+
const standard = new Set(config.standardRoutes);
|
|
1558
|
+
const banner = el("banner", HTMLElement);
|
|
1559
|
+
const keyInput = el("admin-key", HTMLInputElement);
|
|
1560
|
+
keyInput.value = state.key;
|
|
1561
|
+
const params = new URLSearchParams(location.search);
|
|
1562
|
+
const queryKey = params.get("key");
|
|
1563
|
+
if (queryKey) {
|
|
1564
|
+
state.key = queryKey;
|
|
1565
|
+
keyInput.value = state.key;
|
|
1566
|
+
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1567
|
+
params.delete("key");
|
|
1568
|
+
const next = params.toString();
|
|
1569
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
1570
|
+
}
|
|
1571
|
+
const queryNamespace = params.get("namespace");
|
|
1572
|
+
if (queryNamespace)
|
|
1573
|
+
state.namespace = queryNamespace;
|
|
1574
|
+
const showError = (error) => {
|
|
1575
|
+
banner.textContent = error instanceof Error ? error.message : String(error);
|
|
1576
|
+
banner.classList.add("show");
|
|
1577
|
+
};
|
|
1578
|
+
const clearError = () => {
|
|
1579
|
+
banner.classList.remove("show");
|
|
1580
|
+
};
|
|
1581
|
+
const api = async (method, path, body) => {
|
|
1582
|
+
const headers = { accept: "application/json" };
|
|
1583
|
+
if (state.key !== "")
|
|
1584
|
+
headers[config.adminKeyHeader] = state.key;
|
|
1585
|
+
if (state.namespace !== "")
|
|
1586
|
+
headers["x-mockingbird-namespace"] = state.namespace;
|
|
1587
|
+
if (body !== void 0)
|
|
1588
|
+
headers["content-type"] = "application/json";
|
|
1589
|
+
const init = { method, headers };
|
|
1590
|
+
if (body !== void 0)
|
|
1591
|
+
init.body = JSON.stringify(body);
|
|
1592
|
+
const response = await fetch(config.adminPrefix + path, init);
|
|
1593
|
+
const text = await response.text();
|
|
1594
|
+
let data = null;
|
|
1595
|
+
if (text !== "") {
|
|
1596
|
+
try {
|
|
1597
|
+
data = readJson2(text);
|
|
1598
|
+
} catch {
|
|
1599
|
+
data = { raw: text };
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
if (response.status === 401) {
|
|
1603
|
+
keyInput.focus();
|
|
1604
|
+
throw new Error(errorMessage(data, "Admin key required"));
|
|
1605
|
+
}
|
|
1606
|
+
if (!response.ok) {
|
|
1607
|
+
throw new Error(errorMessage(data, `${response.status} ${method} ${path}`));
|
|
1608
|
+
}
|
|
1609
|
+
return data;
|
|
1610
|
+
};
|
|
1611
|
+
const originGet = async (path) => {
|
|
1612
|
+
const response = await fetch(path, { headers: { accept: "application/json" } });
|
|
1613
|
+
if (!response.ok)
|
|
1614
|
+
throw new Error(`${response.status} ${path}`);
|
|
1615
|
+
const value = await response.json();
|
|
1616
|
+
return value;
|
|
1617
|
+
};
|
|
1618
|
+
const panelApi = {
|
|
1619
|
+
get: (path) => api("GET", path),
|
|
1620
|
+
send: (method, path, body) => api(method, path, body),
|
|
1621
|
+
namespace: () => state.namespace
|
|
1622
|
+
};
|
|
1623
|
+
const isPanelScript = (value) => typeof value === "function";
|
|
1624
|
+
const setTheme = (theme) => {
|
|
1625
|
+
if (theme)
|
|
1626
|
+
document.documentElement.dataset.theme = theme;
|
|
1627
|
+
else
|
|
1628
|
+
delete document.documentElement.dataset.theme;
|
|
1629
|
+
};
|
|
1630
|
+
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1631
|
+
if (storedTheme)
|
|
1632
|
+
setTheme(storedTheme);
|
|
1633
|
+
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
1634
|
+
const current = document.documentElement.dataset.theme;
|
|
1635
|
+
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1636
|
+
const next = dark ? "light" : "dark";
|
|
1637
|
+
localStorage.setItem("mockingbird-admin-theme", next);
|
|
1638
|
+
setTheme(next);
|
|
1639
|
+
});
|
|
1640
|
+
const show = (view) => {
|
|
1641
|
+
state.view = view;
|
|
1642
|
+
for (const button of document.querySelectorAll("#nav button")) {
|
|
1643
|
+
if (!(button instanceof HTMLButtonElement))
|
|
1644
|
+
continue;
|
|
1645
|
+
button.setAttribute("aria-current", button.dataset.view === view ? "true" : "false");
|
|
1646
|
+
}
|
|
1647
|
+
for (const section of document.querySelectorAll("main .view")) {
|
|
1648
|
+
if (!(section instanceof HTMLElement))
|
|
1649
|
+
continue;
|
|
1650
|
+
section.hidden = section.id !== `view-${view}`;
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
1654
|
+
const target = event.target;
|
|
1655
|
+
if (!(target instanceof Element))
|
|
1656
|
+
return;
|
|
1657
|
+
const button = target.closest("button[data-view]");
|
|
1658
|
+
if (!(button instanceof HTMLButtonElement))
|
|
1659
|
+
return;
|
|
1660
|
+
const view = button.dataset.view;
|
|
1661
|
+
if (view === void 0)
|
|
1662
|
+
return;
|
|
1663
|
+
show(view);
|
|
1664
|
+
refresh().catch(showError);
|
|
1665
|
+
});
|
|
1666
|
+
const cards = (items) => `<div class="grid">${items.map((item) => `<article class="card"><div class="k">${item.k}</div><div class="v">${item.v}</div></article>`).join("")}</div>`;
|
|
1667
|
+
const table = (columns, rows) => {
|
|
1668
|
+
if (rows.length === 0)
|
|
1669
|
+
return '<p class="empty">Nothing here yet.</p>';
|
|
1670
|
+
const head = `<tr>${columns.map((column) => `<th>${column.label}</th>`).join("")}</tr>`;
|
|
1671
|
+
const body = rows.map((row) => {
|
|
1672
|
+
const cells = columns.map((column) => `<td data-label="${column.label}">${column.cell(row)}</td>`).join("");
|
|
1673
|
+
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
1674
|
+
return `<tr${attrs}>${cells}</tr>`;
|
|
1675
|
+
}).join("");
|
|
1676
|
+
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
1677
|
+
};
|
|
1678
|
+
const preview = (value) => {
|
|
1679
|
+
const text = JSON.stringify(value) ?? "null";
|
|
1680
|
+
return esc(text.length > 180 ? `${text.slice(0, 180)}\u2026` : text);
|
|
1681
|
+
};
|
|
1682
|
+
const when = (epoch) => {
|
|
1683
|
+
const date = new Date(epoch);
|
|
1684
|
+
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
1685
|
+
};
|
|
1686
|
+
const clockCard = (epoch) => {
|
|
1687
|
+
const date = new Date(epoch);
|
|
1688
|
+
if (Number.isNaN(date.getTime()))
|
|
1689
|
+
return esc(String(epoch));
|
|
1690
|
+
const iso = date.toISOString();
|
|
1691
|
+
return `${esc(iso.slice(0, 10))}<span class="clock-time">${esc(iso.slice(11, 19))} UTC</span>`;
|
|
1692
|
+
};
|
|
1693
|
+
const decodeClock = (value) => {
|
|
1694
|
+
if (!isRecord2(value) || typeof value.now !== "number" || typeof value.offsetMs !== "number" || typeof value.frozen !== "boolean") {
|
|
1695
|
+
throw new Error("Clock response was not a clock");
|
|
1696
|
+
}
|
|
1697
|
+
return { now: value.now, offsetMs: value.offsetMs, frozen: value.frozen };
|
|
1698
|
+
};
|
|
1699
|
+
const decodeField = (value) => {
|
|
1700
|
+
if (!isRecord2(value) || typeof value.name !== "string" || !isFieldKind(value.kind)) {
|
|
1701
|
+
return void 0;
|
|
1702
|
+
}
|
|
1703
|
+
const field = {
|
|
1704
|
+
name: value.name,
|
|
1705
|
+
kind: value.kind,
|
|
1706
|
+
optional: value.optional === true
|
|
1707
|
+
};
|
|
1708
|
+
if (typeof value.description === "string")
|
|
1709
|
+
field.description = value.description;
|
|
1710
|
+
if (Array.isArray(value.fields)) {
|
|
1711
|
+
field.fields = value.fields.flatMap((item) => {
|
|
1712
|
+
const child = decodeField(item);
|
|
1713
|
+
return child === void 0 ? [] : [child];
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
return field;
|
|
1717
|
+
};
|
|
1718
|
+
const decodeCollection = (value) => {
|
|
1719
|
+
if (!isRecord2(value) || typeof value.name !== "string")
|
|
1720
|
+
return void 0;
|
|
1721
|
+
const fields = Array.isArray(value.fields) ? value.fields.flatMap((item) => {
|
|
1722
|
+
const field = decodeField(item);
|
|
1723
|
+
return field === void 0 ? [] : [field];
|
|
1724
|
+
}) : [];
|
|
1725
|
+
const collection = {
|
|
1726
|
+
name: value.name,
|
|
1727
|
+
label: typeof value.label === "string" ? value.label : value.name,
|
|
1728
|
+
count: typeof value.count === "number" ? value.count : 0,
|
|
1729
|
+
source: isSource(value.source) ? value.source : "inferred",
|
|
1730
|
+
fields
|
|
1731
|
+
};
|
|
1732
|
+
if (typeof value.description === "string")
|
|
1733
|
+
collection.description = value.description;
|
|
1734
|
+
return collection;
|
|
1735
|
+
};
|
|
1736
|
+
const decodeState = (value) => {
|
|
1737
|
+
if (!isRecord2(value) || !Array.isArray(value.collections)) {
|
|
1738
|
+
throw new Error("State response has no collection list");
|
|
1739
|
+
}
|
|
1740
|
+
return {
|
|
1741
|
+
namespace: typeof value.namespace === "string" ? value.namespace : state.namespace,
|
|
1742
|
+
storageNamespace: typeof value.storageNamespace === "string" ? value.storageNamespace : "",
|
|
1743
|
+
collections: value.collections.flatMap((item) => {
|
|
1744
|
+
const collection = decodeCollection(item);
|
|
1745
|
+
return collection === void 0 ? [] : [collection];
|
|
1746
|
+
})
|
|
1747
|
+
};
|
|
1748
|
+
};
|
|
1749
|
+
const decodeRecords = (value) => requireList(isRecord2(value) ? value.records : void 0, "Records").flatMap((item) => {
|
|
1750
|
+
if (!isRecord2(item) || typeof item.id !== "string")
|
|
1751
|
+
return [];
|
|
1752
|
+
return [
|
|
1753
|
+
{
|
|
1754
|
+
id: item.id,
|
|
1755
|
+
seq: typeof item.seq === "number" ? item.seq : 0,
|
|
1756
|
+
value: item.value,
|
|
1757
|
+
attrs: ` class="clickable" data-id="${esc(item.id)}"`
|
|
1758
|
+
}
|
|
1759
|
+
];
|
|
1760
|
+
});
|
|
1761
|
+
const textOf = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : "";
|
|
1762
|
+
const decodeJournal = (value) => requireList(isRecord2(value) ? value.requests : void 0, "Journal").flatMap((item) => {
|
|
1763
|
+
if (!isRecord2(item))
|
|
1764
|
+
return [];
|
|
1765
|
+
return [
|
|
1766
|
+
{
|
|
1767
|
+
at: textOf(item.at),
|
|
1768
|
+
method: textOf(item.method),
|
|
1769
|
+
path: textOf(item.path),
|
|
1770
|
+
status: textOf(item.status),
|
|
1771
|
+
durationMs: textOf(item.durationMs),
|
|
1772
|
+
operationId: textOf(item.operationId)
|
|
1773
|
+
}
|
|
1774
|
+
];
|
|
1775
|
+
});
|
|
1776
|
+
const decodeFaults = (value) => requireList(isRecord2(value) ? value.faults : void 0, "Faults").flatMap((item) => {
|
|
1777
|
+
if (!isRecord2(item))
|
|
1778
|
+
return [];
|
|
1779
|
+
return [
|
|
1780
|
+
{
|
|
1781
|
+
id: textOf(item.id),
|
|
1782
|
+
method: typeof item.method === "string" ? item.method : void 0,
|
|
1783
|
+
pathPrefix: typeof item.pathPrefix === "string" ? item.pathPrefix : void 0,
|
|
1784
|
+
operationId: typeof item.operationId === "string" ? item.operationId : void 0,
|
|
1785
|
+
status: typeof item.status === "number" ? item.status : void 0,
|
|
1786
|
+
effect: typeof item.effect === "string" ? item.effect : void 0,
|
|
1787
|
+
drop: typeof item.drop === "boolean" ? item.drop : void 0,
|
|
1788
|
+
latencyMs: typeof item.latencyMs === "number" ? item.latencyMs : void 0
|
|
1789
|
+
}
|
|
1790
|
+
];
|
|
1791
|
+
});
|
|
1792
|
+
const loadNamespaces = async () => {
|
|
1793
|
+
const data = await api("GET", "/namespaces");
|
|
1794
|
+
const select = el("namespace", HTMLSelectElement);
|
|
1795
|
+
const listed = requireList(isRecord2(data) ? data.namespaces : void 0, "Namespaces");
|
|
1796
|
+
const fallback = isRecord2(data) && typeof data.default === "string" && data.default !== "" ? data.default : "default";
|
|
1797
|
+
const names = (listed.length > 0 ? listed.map((name) => String(name)) : [fallback]).slice();
|
|
1798
|
+
if (!names.includes(state.namespace))
|
|
1799
|
+
names.unshift(state.namespace);
|
|
1800
|
+
select.innerHTML = names.map((name) => `<option value="${esc(name)}">${esc(name)}</option>`).join("");
|
|
1801
|
+
select.value = state.namespace;
|
|
1802
|
+
};
|
|
1803
|
+
const renderOverview = async () => {
|
|
1804
|
+
const [health, shape, clock, journal] = await Promise.all([
|
|
1805
|
+
originGet("/health"),
|
|
1806
|
+
api("GET", "/state"),
|
|
1807
|
+
api("GET", "/clock"),
|
|
1808
|
+
api("GET", "/requests?limit=8")
|
|
1809
|
+
]);
|
|
1810
|
+
if (!isRecord2(health))
|
|
1811
|
+
throw new Error("Health response was not an object");
|
|
1812
|
+
const decoded = decodeState(shape);
|
|
1813
|
+
state.shape = decoded;
|
|
1814
|
+
const time = decodeClock(clock);
|
|
1815
|
+
el("overview-cards", HTMLElement).innerHTML = cards([
|
|
1816
|
+
{
|
|
1817
|
+
k: "Service",
|
|
1818
|
+
v: esc(typeof health.service === "string" && health.service !== "" ? health.service : config.service)
|
|
1819
|
+
},
|
|
1820
|
+
{
|
|
1821
|
+
k: "Status",
|
|
1822
|
+
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
1823
|
+
},
|
|
1824
|
+
{ k: "Namespace", v: esc(state.namespace) },
|
|
1825
|
+
{ k: "Collections", v: String(decoded.collections.length) },
|
|
1826
|
+
{
|
|
1827
|
+
k: "Records",
|
|
1828
|
+
v: String(decoded.collections.reduce((sum, item) => sum + item.count, 0))
|
|
1829
|
+
},
|
|
1830
|
+
{ k: "Clock", v: clockCard(time.now) }
|
|
1831
|
+
]);
|
|
1832
|
+
el("overview-requests", HTMLElement).innerHTML = table([
|
|
1833
|
+
{ label: "Method", cell: (row) => esc(row.method) },
|
|
1834
|
+
{ label: "Path", cell: (row) => esc(row.path) },
|
|
1835
|
+
{
|
|
1836
|
+
label: "Status",
|
|
1837
|
+
cell: (row) => `<span class="status-${row.status.charAt(0)}">${esc(row.status)}</span>`
|
|
1838
|
+
},
|
|
1839
|
+
{ label: "Operation", cell: (row) => esc(row.operationId) }
|
|
1840
|
+
], decodeJournal(journal));
|
|
1841
|
+
};
|
|
1842
|
+
const schemaChips = (collection) => {
|
|
1843
|
+
if (collection.fields.length === 0) {
|
|
1844
|
+
return '<p class="empty">No fields yet. Add a record or declare the collection on the service.</p>';
|
|
1845
|
+
}
|
|
1846
|
+
return `<div class="chips">${collection.fields.map((field) => `<span class="chip">${esc(field.name)} <i>${esc(field.kind)}${field.optional ? "?" : ""}</i></span>`).join("")}</div>`;
|
|
1847
|
+
};
|
|
1848
|
+
const renderState = async () => {
|
|
1849
|
+
const shape = decodeState(await api("GET", "/state"));
|
|
1850
|
+
state.shape = shape;
|
|
1851
|
+
const host = el("collections", HTMLElement);
|
|
1852
|
+
if (shape.collections.length === 0) {
|
|
1853
|
+
host.innerHTML = '<p class="empty">This namespace has no collections yet.</p>';
|
|
1854
|
+
el("schema", HTMLElement).innerHTML = "";
|
|
1855
|
+
el("records", HTMLElement).innerHTML = "";
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
if (!shape.collections.some((item) => item.name === state.collection)) {
|
|
1859
|
+
const first = shape.collections[0];
|
|
1860
|
+
if (first === void 0)
|
|
1861
|
+
return;
|
|
1862
|
+
state.collection = first.name;
|
|
1863
|
+
}
|
|
1864
|
+
host.innerHTML = shape.collections.map((item) => `<button type="button" class="collection" data-collection="${esc(item.name)}" aria-current="${item.name === state.collection ? "true" : "false"}"><strong>${esc(item.label)}</strong><small>${esc(item.count)} \xB7 ${esc(item.source)}</small></button>`).join("");
|
|
1865
|
+
const selected = shape.collections.find((item) => item.name === state.collection);
|
|
1866
|
+
if (selected === void 0)
|
|
1867
|
+
return;
|
|
1868
|
+
const description = selected.description ? `<p class="lede">${esc(selected.description)}</p>` : "";
|
|
1869
|
+
el("schema", HTMLElement).innerHTML = `<h3>${esc(selected.label)}</h3>${description}${schemaChips(selected)}`;
|
|
1870
|
+
const page = await api("GET", `/state/${encodeURIComponent(state.collection)}?limit=50`);
|
|
1871
|
+
el("records", HTMLElement).innerHTML = table([
|
|
1872
|
+
{ label: "Id", cell: (row) => `<code>${esc(row.id)}</code>` },
|
|
1873
|
+
{ label: "Seq", cell: (row) => esc(row.seq) },
|
|
1874
|
+
{ label: "Value", cell: (row) => preview(row.value) }
|
|
1875
|
+
], decodeRecords(page));
|
|
1876
|
+
};
|
|
1877
|
+
const renderClock = async () => {
|
|
1878
|
+
const clock = decodeClock(await api("GET", "/clock"));
|
|
1879
|
+
el("clock-readout", HTMLElement).innerHTML = `<div class="k">Now</div><div class="v">${esc(when(clock.now))}</div><p>${esc(clock.now)} ms \xB7 offset ${esc(clock.offsetMs)} ms ${clock.frozen ? "<span class='badge warn'>frozen</span>" : "<span class='badge ok'>live</span>"}</p>`;
|
|
1880
|
+
const freeze = el("freeze", HTMLButtonElement);
|
|
1881
|
+
freeze.textContent = clock.frozen ? "Unfreeze" : "Freeze";
|
|
1882
|
+
freeze.dataset.frozen = clock.frozen ? "1" : "0";
|
|
1883
|
+
};
|
|
1884
|
+
const renderFaults = async () => {
|
|
1885
|
+
const [faults, presets] = await Promise.all([
|
|
1886
|
+
api("GET", "/faults"),
|
|
1887
|
+
api("GET", "/faults/presets").catch(() => ({ presets: [] }))
|
|
1888
|
+
]);
|
|
1889
|
+
const select = el("fault-preset", HTMLSelectElement);
|
|
1890
|
+
const current = select.value;
|
|
1891
|
+
const list = faultPresetList(presets).presets;
|
|
1892
|
+
select.innerHTML = `<option value="">Custom rule</option>${list.map((preset) => `<option value="${esc(preset.name)}">${esc(preset.name)}</option>`).join("")}`;
|
|
1893
|
+
select.value = current;
|
|
1894
|
+
el("fault-list", HTMLElement).innerHTML = table([
|
|
1895
|
+
{ label: "Id", cell: (row) => esc(row.id) },
|
|
1896
|
+
{
|
|
1897
|
+
label: "Match",
|
|
1898
|
+
cell: (row) => esc([row.method, row.pathPrefix, row.operationId].filter((part) => Boolean(part)).join(" ") || "all")
|
|
1899
|
+
},
|
|
1900
|
+
{
|
|
1901
|
+
label: "Effect",
|
|
1902
|
+
cell: (row) => esc(row.status ?? row.effect ?? (row.drop ? "drop" : row.latencyMs || ""))
|
|
1903
|
+
}
|
|
1904
|
+
], decodeFaults(faults));
|
|
1905
|
+
};
|
|
1906
|
+
const renderJournal = async () => {
|
|
1907
|
+
const journal = await api("GET", "/requests?limit=100");
|
|
1908
|
+
el("journal", HTMLElement).innerHTML = table([
|
|
1909
|
+
{ label: "When", cell: (row) => esc(row.at) },
|
|
1910
|
+
{ label: "Method", cell: (row) => esc(row.method) },
|
|
1911
|
+
{ label: "Path", cell: (row) => esc(row.path) },
|
|
1912
|
+
{ label: "Status", cell: (row) => esc(row.status) },
|
|
1913
|
+
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
1914
|
+
], decodeJournal(journal));
|
|
1915
|
+
};
|
|
1916
|
+
const renderRoutes = async () => {
|
|
1917
|
+
const data = await api("GET", "/");
|
|
1918
|
+
if (!isRecord2(data))
|
|
1919
|
+
throw new Error("Route list was not an object");
|
|
1920
|
+
const routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
1921
|
+
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
1922
|
+
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
1923
|
+
const parts = route.split(" ");
|
|
1924
|
+
const method = parts[0] ?? "";
|
|
1925
|
+
const path = parts.slice(1).join(" ");
|
|
1926
|
+
return `<div class="route"><span class="method">${esc(method)}</span><span>${esc(path)}${extra}</span><button class="btn" type="button" data-route="${esc(route)}">Use</button></div>`;
|
|
1927
|
+
}).join("");
|
|
1928
|
+
};
|
|
1929
|
+
const renderers = {
|
|
1930
|
+
overview: renderOverview,
|
|
1931
|
+
state: renderState,
|
|
1932
|
+
clock: renderClock,
|
|
1933
|
+
faults: renderFaults,
|
|
1934
|
+
journal: renderJournal,
|
|
1935
|
+
routes: renderRoutes
|
|
1936
|
+
};
|
|
1937
|
+
async function refresh() {
|
|
1938
|
+
clearError();
|
|
1939
|
+
const render = renderers[state.view];
|
|
1940
|
+
if (render)
|
|
1941
|
+
await render();
|
|
1942
|
+
}
|
|
1943
|
+
el("namespace", HTMLSelectElement).addEventListener("change", () => {
|
|
1944
|
+
state.namespace = el("namespace", HTMLSelectElement).value;
|
|
1945
|
+
refresh().catch(showError);
|
|
1946
|
+
});
|
|
1947
|
+
keyInput.addEventListener("change", () => {
|
|
1948
|
+
state.key = keyInput.value.trim();
|
|
1949
|
+
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1950
|
+
refresh().catch(showError);
|
|
1951
|
+
});
|
|
1952
|
+
el("refresh", HTMLButtonElement).addEventListener("click", () => {
|
|
1953
|
+
refresh().catch(showError);
|
|
1954
|
+
});
|
|
1955
|
+
el("collections", HTMLElement).addEventListener("click", (event) => {
|
|
1956
|
+
const target = event.target;
|
|
1957
|
+
if (!(target instanceof Element))
|
|
1958
|
+
return;
|
|
1959
|
+
const button = target.closest("[data-collection]");
|
|
1960
|
+
if (!(button instanceof HTMLElement))
|
|
1961
|
+
return;
|
|
1962
|
+
const collection = button.dataset.collection;
|
|
1963
|
+
if (collection === void 0)
|
|
1964
|
+
return;
|
|
1965
|
+
state.collection = collection;
|
|
1966
|
+
renderState().catch(showError);
|
|
1967
|
+
});
|
|
1968
|
+
el("records", HTMLElement).addEventListener("click", (event) => {
|
|
1969
|
+
const target = event.target;
|
|
1970
|
+
if (!(target instanceof Element))
|
|
1971
|
+
return;
|
|
1972
|
+
const row = target.closest("[data-id]");
|
|
1973
|
+
if (!(row instanceof HTMLElement))
|
|
1974
|
+
return;
|
|
1975
|
+
const id = row.dataset.id;
|
|
1976
|
+
if (id === void 0)
|
|
1977
|
+
return;
|
|
1978
|
+
openEditor(id).catch(showError);
|
|
1979
|
+
});
|
|
1980
|
+
const editor = el("editor", HTMLDialogElement);
|
|
1981
|
+
const editorId = el("editor-id", HTMLInputElement);
|
|
1982
|
+
const editorValue = el("editor-value", HTMLTextAreaElement);
|
|
1983
|
+
const editorDelete = el("editor-delete", HTMLButtonElement);
|
|
1984
|
+
async function openEditor(id) {
|
|
1985
|
+
state.editing = id;
|
|
1986
|
+
el("editor-title", HTMLElement).textContent = id ? "Edit record" : "New record";
|
|
1987
|
+
editorId.value = id ?? "";
|
|
1988
|
+
editorId.readOnly = id !== null;
|
|
1989
|
+
editorDelete.hidden = id === null;
|
|
1990
|
+
if (id) {
|
|
1991
|
+
const record = await api("GET", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`);
|
|
1992
|
+
if (!isRecord2(record))
|
|
1993
|
+
throw new Error("Record response was not an object");
|
|
1994
|
+
editorValue.value = JSON.stringify(record.value, null, 2) ?? "";
|
|
1995
|
+
} else {
|
|
1996
|
+
editorValue.value = "{\n \n}";
|
|
1997
|
+
}
|
|
1998
|
+
editor.showModal();
|
|
1999
|
+
}
|
|
2000
|
+
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
2001
|
+
openEditor(null).catch(showError);
|
|
2002
|
+
});
|
|
2003
|
+
el("editor-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2004
|
+
const submitter = event.submitter;
|
|
2005
|
+
if (!(submitter instanceof HTMLButtonElement) || submitter.value !== "save")
|
|
2006
|
+
return;
|
|
2007
|
+
event.preventDefault();
|
|
2008
|
+
let value;
|
|
2009
|
+
try {
|
|
2010
|
+
value = readJson2(editorValue.value);
|
|
2011
|
+
} catch {
|
|
2012
|
+
showError(new Error("Value is not JSON"));
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
const id = editorId.value.trim();
|
|
2016
|
+
const path = `/state/${encodeURIComponent(state.collection)}`;
|
|
2017
|
+
const request = state.editing ? api("PUT", `${path}/${encodeURIComponent(id)}`, { value }) : api("POST", path, { id: id || void 0, value });
|
|
2018
|
+
request.then(() => {
|
|
2019
|
+
editor.close();
|
|
2020
|
+
return renderState();
|
|
2021
|
+
}).catch(showError);
|
|
2022
|
+
});
|
|
2023
|
+
editorDelete.addEventListener("click", () => {
|
|
2024
|
+
const id = state.editing;
|
|
2025
|
+
if (!id)
|
|
2026
|
+
return;
|
|
2027
|
+
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2028
|
+
editor.close();
|
|
2029
|
+
return renderState();
|
|
2030
|
+
}).catch(showError);
|
|
2031
|
+
});
|
|
2032
|
+
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2033
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2034
|
+
continue;
|
|
2035
|
+
button.addEventListener("click", () => {
|
|
2036
|
+
const raw = button.dataset.advance;
|
|
2037
|
+
if (raw === void 0)
|
|
2038
|
+
return;
|
|
2039
|
+
api("POST", "/clock", { advance: Number(raw) }).then(() => renderClock()).catch(showError);
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
el("freeze", HTMLButtonElement).addEventListener("click", () => {
|
|
2043
|
+
const frozen = el("freeze", HTMLButtonElement).dataset.frozen === "1";
|
|
2044
|
+
api("POST", "/clock", { freeze: !frozen }).then(() => renderClock()).catch(showError);
|
|
2045
|
+
});
|
|
2046
|
+
el("clock-reset", HTMLButtonElement).addEventListener("click", () => {
|
|
2047
|
+
api("POST", "/clock", { reset: true }).then(() => renderClock()).catch(showError);
|
|
2048
|
+
});
|
|
2049
|
+
el("clock-set", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2050
|
+
event.preventDefault();
|
|
2051
|
+
const raw = el("clock-instant", HTMLInputElement).value.trim();
|
|
2052
|
+
const set = /^-?\d+$/.test(raw) ? Number(raw) : raw;
|
|
2053
|
+
api("POST", "/clock", { set }).then(() => renderClock()).catch(showError);
|
|
2054
|
+
});
|
|
2055
|
+
el("fault-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2056
|
+
event.preventDefault();
|
|
2057
|
+
const preset = el("fault-preset", HTMLSelectElement).value;
|
|
2058
|
+
if (preset !== "") {
|
|
2059
|
+
api("POST", "/faults", { preset }).then(() => renderFaults()).catch(showError);
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
const status = Number(el("fault-status", HTMLInputElement).value);
|
|
2063
|
+
if (!Number.isFinite(status)) {
|
|
2064
|
+
showError(new Error("Status must be a number, or choose a preset"));
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
const path = el("fault-path", HTMLInputElement).value;
|
|
2068
|
+
const method = el("fault-method", HTMLInputElement).value;
|
|
2069
|
+
api("POST", "/faults", {
|
|
2070
|
+
status,
|
|
2071
|
+
...path !== "" ? { pathPrefix: path } : {},
|
|
2072
|
+
...method !== "" ? { method } : {}
|
|
2073
|
+
}).then(() => renderFaults()).catch(showError);
|
|
2074
|
+
});
|
|
2075
|
+
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2076
|
+
api("DELETE", "/faults").then(() => renderFaults()).catch(showError);
|
|
2077
|
+
});
|
|
2078
|
+
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2079
|
+
api("DELETE", "/requests").then(() => renderJournal()).catch(showError);
|
|
2080
|
+
});
|
|
2081
|
+
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2082
|
+
const target = event.target;
|
|
2083
|
+
if (!(target instanceof Element))
|
|
2084
|
+
return;
|
|
2085
|
+
const button = target.closest("[data-route]");
|
|
2086
|
+
if (!(button instanceof HTMLElement))
|
|
2087
|
+
return;
|
|
2088
|
+
const route = button.dataset.route;
|
|
2089
|
+
if (route === void 0)
|
|
2090
|
+
return;
|
|
2091
|
+
state.route = route;
|
|
2092
|
+
el("route-title", HTMLElement).textContent = route;
|
|
2093
|
+
const method = route.split(" ")[0] ?? "";
|
|
2094
|
+
el("route-body", HTMLTextAreaElement).disabled = method === "GET" || method === "DELETE";
|
|
2095
|
+
});
|
|
2096
|
+
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2097
|
+
event.preventDefault();
|
|
2098
|
+
const send = async () => {
|
|
2099
|
+
if (state.route === null)
|
|
2100
|
+
throw new Error("Choose a route first");
|
|
2101
|
+
const parts = state.route.split(" ");
|
|
2102
|
+
const method = parts[0];
|
|
2103
|
+
const path = parts.slice(1).join(" ");
|
|
2104
|
+
if (method === void 0)
|
|
2105
|
+
throw new Error("Choose a route first");
|
|
2106
|
+
const concrete = path.replace(/:([A-Za-z0-9_]+)/g, (_match, key) => prompt(`Value for ${key}`) || "");
|
|
2107
|
+
let body;
|
|
2108
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2109
|
+
if (method !== "GET" && method !== "DELETE" && routeBody.value.trim() !== "") {
|
|
2110
|
+
try {
|
|
2111
|
+
body = readJson2(routeBody.value);
|
|
2112
|
+
} catch {
|
|
2113
|
+
throw new Error("Body is not JSON");
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
const data = await api(method, concrete, body);
|
|
2117
|
+
el("route-result", HTMLElement).textContent = JSON.stringify(data, null, 2);
|
|
2118
|
+
};
|
|
2119
|
+
send().catch(showError);
|
|
2120
|
+
});
|
|
2121
|
+
const decodePanel = (value) => {
|
|
2122
|
+
if (!isRecord2(value) || typeof value.id !== "string" || typeof value.title !== "string") {
|
|
2123
|
+
return void 0;
|
|
2124
|
+
}
|
|
2125
|
+
const panel = {
|
|
2126
|
+
id: value.id,
|
|
2127
|
+
title: value.title,
|
|
2128
|
+
description: typeof value.description === "string" ? value.description : "",
|
|
2129
|
+
html: typeof value.html === "string" ? value.html : ""
|
|
2130
|
+
};
|
|
2131
|
+
if (typeof value.script === "string")
|
|
2132
|
+
panel.script = value.script;
|
|
2133
|
+
return panel;
|
|
2134
|
+
};
|
|
2135
|
+
const mountPanels = (panels) => {
|
|
2136
|
+
const nav = el("nav", HTMLElement);
|
|
2137
|
+
const views = el("views", HTMLElement);
|
|
2138
|
+
for (const panel of panels) {
|
|
2139
|
+
const button = document.createElement("button");
|
|
2140
|
+
button.type = "button";
|
|
2141
|
+
button.dataset.view = panel.id;
|
|
2142
|
+
button.textContent = panel.title;
|
|
2143
|
+
nav.append(button);
|
|
2144
|
+
const section = document.createElement("section");
|
|
2145
|
+
section.className = "view";
|
|
2146
|
+
section.id = `view-${panel.id}`;
|
|
2147
|
+
section.hidden = true;
|
|
2148
|
+
const title = document.createElement("h2");
|
|
2149
|
+
title.textContent = panel.title;
|
|
2150
|
+
const lede = document.createElement("p");
|
|
2151
|
+
lede.className = "lede";
|
|
2152
|
+
lede.textContent = panel.description;
|
|
2153
|
+
const body = document.createElement("div");
|
|
2154
|
+
body.className = "panel-body";
|
|
2155
|
+
body.id = `panel-${panel.id}`;
|
|
2156
|
+
body.innerHTML = panel.html;
|
|
2157
|
+
section.append(title, lede, body);
|
|
2158
|
+
views.append(section);
|
|
2159
|
+
if (panel.script === void 0)
|
|
2160
|
+
continue;
|
|
2161
|
+
try {
|
|
2162
|
+
const compiled = new Function("root", "api", panel.script);
|
|
2163
|
+
if (!isPanelScript(compiled))
|
|
2164
|
+
throw new Error("panel script did not compile");
|
|
2165
|
+
compiled(body, panelApi);
|
|
2166
|
+
} catch (error) {
|
|
2167
|
+
const note = document.createElement("p");
|
|
2168
|
+
note.className = "banner show";
|
|
2169
|
+
note.textContent = error instanceof Error ? error.message : String(error);
|
|
2170
|
+
body.append(note);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
};
|
|
2174
|
+
startAdminBrand({
|
|
2175
|
+
document,
|
|
2176
|
+
$: (id) => document.getElementById(id),
|
|
2177
|
+
params,
|
|
2178
|
+
location: { href: location.href, origin: location.origin },
|
|
2179
|
+
fetch,
|
|
2180
|
+
brandsUrl: config.brandsUrl
|
|
2181
|
+
});
|
|
2182
|
+
loadNamespaces().then(() => api("GET", "/ui/manifest")).then((manifest) => {
|
|
2183
|
+
const panels = requireList(isRecord2(manifest) ? manifest.panels : void 0, "Panels").flatMap((panel) => {
|
|
2184
|
+
const decoded = decodePanel(panel);
|
|
2185
|
+
return decoded === void 0 ? [] : [decoded];
|
|
2186
|
+
});
|
|
2187
|
+
mountPanels(panels);
|
|
2188
|
+
return refresh();
|
|
2189
|
+
}).catch(showError);
|
|
2190
|
+
}
|
|
2191
|
+
var clientPrelude = [isRecord2, mountAdminBrand, startAdminBrand, faultPresetList, bootAdmin].map((fn) => fn.toString()).join("\n");
|
|
2192
|
+
var adminClientSource = (config) => {
|
|
2193
|
+
const payload = JSON.stringify(config).replace(/</g, "\\u003c");
|
|
2194
|
+
return `<script>
|
|
2195
|
+
${clientPrelude}
|
|
2196
|
+
bootAdmin(${payload});
|
|
2197
|
+
</script>`;
|
|
2198
|
+
};
|
|
2199
|
+
|
|
974
2200
|
// ../core/dist/surface.js
|
|
975
2201
|
var defineMock = (runtime) => runtime;
|
|
976
2202
|
var STANDARD_ADMIN_ROUTES = [
|
|
@@ -1039,94 +2265,106 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
1039
2265
|
return "'";
|
|
1040
2266
|
}
|
|
1041
2267
|
});
|
|
2268
|
+
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
1042
2269
|
var renderAdminDocument = (service) => {
|
|
1043
2270
|
const name = escapeHtml(service);
|
|
1044
|
-
const
|
|
2271
|
+
const script = adminClientSource({
|
|
2272
|
+
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2273
|
+
adminPrefix: ADMIN_PREFIX,
|
|
2274
|
+
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2275
|
+
brandsUrl: ADMIN_BRANDS_URL,
|
|
2276
|
+
service
|
|
2277
|
+
});
|
|
1045
2278
|
return `<!DOCTYPE html>
|
|
1046
2279
|
<html lang="en">
|
|
1047
2280
|
<head>
|
|
1048
2281
|
<meta charset="utf-8">
|
|
1049
2282
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1050
2283
|
<meta name="color-scheme" content="light dark">
|
|
1051
|
-
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='
|
|
2284
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='2' fill='%2327272a'/%3E%3C/svg%3E">
|
|
1052
2285
|
<title>${name} admin</title>
|
|
1053
2286
|
<style>
|
|
2287
|
+
${CSS_RESET}
|
|
1054
2288
|
:root {
|
|
1055
2289
|
color-scheme: light;
|
|
1056
|
-
--bg: #
|
|
2290
|
+
--bg: #fafafa;
|
|
1057
2291
|
--bg-elev: #ffffff;
|
|
1058
|
-
--bg-muted: #
|
|
1059
|
-
--bg-inset: #
|
|
1060
|
-
--line: #
|
|
1061
|
-
--line-strong: #
|
|
1062
|
-
--ink: #
|
|
1063
|
-
--muted: #
|
|
1064
|
-
--faint: #
|
|
1065
|
-
--accent: #
|
|
1066
|
-
--accent-ink: #
|
|
1067
|
-
--accent-soft: #
|
|
1068
|
-
--ok: #
|
|
1069
|
-
--ok-soft: #
|
|
1070
|
-
--warn: #
|
|
1071
|
-
--warn-soft: #
|
|
1072
|
-
--err: #
|
|
1073
|
-
--err-soft: #
|
|
1074
|
-
--shadow:
|
|
1075
|
-
--sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2292
|
+
--bg-muted: #f4f4f5;
|
|
2293
|
+
--bg-inset: #f4f4f5;
|
|
2294
|
+
--line: #dedee3;
|
|
2295
|
+
--line-strong: #c4c4cc;
|
|
2296
|
+
--ink: #18181b;
|
|
2297
|
+
--muted: #62626b;
|
|
2298
|
+
--faint: #71717a;
|
|
2299
|
+
--accent: #27272a;
|
|
2300
|
+
--accent-ink: #ffffff;
|
|
2301
|
+
--accent-soft: #f4f4f5;
|
|
2302
|
+
--ok: #166534;
|
|
2303
|
+
--ok-soft: #f0fdf4;
|
|
2304
|
+
--warn: #a16207;
|
|
2305
|
+
--warn-soft: #fefce8;
|
|
2306
|
+
--err: #a82d32;
|
|
2307
|
+
--err-soft: #fef2f2;
|
|
2308
|
+
--shadow: none;
|
|
2309
|
+
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1076
2310
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
1077
|
-
--radius:
|
|
1078
|
-
--header:
|
|
1079
|
-
--nav:
|
|
2311
|
+
--radius: 10px;
|
|
2312
|
+
--header: 56px;
|
|
2313
|
+
--nav: 200px;
|
|
1080
2314
|
}
|
|
1081
2315
|
:root[data-theme="dark"] {
|
|
1082
2316
|
color-scheme: dark;
|
|
1083
|
-
--bg: #
|
|
1084
|
-
--bg-elev: #
|
|
1085
|
-
--bg-muted: #
|
|
1086
|
-
--bg-inset: #
|
|
1087
|
-
--line: #
|
|
1088
|
-
--line-strong: #
|
|
1089
|
-
--ink: #
|
|
1090
|
-
--muted: #
|
|
1091
|
-
--faint: #
|
|
1092
|
-
--accent: #
|
|
1093
|
-
--accent-ink: #
|
|
1094
|
-
--accent-soft: #
|
|
1095
|
-
--ok: #
|
|
1096
|
-
--ok-soft: #
|
|
1097
|
-
--warn: #
|
|
1098
|
-
--warn-soft: #
|
|
1099
|
-
--err: #
|
|
1100
|
-
--err-soft: #
|
|
1101
|
-
--shadow:
|
|
2317
|
+
--bg: #111113;
|
|
2318
|
+
--bg-elev: #19191c;
|
|
2319
|
+
--bg-muted: #242428;
|
|
2320
|
+
--bg-inset: #111113;
|
|
2321
|
+
--line: #36363c;
|
|
2322
|
+
--line-strong: #52525b;
|
|
2323
|
+
--ink: #f4f4f5;
|
|
2324
|
+
--muted: #a9a9b2;
|
|
2325
|
+
--faint: #71717a;
|
|
2326
|
+
--accent: #e4e4e7;
|
|
2327
|
+
--accent-ink: #18181b;
|
|
2328
|
+
--accent-soft: #242428;
|
|
2329
|
+
--ok: #86efac;
|
|
2330
|
+
--ok-soft: #14241b;
|
|
2331
|
+
--warn: #fde68a;
|
|
2332
|
+
--warn-soft: #2a2410;
|
|
2333
|
+
--err: #ffaaaa;
|
|
2334
|
+
--err-soft: #2c1516;
|
|
2335
|
+
--shadow: none;
|
|
1102
2336
|
}
|
|
1103
2337
|
@media (prefers-color-scheme: dark) {
|
|
1104
2338
|
:root:not([data-theme="light"]) {
|
|
1105
2339
|
color-scheme: dark;
|
|
1106
|
-
--bg: #
|
|
1107
|
-
--bg-elev: #
|
|
1108
|
-
--bg-muted: #
|
|
1109
|
-
--bg-inset: #
|
|
1110
|
-
--line: #
|
|
1111
|
-
--line-strong: #
|
|
1112
|
-
--ink: #
|
|
1113
|
-
--muted: #
|
|
1114
|
-
--faint: #
|
|
1115
|
-
--accent: #
|
|
1116
|
-
--accent-ink: #
|
|
1117
|
-
--accent-soft: #
|
|
1118
|
-
--ok: #
|
|
1119
|
-
--ok-soft: #
|
|
1120
|
-
--warn: #
|
|
1121
|
-
--warn-soft: #
|
|
1122
|
-
--err: #
|
|
1123
|
-
--err-soft: #
|
|
1124
|
-
--shadow:
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
html
|
|
2340
|
+
--bg: #111113;
|
|
2341
|
+
--bg-elev: #19191c;
|
|
2342
|
+
--bg-muted: #242428;
|
|
2343
|
+
--bg-inset: #111113;
|
|
2344
|
+
--line: #36363c;
|
|
2345
|
+
--line-strong: #52525b;
|
|
2346
|
+
--ink: #f4f4f5;
|
|
2347
|
+
--muted: #a9a9b2;
|
|
2348
|
+
--faint: #71717a;
|
|
2349
|
+
--accent: #e4e4e7;
|
|
2350
|
+
--accent-ink: #18181b;
|
|
2351
|
+
--accent-soft: #242428;
|
|
2352
|
+
--ok: #86efac;
|
|
2353
|
+
--ok-soft: #14241b;
|
|
2354
|
+
--warn: #fde68a;
|
|
2355
|
+
--warn-soft: #2a2410;
|
|
2356
|
+
--err: #ffaaaa;
|
|
2357
|
+
--err-soft: #2c1516;
|
|
2358
|
+
--shadow: none;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
2362
|
+
html { height: 100%; }
|
|
1129
2363
|
body {
|
|
2364
|
+
margin: 0;
|
|
2365
|
+
min-height: 100%;
|
|
2366
|
+
display: flex;
|
|
2367
|
+
flex-direction: column;
|
|
1130
2368
|
font-family: var(--sans);
|
|
1131
2369
|
background: var(--bg);
|
|
1132
2370
|
color: var(--ink);
|
|
@@ -1135,24 +2373,37 @@ var renderAdminDocument = (service) => {
|
|
|
1135
2373
|
padding-right: env(safe-area-inset-right);
|
|
1136
2374
|
-webkit-text-size-adjust: 100%;
|
|
1137
2375
|
}
|
|
1138
|
-
button, input, select, textarea { font: inherit; color: inherit; }
|
|
1139
|
-
button { cursor: pointer; }
|
|
1140
2376
|
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
1141
|
-
|
|
1142
|
-
* { scroll-behavior: auto !important; transition: none !important; }
|
|
1143
|
-
}
|
|
1144
|
-
.app { min-height: 100%; display: flex; flex-direction: column; }
|
|
2377
|
+
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
1145
2378
|
header.top {
|
|
1146
2379
|
position: sticky; top: 0; z-index: 5;
|
|
1147
2380
|
display: flex; align-items: center; gap: 12px;
|
|
1148
2381
|
min-height: var(--header); padding: 10px 16px;
|
|
1149
|
-
background:
|
|
2382
|
+
background: var(--bg-elev);
|
|
1150
2383
|
border-bottom: 1px solid var(--line);
|
|
1151
|
-
|
|
1152
|
-
}
|
|
1153
|
-
.brand {
|
|
1154
|
-
|
|
1155
|
-
|
|
2384
|
+
min-width: 0; max-width: 100%;
|
|
2385
|
+
}
|
|
2386
|
+
.brand {
|
|
2387
|
+
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2388
|
+
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2389
|
+
}
|
|
2390
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; }
|
|
2391
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.01em; }
|
|
2392
|
+
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2393
|
+
.vendor {
|
|
2394
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2395
|
+
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
2396
|
+
}
|
|
2397
|
+
.vendor[hidden] { display: none; }
|
|
2398
|
+
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
2399
|
+
.vendor img {
|
|
2400
|
+
width: 16px; height: 16px; padding: 2px; box-sizing: content-box;
|
|
2401
|
+
border-radius: 4px; border: 1px solid var(--line); background: #fff; object-fit: contain;
|
|
2402
|
+
}
|
|
2403
|
+
.vendor-name { color: var(--ink); font-weight: 600; }
|
|
2404
|
+
.vendor-sep { color: var(--faint); }
|
|
2405
|
+
.vendor-link { color: inherit; text-decoration: none; }
|
|
2406
|
+
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
1156
2407
|
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
1157
2408
|
label.field { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--faint); }
|
|
1158
2409
|
label.field input, label.field select, .controls input[type="password"] {
|
|
@@ -1170,7 +2421,7 @@ var renderAdminDocument = (service) => {
|
|
|
1170
2421
|
record or the full nav row would widen the page past the viewport. */
|
|
1171
2422
|
.layout {
|
|
1172
2423
|
display: grid; grid-template-columns: var(--nav) minmax(0, 1fr);
|
|
1173
|
-
flex: 1
|
|
2424
|
+
flex: 1 0 auto; min-width: 0;
|
|
1174
2425
|
}
|
|
1175
2426
|
nav.side {
|
|
1176
2427
|
border-right: 1px solid var(--line); padding: 12px;
|
|
@@ -1224,8 +2475,8 @@ var renderAdminDocument = (service) => {
|
|
|
1224
2475
|
.banner.show { display: block; }
|
|
1225
2476
|
.empty { color: var(--muted); padding: 24px 8px; }
|
|
1226
2477
|
dialog {
|
|
1227
|
-
border: 1px solid var(--line); border-radius:
|
|
1228
|
-
color: var(--ink); width: min(640px, calc(100vw - 24px)); box-shadow:
|
|
2478
|
+
border: 1px solid var(--line); border-radius: var(--radius); padding: 0; background: var(--bg-elev);
|
|
2479
|
+
color: var(--ink); width: min(640px, calc(100vw - 24px)); box-shadow: none;
|
|
1229
2480
|
}
|
|
1230
2481
|
dialog::backdrop { background: rgb(20 19 17 / 0.45); }
|
|
1231
2482
|
dialog form, .dialog-body { padding: 16px; display: flex; flex-direction: column; gap: 10px; }
|
|
@@ -1252,7 +2503,7 @@ var renderAdminDocument = (service) => {
|
|
|
1252
2503
|
padding-top: max(10px, env(safe-area-inset-top));
|
|
1253
2504
|
}
|
|
1254
2505
|
input, select, textarea { font-size: 16px; }
|
|
1255
|
-
.controls { width: 100%; }
|
|
2506
|
+
.brand, .vendor, .controls { width: 100%; }
|
|
1256
2507
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
1257
2508
|
.layout { grid-template-columns: minmax(0, 1fr); }
|
|
1258
2509
|
nav.side {
|
|
@@ -1283,8 +2534,11 @@ var renderAdminDocument = (service) => {
|
|
|
1283
2534
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
1284
2535
|
<header class="top">
|
|
1285
2536
|
<div class="brand">
|
|
1286
|
-
<
|
|
1287
|
-
|
|
2537
|
+
<div class="brand-id">
|
|
2538
|
+
<strong>${name}</strong>
|
|
2539
|
+
<span>Admin</span>
|
|
2540
|
+
</div>
|
|
2541
|
+
<div class="vendor" id="vendor" hidden></div>
|
|
1288
2542
|
</div>
|
|
1289
2543
|
<div class="controls">
|
|
1290
2544
|
<label class="field">Namespace
|
|
@@ -1414,460 +2668,7 @@ var renderAdminDocument = (service) => {
|
|
|
1414
2668
|
</div>
|
|
1415
2669
|
</form>
|
|
1416
2670
|
</dialog>
|
|
1417
|
-
|
|
1418
|
-
(() => {
|
|
1419
|
-
const STANDARD = new Set(${standard})
|
|
1420
|
-
const state = {
|
|
1421
|
-
namespace: "default",
|
|
1422
|
-
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1423
|
-
view: "overview",
|
|
1424
|
-
collection: "",
|
|
1425
|
-
shape: null,
|
|
1426
|
-
route: null,
|
|
1427
|
-
editing: null
|
|
1428
|
-
}
|
|
1429
|
-
const $ = (id) => document.getElementById(id)
|
|
1430
|
-
const banner = $("banner")
|
|
1431
|
-
const keyInput = $("admin-key")
|
|
1432
|
-
keyInput.value = state.key
|
|
1433
|
-
const params = new URLSearchParams(location.search)
|
|
1434
|
-
if (params.get("key")) {
|
|
1435
|
-
state.key = params.get("key")
|
|
1436
|
-
keyInput.value = state.key
|
|
1437
|
-
sessionStorage.setItem("mockingbird-admin-key", state.key)
|
|
1438
|
-
params.delete("key")
|
|
1439
|
-
const next = params.toString()
|
|
1440
|
-
history.replaceState(null, "", location.pathname + (next ? "?" + next : ""))
|
|
1441
|
-
}
|
|
1442
|
-
if (params.get("namespace")) state.namespace = params.get("namespace")
|
|
1443
|
-
|
|
1444
|
-
const showError = (error) => {
|
|
1445
|
-
banner.textContent = error instanceof Error ? error.message : String(error)
|
|
1446
|
-
banner.classList.add("show")
|
|
1447
|
-
}
|
|
1448
|
-
const clearError = () => banner.classList.remove("show")
|
|
1449
|
-
|
|
1450
|
-
async function originGet(path) {
|
|
1451
|
-
const response = await fetch(path, { headers: { accept: "application/json" } })
|
|
1452
|
-
if (!response.ok) throw new Error(response.status + " " + path)
|
|
1453
|
-
return response.json()
|
|
1454
|
-
}
|
|
1455
|
-
async function api(method, path, body) {
|
|
1456
|
-
const headers = { accept: "application/json" }
|
|
1457
|
-
if (state.key) headers["${ADMIN_KEY_HEADER}"] = state.key
|
|
1458
|
-
if (state.namespace) headers["x-mockingbird-namespace"] = state.namespace
|
|
1459
|
-
if (body !== undefined) headers["content-type"] = "application/json"
|
|
1460
|
-
const response = await fetch("${ADMIN_PREFIX}" + path, {
|
|
1461
|
-
method,
|
|
1462
|
-
headers,
|
|
1463
|
-
body: body === undefined ? undefined : JSON.stringify(body)
|
|
1464
|
-
})
|
|
1465
|
-
const text = await response.text()
|
|
1466
|
-
let data = null
|
|
1467
|
-
if (text) {
|
|
1468
|
-
try { data = JSON.parse(text) } catch { data = { raw: text } }
|
|
1469
|
-
}
|
|
1470
|
-
if (response.status === 401) {
|
|
1471
|
-
keyInput.focus()
|
|
1472
|
-
throw new Error((data && data.error && data.error.message) || "Admin key required")
|
|
1473
|
-
}
|
|
1474
|
-
if (!response.ok) {
|
|
1475
|
-
throw new Error((data && data.error && data.error.message) || (response.status + " " + method + " " + path))
|
|
1476
|
-
}
|
|
1477
|
-
return data
|
|
1478
|
-
}
|
|
1479
|
-
const panelApi = {
|
|
1480
|
-
get: (path) => api("GET", path),
|
|
1481
|
-
send: (method, path, body) => api(method, path, body),
|
|
1482
|
-
namespace: () => state.namespace
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
function setTheme(theme) {
|
|
1486
|
-
if (theme) document.documentElement.dataset.theme = theme
|
|
1487
|
-
else delete document.documentElement.dataset.theme
|
|
1488
|
-
}
|
|
1489
|
-
const storedTheme = localStorage.getItem("mockingbird-admin-theme")
|
|
1490
|
-
if (storedTheme) setTheme(storedTheme)
|
|
1491
|
-
$("theme").addEventListener("click", () => {
|
|
1492
|
-
const current = document.documentElement.dataset.theme
|
|
1493
|
-
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches
|
|
1494
|
-
const next = dark ? "light" : "dark"
|
|
1495
|
-
localStorage.setItem("mockingbird-admin-theme", next)
|
|
1496
|
-
setTheme(next)
|
|
1497
|
-
})
|
|
1498
|
-
|
|
1499
|
-
function show(view) {
|
|
1500
|
-
state.view = view
|
|
1501
|
-
for (const button of document.querySelectorAll("#nav button")) {
|
|
1502
|
-
button.setAttribute("aria-current", button.dataset.view === view ? "true" : "false")
|
|
1503
|
-
}
|
|
1504
|
-
for (const section of document.querySelectorAll("main .view")) {
|
|
1505
|
-
section.hidden = section.id !== "view-" + view
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
document.getElementById("nav").addEventListener("click", (event) => {
|
|
1509
|
-
const button = event.target.closest("button[data-view]")
|
|
1510
|
-
if (!button) return
|
|
1511
|
-
show(button.dataset.view)
|
|
1512
|
-
refresh().catch(showError)
|
|
1513
|
-
})
|
|
1514
|
-
|
|
1515
|
-
function cards(items) {
|
|
1516
|
-
return '<div class="grid">' + items.map((item) =>
|
|
1517
|
-
'<article class="card"><div class="k">' + item.k + '</div><div class="v">' + item.v + '</div></article>'
|
|
1518
|
-
).join("") + "</div>"
|
|
1519
|
-
}
|
|
1520
|
-
function table(columns, rows) {
|
|
1521
|
-
if (rows.length === 0) return '<p class="empty">Nothing here yet.</p>'
|
|
1522
|
-
const head = "<tr>" + columns.map((column) => "<th>" + column.label + "</th>").join("") + "</tr>"
|
|
1523
|
-
const body = rows.map((row) => "<tr" + (row.attrs || "") + ">" + columns.map((column) =>
|
|
1524
|
-
'<td data-label="' + column.label + '">' + column.cell(row) + "</td>"
|
|
1525
|
-
).join("") + "</tr>").join("")
|
|
1526
|
-
return "<table><thead>" + head + "</thead><tbody>" + body + "</tbody></table>"
|
|
1527
|
-
}
|
|
1528
|
-
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({
|
|
1529
|
-
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
|
1530
|
-
}[char]))
|
|
1531
|
-
const preview = (value) => {
|
|
1532
|
-
const text = JSON.stringify(value)
|
|
1533
|
-
return esc(text.length > 180 ? text.slice(0, 180) + "\u2026" : text)
|
|
1534
|
-
}
|
|
1535
|
-
const when = (epoch) => {
|
|
1536
|
-
const date = new Date(epoch)
|
|
1537
|
-
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString()
|
|
1538
|
-
}
|
|
1539
|
-
const clockCard = (epoch) => {
|
|
1540
|
-
const date = new Date(epoch)
|
|
1541
|
-
if (Number.isNaN(date.getTime())) return esc(String(epoch))
|
|
1542
|
-
const iso = date.toISOString()
|
|
1543
|
-
return esc(iso.slice(0, 10)) + '<span class="clock-time">' + esc(iso.slice(11, 19)) + " UTC</span>"
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
async function loadNamespaces() {
|
|
1547
|
-
const data = await api("GET", "/namespaces")
|
|
1548
|
-
const select = $("namespace")
|
|
1549
|
-
const names = data.namespaces && data.namespaces.length ? data.namespaces : [data.default || "default"]
|
|
1550
|
-
if (!names.includes(state.namespace)) names.unshift(state.namespace)
|
|
1551
|
-
select.innerHTML = names.map((name) =>
|
|
1552
|
-
'<option value="' + esc(name) + '">' + esc(name) + "</option>"
|
|
1553
|
-
).join("")
|
|
1554
|
-
select.value = state.namespace
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
async function renderOverview() {
|
|
1558
|
-
const [health, shape, clock, journal] = await Promise.all([
|
|
1559
|
-
originGet("/health"),
|
|
1560
|
-
api("GET", "/state"),
|
|
1561
|
-
api("GET", "/clock"),
|
|
1562
|
-
api("GET", "/requests?limit=8")
|
|
1563
|
-
])
|
|
1564
|
-
state.shape = shape
|
|
1565
|
-
$("overview-cards").innerHTML = cards([
|
|
1566
|
-
{ k: "Service", v: esc(health.service || "${name}") },
|
|
1567
|
-
{ k: "Status", v: esc(health.status || "ok") },
|
|
1568
|
-
{ k: "Namespace", v: esc(state.namespace) },
|
|
1569
|
-
{ k: "Collections", v: String(shape.collections.length) },
|
|
1570
|
-
{ k: "Records", v: String(shape.collections.reduce((sum, item) => sum + item.count, 0)) },
|
|
1571
|
-
{ k: "Clock", v: clockCard(clock.now) }
|
|
1572
|
-
])
|
|
1573
|
-
const requests = (journal && journal.requests) || []
|
|
1574
|
-
$("overview-requests").innerHTML = table(
|
|
1575
|
-
[
|
|
1576
|
-
{ label: "Method", cell: (row) => esc(row.method) },
|
|
1577
|
-
{ label: "Path", cell: (row) => esc(row.path) },
|
|
1578
|
-
{ label: "Status", cell: (row) => '<span class="status-' + String(row.status).charAt(0) + '">' + esc(row.status) + "</span>" },
|
|
1579
|
-
{ label: "Operation", cell: (row) => esc(row.operationId || "") }
|
|
1580
|
-
],
|
|
1581
|
-
requests
|
|
1582
|
-
)
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
function schemaChips(collection) {
|
|
1586
|
-
if (!collection || collection.fields.length === 0) {
|
|
1587
|
-
return '<p class="empty">No fields yet. Add a record or declare the collection on the service.</p>'
|
|
1588
|
-
}
|
|
1589
|
-
return '<div class="chips">' + collection.fields.map((field) =>
|
|
1590
|
-
'<span class="chip">' + esc(field.name) + ' <i>' + esc(field.kind) + (field.optional ? "?" : "") + "</i></span>"
|
|
1591
|
-
).join("") + "</div>"
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
async function renderState() {
|
|
1595
|
-
const shape = await api("GET", "/state")
|
|
1596
|
-
state.shape = shape
|
|
1597
|
-
const host = $("collections")
|
|
1598
|
-
if (shape.collections.length === 0) {
|
|
1599
|
-
host.innerHTML = '<p class="empty">This namespace has no collections yet.</p>'
|
|
1600
|
-
$("schema").innerHTML = ""
|
|
1601
|
-
$("records").innerHTML = ""
|
|
1602
|
-
return
|
|
1603
|
-
}
|
|
1604
|
-
if (!shape.collections.some((item) => item.name === state.collection)) {
|
|
1605
|
-
state.collection = shape.collections[0].name
|
|
1606
|
-
}
|
|
1607
|
-
host.innerHTML = shape.collections.map((item) =>
|
|
1608
|
-
'<button type="button" class="collection" data-collection="' + esc(item.name) + '" aria-current="' +
|
|
1609
|
-
(item.name === state.collection ? "true" : "false") + '"><strong>' + esc(item.label) +
|
|
1610
|
-
'</strong><small>' + esc(item.count) + " \xB7 " + esc(item.source) + "</small></button>"
|
|
1611
|
-
).join("")
|
|
1612
|
-
const selected = shape.collections.find((item) => item.name === state.collection)
|
|
1613
|
-
$("schema").innerHTML = "<h3>" + esc(selected.label) + "</h3>" +
|
|
1614
|
-
(selected.description ? "<p class=" + '"lede">' + esc(selected.description) + "</p>" : "") +
|
|
1615
|
-
schemaChips(selected)
|
|
1616
|
-
const page = await api("GET", "/state/" + encodeURIComponent(state.collection) + "?limit=50")
|
|
1617
|
-
const rows = page.records.map((record) => ({
|
|
1618
|
-
...record,
|
|
1619
|
-
attrs: ' class="clickable" data-id="' + esc(record.id) + '"'
|
|
1620
|
-
}))
|
|
1621
|
-
$("records").innerHTML = table(
|
|
1622
|
-
[
|
|
1623
|
-
{ label: "Id", cell: (row) => "<code>" + esc(row.id) + "</code>" },
|
|
1624
|
-
{ label: "Seq", cell: (row) => esc(row.seq) },
|
|
1625
|
-
{ label: "Value", cell: (row) => preview(row.value) }
|
|
1626
|
-
],
|
|
1627
|
-
rows
|
|
1628
|
-
)
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
async function renderClock() {
|
|
1632
|
-
const clock = await api("GET", "/clock")
|
|
1633
|
-
$("clock-readout").innerHTML = "<div class='k'>Now</div><div class='v'>" + esc(when(clock.now)) +
|
|
1634
|
-
"</div><p>" + esc(clock.now) + " ms \xB7 offset " + esc(clock.offsetMs) + " ms " +
|
|
1635
|
-
(clock.frozen ? "<span class='badge warn'>frozen</span>" : "<span class='badge ok'>live</span>") + "</p>"
|
|
1636
|
-
$("freeze").textContent = clock.frozen ? "Unfreeze" : "Freeze"
|
|
1637
|
-
$("freeze").dataset.frozen = clock.frozen ? "1" : "0"
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
async function renderFaults() {
|
|
1641
|
-
const [faults, presets] = await Promise.all([
|
|
1642
|
-
api("GET", "/faults"),
|
|
1643
|
-
api("GET", "/faults/presets").catch(() => ({ presets: [] }))
|
|
1644
|
-
])
|
|
1645
|
-
const select = $("fault-preset")
|
|
1646
|
-
const current = select.value
|
|
1647
|
-
const list = presets.presets || []
|
|
1648
|
-
select.innerHTML = '<option value="">Custom rule</option>' + list.map((preset) =>
|
|
1649
|
-
'<option value="' + esc(preset.name) + '">' + esc(preset.name) + "</option>"
|
|
1650
|
-
).join("")
|
|
1651
|
-
select.value = current
|
|
1652
|
-
const rules = faults.faults || []
|
|
1653
|
-
$("fault-list").innerHTML = table(
|
|
1654
|
-
[
|
|
1655
|
-
{ label: "Id", cell: (row) => esc(row.id) },
|
|
1656
|
-
{ label: "Match", cell: (row) => esc([row.method, row.pathPrefix, row.operationId].filter(Boolean).join(" ") || "all") },
|
|
1657
|
-
{ label: "Effect", cell: (row) => esc(row.status ?? row.effect ?? (row.drop ? "drop" : row.latencyMs || "")) }
|
|
1658
|
-
],
|
|
1659
|
-
rules
|
|
1660
|
-
)
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
async function renderJournal() {
|
|
1664
|
-
const journal = await api("GET", "/requests?limit=100")
|
|
1665
|
-
$("journal").innerHTML = table(
|
|
1666
|
-
[
|
|
1667
|
-
{ label: "When", cell: (row) => esc(row.at || "") },
|
|
1668
|
-
{ label: "Method", cell: (row) => esc(row.method) },
|
|
1669
|
-
{ label: "Path", cell: (row) => esc(row.path) },
|
|
1670
|
-
{ label: "Status", cell: (row) => esc(row.status) },
|
|
1671
|
-
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
1672
|
-
],
|
|
1673
|
-
journal.requests || []
|
|
1674
|
-
)
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
async function renderRoutes() {
|
|
1678
|
-
const data = await api("GET", "/")
|
|
1679
|
-
const routes = data.routes || []
|
|
1680
|
-
$("route-list").innerHTML = routes.map((route) => {
|
|
1681
|
-
const extra = STANDARD.has(route) ? "" : " <span class='badge'>extension</span>"
|
|
1682
|
-
return '<div class="route"><span class="method">' + esc(route.split(" ")[0]) +
|
|
1683
|
-
'</span><span>' + esc(route.split(" ").slice(1).join(" ")) + extra +
|
|
1684
|
-
'</span><button class="btn" type="button" data-route="' + esc(route) + '">Use</button></div>'
|
|
1685
|
-
}).join("")
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
const renderers = {
|
|
1689
|
-
overview: renderOverview,
|
|
1690
|
-
state: renderState,
|
|
1691
|
-
clock: renderClock,
|
|
1692
|
-
faults: renderFaults,
|
|
1693
|
-
journal: renderJournal,
|
|
1694
|
-
routes: renderRoutes
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
async function refresh() {
|
|
1698
|
-
clearError()
|
|
1699
|
-
if (state.view in renderers) await renderers[state.view]()
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
$("namespace").addEventListener("change", () => {
|
|
1703
|
-
state.namespace = $("namespace").value
|
|
1704
|
-
refresh().catch(showError)
|
|
1705
|
-
})
|
|
1706
|
-
keyInput.addEventListener("change", () => {
|
|
1707
|
-
state.key = keyInput.value.trim()
|
|
1708
|
-
sessionStorage.setItem("mockingbird-admin-key", state.key)
|
|
1709
|
-
refresh().catch(showError)
|
|
1710
|
-
})
|
|
1711
|
-
$("refresh").addEventListener("click", () => refresh().catch(showError))
|
|
1712
|
-
|
|
1713
|
-
$("collections").addEventListener("click", (event) => {
|
|
1714
|
-
const button = event.target.closest("[data-collection]")
|
|
1715
|
-
if (!button) return
|
|
1716
|
-
state.collection = button.dataset.collection
|
|
1717
|
-
renderState().catch(showError)
|
|
1718
|
-
})
|
|
1719
|
-
$("records").addEventListener("click", (event) => {
|
|
1720
|
-
const row = event.target.closest("[data-id]")
|
|
1721
|
-
if (!row) return
|
|
1722
|
-
openEditor(row.dataset.id).catch(showError)
|
|
1723
|
-
})
|
|
1724
|
-
|
|
1725
|
-
const editor = $("editor")
|
|
1726
|
-
async function openEditor(id) {
|
|
1727
|
-
state.editing = id || null
|
|
1728
|
-
$("editor-title").textContent = id ? "Edit record" : "New record"
|
|
1729
|
-
$("editor-id").value = id || ""
|
|
1730
|
-
$("editor-id").readOnly = Boolean(id)
|
|
1731
|
-
$("editor-delete").hidden = !id
|
|
1732
|
-
if (id) {
|
|
1733
|
-
const record = await api("GET", "/state/" + encodeURIComponent(state.collection) + "/" + encodeURIComponent(id))
|
|
1734
|
-
$("editor-value").value = JSON.stringify(record.value, null, 2)
|
|
1735
|
-
} else {
|
|
1736
|
-
$("editor-value").value = "{\\n \\n}"
|
|
1737
|
-
}
|
|
1738
|
-
editor.showModal()
|
|
1739
|
-
}
|
|
1740
|
-
$("new-record").addEventListener("click", () => openEditor(null).catch(showError))
|
|
1741
|
-
$("editor-form").addEventListener("submit", (event) => {
|
|
1742
|
-
const submitter = event.submitter
|
|
1743
|
-
if (!submitter || submitter.value !== "save") return
|
|
1744
|
-
event.preventDefault()
|
|
1745
|
-
let value
|
|
1746
|
-
try { value = JSON.parse($("editor-value").value) }
|
|
1747
|
-
catch { showError(new Error("Value is not JSON")); return }
|
|
1748
|
-
const id = $("editor-id").value.trim()
|
|
1749
|
-
const path = "/state/" + encodeURIComponent(state.collection)
|
|
1750
|
-
const request = state.editing
|
|
1751
|
-
? api("PUT", path + "/" + encodeURIComponent(id), { value })
|
|
1752
|
-
: api("POST", path, { id: id || undefined, value })
|
|
1753
|
-
request.then(() => { editor.close(); return renderState() }).catch(showError)
|
|
1754
|
-
})
|
|
1755
|
-
$("editor-delete").addEventListener("click", () => {
|
|
1756
|
-
const id = state.editing
|
|
1757
|
-
if (!id) return
|
|
1758
|
-
api("DELETE", "/state/" + encodeURIComponent(state.collection) + "/" + encodeURIComponent(id))
|
|
1759
|
-
.then(() => { editor.close(); return renderState() })
|
|
1760
|
-
.catch(showError)
|
|
1761
|
-
})
|
|
1762
|
-
|
|
1763
|
-
document.querySelectorAll("[data-advance]").forEach((button) => {
|
|
1764
|
-
button.addEventListener("click", () => {
|
|
1765
|
-
api("POST", "/clock", { advance: Number(button.dataset.advance) }).then(renderClock).catch(showError)
|
|
1766
|
-
})
|
|
1767
|
-
})
|
|
1768
|
-
$("freeze").addEventListener("click", () => {
|
|
1769
|
-
const frozen = $("freeze").dataset.frozen === "1"
|
|
1770
|
-
api("POST", "/clock", { freeze: !frozen }).then(renderClock).catch(showError)
|
|
1771
|
-
})
|
|
1772
|
-
$("clock-reset").addEventListener("click", () => {
|
|
1773
|
-
api("POST", "/clock", { reset: true }).then(renderClock).catch(showError)
|
|
1774
|
-
})
|
|
1775
|
-
$("clock-set").addEventListener("submit", (event) => {
|
|
1776
|
-
event.preventDefault()
|
|
1777
|
-
const raw = $("clock-instant").value.trim()
|
|
1778
|
-
const set = /^-?\\d+$/.test(raw) ? Number(raw) : raw
|
|
1779
|
-
api("POST", "/clock", { set }).then(renderClock).catch(showError)
|
|
1780
|
-
})
|
|
1781
|
-
|
|
1782
|
-
$("fault-form").addEventListener("submit", (event) => {
|
|
1783
|
-
event.preventDefault()
|
|
1784
|
-
const preset = $("fault-preset").value
|
|
1785
|
-
const body = preset
|
|
1786
|
-
? { preset }
|
|
1787
|
-
: {
|
|
1788
|
-
status: Number($("fault-status").value),
|
|
1789
|
-
...($("fault-path").value ? { pathPrefix: $("fault-path").value } : {}),
|
|
1790
|
-
...($("fault-method").value ? { method: $("fault-method").value } : {})
|
|
1791
|
-
}
|
|
1792
|
-
if (!preset && !Number.isFinite(body.status)) {
|
|
1793
|
-
showError(new Error("Status must be a number, or choose a preset"))
|
|
1794
|
-
return
|
|
1795
|
-
}
|
|
1796
|
-
api("POST", "/faults", body).then(renderFaults).catch(showError)
|
|
1797
|
-
})
|
|
1798
|
-
$("fault-clear").addEventListener("click", () => api("DELETE", "/faults").then(renderFaults).catch(showError))
|
|
1799
|
-
$("journal-clear").addEventListener("click", () => api("DELETE", "/requests").then(renderJournal).catch(showError))
|
|
1800
|
-
|
|
1801
|
-
$("route-list").addEventListener("click", (event) => {
|
|
1802
|
-
const button = event.target.closest("[data-route]")
|
|
1803
|
-
if (!button) return
|
|
1804
|
-
state.route = button.dataset.route
|
|
1805
|
-
$("route-title").textContent = state.route
|
|
1806
|
-
const method = state.route.split(" ")[0]
|
|
1807
|
-
$("route-body").disabled = method === "GET" || method === "DELETE"
|
|
1808
|
-
})
|
|
1809
|
-
$("route-form").addEventListener("submit", async (event) => {
|
|
1810
|
-
event.preventDefault()
|
|
1811
|
-
if (!state.route) return showError(new Error("Choose a route first"))
|
|
1812
|
-
const [method, path] = [state.route.split(" ")[0], state.route.split(" ").slice(1).join(" ")]
|
|
1813
|
-
const concrete = path.replace(/:([A-Za-z0-9_]+)/g, (_, key) => prompt("Value for " + key) || "")
|
|
1814
|
-
let body
|
|
1815
|
-
if (method !== "GET" && method !== "DELETE" && $("route-body").value.trim()) {
|
|
1816
|
-
try { body = JSON.parse($("route-body").value) }
|
|
1817
|
-
catch { return showError(new Error("Body is not JSON")) }
|
|
1818
|
-
}
|
|
1819
|
-
try {
|
|
1820
|
-
const data = await api(method, concrete, body)
|
|
1821
|
-
$("route-result").textContent = JSON.stringify(data, null, 2)
|
|
1822
|
-
} catch (error) {
|
|
1823
|
-
showError(error)
|
|
1824
|
-
}
|
|
1825
|
-
})
|
|
1826
|
-
|
|
1827
|
-
function mountPanels(panels) {
|
|
1828
|
-
const nav = $("nav")
|
|
1829
|
-
const views = $("views")
|
|
1830
|
-
for (const panel of panels) {
|
|
1831
|
-
const button = document.createElement("button")
|
|
1832
|
-
button.type = "button"
|
|
1833
|
-
button.dataset.view = panel.id
|
|
1834
|
-
button.textContent = panel.title
|
|
1835
|
-
nav.append(button)
|
|
1836
|
-
const section = document.createElement("section")
|
|
1837
|
-
section.className = "view"
|
|
1838
|
-
section.id = "view-" + panel.id
|
|
1839
|
-
section.hidden = true
|
|
1840
|
-
const title = document.createElement("h2")
|
|
1841
|
-
title.textContent = panel.title
|
|
1842
|
-
const lede = document.createElement("p")
|
|
1843
|
-
lede.className = "lede"
|
|
1844
|
-
lede.textContent = panel.description || ""
|
|
1845
|
-
const body = document.createElement("div")
|
|
1846
|
-
body.className = "panel-body"
|
|
1847
|
-
body.id = "panel-" + panel.id
|
|
1848
|
-
body.innerHTML = panel.html || ""
|
|
1849
|
-
section.append(title, lede, body)
|
|
1850
|
-
views.append(section)
|
|
1851
|
-
if (panel.script) {
|
|
1852
|
-
try {
|
|
1853
|
-
const run = new Function("root", "api", panel.script)
|
|
1854
|
-
run(body, panelApi)
|
|
1855
|
-
} catch (error) {
|
|
1856
|
-
const note = document.createElement("p")
|
|
1857
|
-
note.className = "banner show"
|
|
1858
|
-
note.textContent = error instanceof Error ? error.message : String(error)
|
|
1859
|
-
body.append(note)
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
loadNamespaces()
|
|
1866
|
-
.then(() => api("GET", "/ui/manifest"))
|
|
1867
|
-
.then((manifest) => { mountPanels((manifest && manifest.panels) || []); return refresh() })
|
|
1868
|
-
.catch(showError)
|
|
1869
|
-
})()
|
|
1870
|
-
</script>
|
|
2671
|
+
${script}
|
|
1871
2672
|
</body>
|
|
1872
2673
|
</html>`;
|
|
1873
2674
|
};
|
|
@@ -1884,10 +2685,10 @@ var json2 = (body) => new Response(JSON.stringify(body), {
|
|
|
1884
2685
|
});
|
|
1885
2686
|
var adminUiRoutes = (service, ui) => {
|
|
1886
2687
|
const shell = () => renderAdminDocument(service);
|
|
1887
|
-
const
|
|
2688
|
+
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
1888
2689
|
return {
|
|
1889
|
-
"GET /ui": () => htmlResponse(
|
|
1890
|
-
"GET /ui/": () => htmlResponse(
|
|
2690
|
+
"GET /ui": () => htmlResponse(document3()),
|
|
2691
|
+
"GET /ui/": () => htmlResponse(document3()),
|
|
1891
2692
|
"GET /ui/manifest": () => json2({
|
|
1892
2693
|
service,
|
|
1893
2694
|
panels: (ui?.panels ?? []).map((panel) => ({
|
|
@@ -2375,7 +3176,7 @@ var SAMPLE = 25;
|
|
|
2375
3176
|
var WALK_LIMIT = 1e4;
|
|
2376
3177
|
var SKIP_CTORS = /* @__PURE__ */ new Set(["Hono", "HonoRequest", "Router"]);
|
|
2377
3178
|
var fail = (status, message) => ({ ok: false, status, message });
|
|
2378
|
-
var
|
|
3179
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2379
3180
|
var kindOf = (value) => {
|
|
2380
3181
|
if (value === null)
|
|
2381
3182
|
return "null";
|
|
@@ -2474,7 +3275,7 @@ var absorb = (accum, value) => {
|
|
|
2474
3275
|
const kind = kindOf(value);
|
|
2475
3276
|
accum.kinds.add(kind);
|
|
2476
3277
|
accum.present++;
|
|
2477
|
-
if (!
|
|
3278
|
+
if (!isRecord3(value))
|
|
2478
3279
|
return;
|
|
2479
3280
|
for (const [key, child] of Object.entries(value)) {
|
|
2480
3281
|
const nested = accum.nested.get(key) ?? blankAccum(key);
|
|
@@ -2499,7 +3300,7 @@ var inferFields = (records) => {
|
|
|
2499
3300
|
if (records.length === 0)
|
|
2500
3301
|
return [];
|
|
2501
3302
|
const sample = records.slice(0, SAMPLE);
|
|
2502
|
-
const objects = sample.filter((record) =>
|
|
3303
|
+
const objects = sample.filter((record) => isRecord3(record.value));
|
|
2503
3304
|
if (objects.length === 0) {
|
|
2504
3305
|
const kinds = new Set(sample.map((record) => kindOf(record.value)));
|
|
2505
3306
|
const list = [...kinds];
|
|
@@ -2686,7 +3487,7 @@ var patchStateRecord = (scope, name, id, value) => {
|
|
|
2686
3487
|
const parsed = jsonValue(value);
|
|
2687
3488
|
if (!parsed.ok)
|
|
2688
3489
|
return parsed;
|
|
2689
|
-
const next =
|
|
3490
|
+
const next = isRecord3(current.value.value) && isRecord3(parsed.value) ? { ...current.value.value, ...parsed.value } : parsed.value;
|
|
2690
3491
|
return replaceStateRecord(scope, name, id, next);
|
|
2691
3492
|
};
|
|
2692
3493
|
var deleteStateRecord = (scope, name, id) => {
|
|
@@ -2754,7 +3555,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
2754
3555
|
const scope = open(namespace);
|
|
2755
3556
|
if (!scope.ok)
|
|
2756
3557
|
return fromResult(scope, 201);
|
|
2757
|
-
if (!
|
|
3558
|
+
if (!isRecord3(body) || !("value" in body)) {
|
|
2758
3559
|
return adminError2(400, 'expected {"id?": "<id>", "value": <json>}');
|
|
2759
3560
|
}
|
|
2760
3561
|
const id = body.id;
|
|
@@ -2772,7 +3573,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
2772
3573
|
const scope = open(namespace);
|
|
2773
3574
|
if (!scope.ok)
|
|
2774
3575
|
return fromResult(scope, 200);
|
|
2775
|
-
if (!
|
|
3576
|
+
if (!isRecord3(body) || !("value" in body))
|
|
2776
3577
|
return adminError2(400, 'expected {"value": <json>}');
|
|
2777
3578
|
const replaced = replaceStateRecord(scope.value, params.collection ?? "", params.id ?? "", body.value);
|
|
2778
3579
|
if (replaced.ok)
|
|
@@ -2783,7 +3584,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
2783
3584
|
const scope = open(namespace);
|
|
2784
3585
|
if (!scope.ok)
|
|
2785
3586
|
return fromResult(scope, 200);
|
|
2786
|
-
if (!
|
|
3587
|
+
if (!isRecord3(body) || !("value" in body))
|
|
2787
3588
|
return adminError2(400, 'expected {"value": <json>}');
|
|
2788
3589
|
const patched = patchStateRecord(scope.value, params.collection ?? "", params.id ?? "", body.value);
|
|
2789
3590
|
if (patched.ok)
|
|
@@ -2803,14 +3604,14 @@ var stateAdminRoutes = (routes) => {
|
|
|
2803
3604
|
};
|
|
2804
3605
|
|
|
2805
3606
|
// ../core/dist/version.js
|
|
2806
|
-
var PACKAGE_VERSION = true ? "1.
|
|
3607
|
+
var PACKAGE_VERSION = true ? "1.5.0" : UNRELEASED_VERSION;
|
|
2807
3608
|
|
|
2808
3609
|
// ../core/dist/webhooks.js
|
|
2809
3610
|
var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
2810
3611
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
2811
|
-
var
|
|
3612
|
+
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2812
3613
|
var parseEndpoint = (value) => {
|
|
2813
|
-
if (!
|
|
3614
|
+
if (!isRecord4(value) || typeof value.url !== "string")
|
|
2814
3615
|
return "each endpoint needs a url";
|
|
2815
3616
|
try {
|
|
2816
3617
|
new URL(value.url);
|
|
@@ -2827,12 +3628,12 @@ var parseEndpoint = (value) => {
|
|
|
2827
3628
|
const events = value.events ?? value.enabledEvents;
|
|
2828
3629
|
if (Array.isArray(events))
|
|
2829
3630
|
endpoint.events = events.map(String);
|
|
2830
|
-
if (
|
|
3631
|
+
if (isRecord4(value.tags)) {
|
|
2831
3632
|
endpoint.tags = Object.fromEntries(Object.entries(value.tags).map(([k, v]) => [k, String(v)]));
|
|
2832
3633
|
}
|
|
2833
3634
|
if (typeof value.account === "string")
|
|
2834
3635
|
endpoint.tags = { ...endpoint.tags, account: value.account };
|
|
2835
|
-
if (
|
|
3636
|
+
if (isRecord4(value.headers)) {
|
|
2836
3637
|
endpoint.headers = Object.fromEntries(Object.entries(value.headers).map(([k, v]) => [k, String(v)]));
|
|
2837
3638
|
}
|
|
2838
3639
|
return endpoint;
|
|
@@ -2859,7 +3660,7 @@ var webhookAdminRoutes = (hub) => ({
|
|
|
2859
3660
|
return json4(200, { status: "ok" });
|
|
2860
3661
|
},
|
|
2861
3662
|
"POST /webhooks/faults": ({ body, namespace }) => {
|
|
2862
|
-
if (!
|
|
3663
|
+
if (!isRecord4(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
|
|
2863
3664
|
return adminError3(400, "mode must be duplicate, reorder or drop");
|
|
2864
3665
|
}
|
|
2865
3666
|
const fault = { mode: body.mode };
|
|
@@ -2875,7 +3676,7 @@ var webhookAdminRoutes = (hub) => ({
|
|
|
2875
3676
|
}))
|
|
2876
3677
|
}),
|
|
2877
3678
|
"PUT /webhook-endpoints": ({ body, namespace }) => {
|
|
2878
|
-
const list = Array.isArray(body) ? body :
|
|
3679
|
+
const list = Array.isArray(body) ? body : isRecord4(body) ? body.endpoints : void 0;
|
|
2879
3680
|
if (!Array.isArray(list))
|
|
2880
3681
|
return adminError3(400, "expected [{url, secret?, events?}]");
|
|
2881
3682
|
const parsed = [];
|
|
@@ -2943,8 +3744,8 @@ var DroppedConnectionError = class extends TypeError {
|
|
|
2943
3744
|
this.name = "TypeError";
|
|
2944
3745
|
}
|
|
2945
3746
|
};
|
|
2946
|
-
var operationMatcher = (
|
|
2947
|
-
const matchers = listOperations(
|
|
3747
|
+
var operationMatcher = (document3) => {
|
|
3748
|
+
const matchers = listOperations(document3).map((operation) => ({
|
|
2948
3749
|
operationId: operation.operationId,
|
|
2949
3750
|
method: operation.method.toUpperCase(),
|
|
2950
3751
|
pattern: new RegExp(`^${operation.path.split("/").map((segment) => segment.startsWith("{") ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/")}/?$`),
|
|
@@ -3039,10 +3840,10 @@ var createRuntime = (options) => {
|
|
|
3039
3840
|
const ensureBranch = (namespace, branch2, at) => {
|
|
3040
3841
|
if (!BRANCH_PATTERN2.test(branch2))
|
|
3041
3842
|
throw new RangeError(`branch must match ${BRANCH_PATTERN2}`);
|
|
3042
|
-
const
|
|
3843
|
+
const history2 = timeline(namespace);
|
|
3043
3844
|
if (branch2 === "main") {
|
|
3044
3845
|
if (at !== void 0) {
|
|
3045
|
-
const point =
|
|
3846
|
+
const point = history2.checkout("main", at);
|
|
3046
3847
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
3047
3848
|
captured.set(namespace, point.value.snapshot);
|
|
3048
3849
|
rng.setState(point.value.rngState);
|
|
@@ -3055,10 +3856,10 @@ var createRuntime = (options) => {
|
|
|
3055
3856
|
return namespace;
|
|
3056
3857
|
}
|
|
3057
3858
|
const storage = physicalBranch(namespace, branch2);
|
|
3058
|
-
if (!
|
|
3859
|
+
if (!history2.hasBranch(branch2)) {
|
|
3059
3860
|
if (at === void 0)
|
|
3060
|
-
|
|
3061
|
-
const point =
|
|
3861
|
+
history2.commit(capture(namespace));
|
|
3862
|
+
const point = history2.fork(branch2, at === void 0 ? {} : { from: at });
|
|
3062
3863
|
const branchRng = createRng(options.seed ?? 0);
|
|
3063
3864
|
if (point)
|
|
3064
3865
|
branchRng.setState(point.value.rngState);
|
|
@@ -3067,8 +3868,8 @@ var createRuntime = (options) => {
|
|
|
3067
3868
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
3068
3869
|
if (point)
|
|
3069
3870
|
captured.set(storage, point.value.snapshot);
|
|
3070
|
-
} else if (at !== void 0 &&
|
|
3071
|
-
const point =
|
|
3871
|
+
} else if (at !== void 0 && history2.head(branch2)?.id !== at) {
|
|
3872
|
+
const point = history2.checkout(branch2, at);
|
|
3072
3873
|
if (!instances.has(storage)) {
|
|
3073
3874
|
const branchRng = createRng(options.seed ?? 0);
|
|
3074
3875
|
branchRng.setState(point.value.rngState);
|
|
@@ -3079,7 +3880,7 @@ var createRuntime = (options) => {
|
|
|
3079
3880
|
captured.set(storage, point.value.snapshot);
|
|
3080
3881
|
} else {
|
|
3081
3882
|
if (!instances.has(storage)) {
|
|
3082
|
-
const point =
|
|
3883
|
+
const point = history2.head(branch2);
|
|
3083
3884
|
const branchRng = createRng(options.seed ?? 0);
|
|
3084
3885
|
if (point)
|
|
3085
3886
|
branchRng.setState(point.value.rngState);
|
|
@@ -3103,8 +3904,8 @@ var createRuntime = (options) => {
|
|
|
3103
3904
|
const checkout = (checkpointId, checkoutOptions = {}) => {
|
|
3104
3905
|
const namespace = checkoutOptions.namespace ?? DEFAULT_NAMESPACE;
|
|
3105
3906
|
const branchName = checkoutOptions.branch ?? "main";
|
|
3106
|
-
const
|
|
3107
|
-
const point =
|
|
3907
|
+
const history2 = timeline(namespace);
|
|
3908
|
+
const point = history2.checkout(branchName, checkpointId);
|
|
3108
3909
|
const storage = ensureBranch(namespace, branchName);
|
|
3109
3910
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
3110
3911
|
captured.set(storage, point.value.snapshot);
|
|
@@ -3154,9 +3955,9 @@ var createRuntime = (options) => {
|
|
|
3154
3955
|
instance(name);
|
|
3155
3956
|
restoreNamespace(sqlite, storageNamespace(name), from);
|
|
3156
3957
|
captured.set(name, from);
|
|
3157
|
-
const
|
|
3158
|
-
if (
|
|
3159
|
-
|
|
3958
|
+
const history2 = timelines.get(name);
|
|
3959
|
+
if (history2)
|
|
3960
|
+
history2.commit(capture(name), { branch: "main" });
|
|
3160
3961
|
else
|
|
3161
3962
|
timeline(name);
|
|
3162
3963
|
};
|
|
@@ -3385,10 +4186,10 @@ var createRuntime = (options) => {
|
|
|
3385
4186
|
},
|
|
3386
4187
|
release: (name, checkpointId) => timeline(name).release(checkpointId),
|
|
3387
4188
|
inspect: (name) => {
|
|
3388
|
-
const
|
|
4189
|
+
const history2 = timeline(name);
|
|
3389
4190
|
return {
|
|
3390
|
-
branches:
|
|
3391
|
-
checkpoints:
|
|
4191
|
+
branches: history2.branches(),
|
|
4192
|
+
checkpoints: history2.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
|
|
3392
4193
|
id,
|
|
3393
4194
|
branch: branchName,
|
|
3394
4195
|
parent,
|
|
@@ -3403,11 +4204,11 @@ var createRuntime = (options) => {
|
|
|
3403
4204
|
} : {},
|
|
3404
4205
|
routes: {
|
|
3405
4206
|
...credentialRoutes(credentials),
|
|
3406
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
3407
4207
|
...options.webhooks ? webhookAdminRoutes(options.webhooks) : {},
|
|
3408
4208
|
...options.admin?.(runtime) ?? {},
|
|
3409
|
-
//
|
|
3410
|
-
//
|
|
4209
|
+
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4210
|
+
// maps `presets`, and a service route must not replace that list with a record.
|
|
4211
|
+
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
3411
4212
|
...stateAdminRoutes({
|
|
3412
4213
|
open: stateScope,
|
|
3413
4214
|
afterWrite: (namespace) => {
|
|
@@ -3482,9 +4283,12 @@ var credentialRoutes = (registry) => ({
|
|
|
3482
4283
|
}
|
|
3483
4284
|
});
|
|
3484
4285
|
var presetRoutes = (presets, runtime) => ({
|
|
3485
|
-
"GET /faults/presets": () =>
|
|
3486
|
-
|
|
3487
|
-
|
|
4286
|
+
"GET /faults/presets": () => {
|
|
4287
|
+
const body = {
|
|
4288
|
+
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
4289
|
+
};
|
|
4290
|
+
return adminJson(200, body);
|
|
4291
|
+
},
|
|
3488
4292
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
3489
4293
|
const name = params.name;
|
|
3490
4294
|
if (!presets[name])
|
|
@@ -3495,7 +4299,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
3495
4299
|
});
|
|
3496
4300
|
|
|
3497
4301
|
// src/generated/openapi.ts
|
|
3498
|
-
var
|
|
4302
|
+
var document2 = JSON.parse(`{"openapi":"3.1.0","info":{"title":"AWS Step Functions (Mockingbird subset)","version":"2016-11-23","description":"AWS JSON contract used by the pinned SFN client."},"servers":[{"url":"https://states.us-east-1.amazonaws.com"}],"paths":{"/":{"post":{"operationId":"StepFunctionsRpc","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"X-Amz-Target","in":"header","required":true,"schema":{"type":"string","pattern":"^AWSStepFunctions\\\\\\\\.[A-Za-z]+$"}}],"requestBody":{"required":true,"content":{"application/x-amz-json-1.0":{"schema":{"type":"object","additionalProperties":true}},"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Operation response","content":{"application/x-amz-json-1.0":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"SFN exception","content":{"application/x-amz-json-1.0":{"schema":{"type":"object","additionalProperties":true}}}}}}}}}`);
|
|
3499
4303
|
var operationIds = ["StepFunctionsRpc"];
|
|
3500
4304
|
var supportedOperationIds = ["StepFunctionsRpc"];
|
|
3501
4305
|
|
|
@@ -3559,7 +4363,7 @@ var admin = (runtime) => ({
|
|
|
3559
4363
|
});
|
|
3560
4364
|
var createRuntime2 = (options = {}) => createRuntime({
|
|
3561
4365
|
name: STEP_FUNCTIONS_NAMESPACE,
|
|
3562
|
-
document,
|
|
4366
|
+
document: document2,
|
|
3563
4367
|
presets: STEP_FUNCTIONS_PRESETS,
|
|
3564
4368
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
3565
4369
|
...options.clock ? { clock: options.clock } : {},
|
|
@@ -3823,7 +4627,7 @@ var StepFunctionsAPI = class {
|
|
|
3823
4627
|
};
|
|
3824
4628
|
|
|
3825
4629
|
export {
|
|
3826
|
-
document,
|
|
4630
|
+
document2 as document,
|
|
3827
4631
|
operationIds,
|
|
3828
4632
|
supportedOperationIds,
|
|
3829
4633
|
STEP_FUNCTIONS_PRESETS,
|
|
@@ -3832,4 +4636,4 @@ export {
|
|
|
3832
4636
|
accessKeyCredential,
|
|
3833
4637
|
StepFunctionsAPI
|
|
3834
4638
|
};
|
|
3835
|
-
//# sourceMappingURL=chunk-
|
|
4639
|
+
//# sourceMappingURL=chunk-FWF7NYHK.js.map
|