@hozu/devtools 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/overlay/app.js +29 -15
- package/dist/overlay/app.js.map +1 -1
- package/dist/overlay/effects.d.ts +46 -12
- package/dist/overlay/effects.d.ts.map +1 -1
- package/dist/overlay/effects.js +762 -191
- package/dist/overlay/effects.js.map +1 -1
- package/dist/overlay/style.d.ts +1 -1
- package/dist/overlay/style.d.ts.map +1 -1
- package/dist/overlay/style.js +110 -12
- package/dist/overlay/style.js.map +1 -1
- package/dist/overlay/trace.d.ts +22 -0
- package/dist/overlay/trace.d.ts.map +1 -0
- package/dist/overlay/trace.js +94 -0
- package/dist/overlay/trace.js.map +1 -0
- package/package.json +2 -2
package/dist/overlay/effects.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
|
-
import { h } from './dom.js';
|
|
10
|
-
export const effects = (path) => fetch(`/_hozu/dev/effects?path=${encodeURIComponent(path)}`)
|
|
1
|
+
import { h, read, write } from './dom.js';
|
|
2
|
+
import { curl, traced } from './trace.js';
|
|
3
|
+
const getJson = (path) => fetch(path)
|
|
11
4
|
.then((r) => (r.ok ? r.json() : null))
|
|
12
5
|
.catch(() => null);
|
|
6
|
+
export const effects = (path) => getJson(`/_hozu/dev/effects?path=${encodeURIComponent(path)}`);
|
|
13
7
|
const payloadOf = (doc) => {
|
|
14
8
|
try {
|
|
15
9
|
return JSON.parse(doc.getElementById('hozu-payload')?.textContent ?? 'null');
|
|
@@ -18,32 +12,23 @@ const payloadOf = (doc) => {
|
|
|
18
12
|
return null;
|
|
19
13
|
}
|
|
20
14
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const key = data
|
|
25
|
-
.map(([k]) => k)
|
|
26
|
-
.find((k) => k.startsWith(effect.ref) && '{['.includes(k[effect.ref.length] ?? ''));
|
|
27
|
-
if (key) {
|
|
28
|
-
try {
|
|
29
|
-
return JSON.parse(key.slice(effect.ref.length));
|
|
30
|
-
}
|
|
31
|
-
catch { }
|
|
32
|
-
}
|
|
33
|
-
return example(effect.input);
|
|
34
|
-
}
|
|
35
|
-
function example(schema) {
|
|
15
|
+
export function example(schema) {
|
|
16
|
+
if (!schema)
|
|
17
|
+
return null;
|
|
36
18
|
if (schema.default !== undefined)
|
|
37
19
|
return schema.default;
|
|
38
20
|
if (Array.isArray(schema.enum))
|
|
39
21
|
return schema.enum[0];
|
|
40
22
|
if ('const' in schema)
|
|
41
23
|
return schema.const;
|
|
24
|
+
const alternatives = schema.anyOf ?? schema.oneOf;
|
|
25
|
+
if (Array.isArray(alternatives))
|
|
26
|
+
return example(alternatives.find((a) => a.type !== 'null') ?? alternatives[0]);
|
|
42
27
|
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
43
28
|
if (type === 'object')
|
|
44
29
|
return Object.fromEntries(Object.entries((schema.properties ?? {})).map(([k, v]) => [k, example(v)]));
|
|
45
30
|
if (type === 'array')
|
|
46
|
-
return [];
|
|
31
|
+
return schema.items ? [example(schema.items)] : [];
|
|
47
32
|
if (type === 'number' || type === 'integer')
|
|
48
33
|
return 0;
|
|
49
34
|
if (type === 'boolean')
|
|
@@ -52,105 +37,296 @@ function example(schema) {
|
|
|
52
37
|
return null;
|
|
53
38
|
return '';
|
|
54
39
|
}
|
|
40
|
+
/** The input the page uses for a query (from its payload), else an example from the schema. */
|
|
41
|
+
function startInput(effect, doc) {
|
|
42
|
+
const data = (payloadOf(doc)?.data ?? []);
|
|
43
|
+
const key = data
|
|
44
|
+
.map(([k]) => k)
|
|
45
|
+
.find((k) => k.startsWith(effect.ref) && '{['.includes(k[effect.ref.length] ?? ''));
|
|
46
|
+
if (key)
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(key.slice(effect.ref.length));
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
return example(effect.input);
|
|
52
|
+
}
|
|
55
53
|
const scalar = (s) => ['string', 'number', 'integer', 'boolean'].includes(s.type) || Array.isArray(s.enum);
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
const flat = (schema) => schema.type === 'object' && Object.values((schema.properties ?? {})).every((p) => scalar(p));
|
|
55
|
+
const hookOf = (win) => win.__hozu;
|
|
56
|
+
/** Runs an effect the way the page would: through the page's client when it has one, else the app's endpoints. */
|
|
57
|
+
async function call(effect, input, win) {
|
|
58
58
|
const started = performance.now();
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
const hook = hookOf(win);
|
|
60
|
+
const where = effect.runs === 'server' ? 'server' : 'browser';
|
|
61
|
+
const base = {
|
|
62
|
+
ref: effect.ref,
|
|
63
|
+
kind: effect.kind,
|
|
64
|
+
runs: effect.runs,
|
|
65
|
+
input,
|
|
66
|
+
where,
|
|
67
|
+
status: null,
|
|
68
|
+
headers: [],
|
|
69
|
+
};
|
|
70
|
+
const done = (result, tags, refreshed) => ({
|
|
71
|
+
...base,
|
|
72
|
+
ok: result.ok,
|
|
73
|
+
error: result.ok ? null : result.error,
|
|
74
|
+
value: result.ok ? result.value : result.data,
|
|
75
|
+
ms: Math.round(performance.now() - started),
|
|
76
|
+
tags,
|
|
77
|
+
refreshed,
|
|
78
|
+
});
|
|
79
|
+
try {
|
|
80
|
+
if (hook?.invoke && hook.query) {
|
|
81
|
+
if (effect.kind === 'mutation') {
|
|
82
|
+
const { result, tags } = await hook.invoke(effect.ref, input);
|
|
83
|
+
return done(result, tags, true);
|
|
84
|
+
}
|
|
85
|
+
return done(await hook.query(effect.ref, input), [], false);
|
|
86
|
+
}
|
|
87
|
+
if (effect.runs === 'browser')
|
|
88
|
+
return done({
|
|
66
89
|
ok: false,
|
|
67
90
|
error: 'Unexpected',
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
91
|
+
data: { message: 'This page has no client: browser-run effects need one' },
|
|
92
|
+
}, [], false);
|
|
93
|
+
const query = effect.kind === 'query';
|
|
94
|
+
const response = await win.fetch(query ? '/_hozu/query' : '/_hozu/effect', {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: { 'content-type': 'application/json' },
|
|
97
|
+
body: JSON.stringify(query ? { query: effect.ref, input } : { effect: effect.ref, input, keys: [] }),
|
|
98
|
+
});
|
|
99
|
+
if (!response.ok)
|
|
100
|
+
return done({ ok: false, error: 'Unexpected', data: { message: await response.text() } }, [], false);
|
|
101
|
+
const body = (await response.json());
|
|
102
|
+
return {
|
|
103
|
+
...done((query ? body : body.result), body.tags ?? [], false),
|
|
104
|
+
where: 'server',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return done({ ok: false, error: 'Unexpected', data: { message: String(error) } }, [], false);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const parsed = (text) => {
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(text);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return text;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const paramsOf = (path) => [...path.matchAll(/:([A-Za-z_]\w*)/g)].map((m) => m[1]);
|
|
120
|
+
/** Sends a request to a declared endpoint, as a client of the app would (cookies included). */
|
|
121
|
+
async function send(endpoint, request, win) {
|
|
122
|
+
const started = performance.now();
|
|
123
|
+
let path = endpoint.path;
|
|
124
|
+
for (const [name, value] of Object.entries(request.params))
|
|
125
|
+
path = path.replace(`:${name}`, encodeURIComponent(value));
|
|
126
|
+
const url = new URL(path, win.location.origin);
|
|
127
|
+
let body = null;
|
|
128
|
+
const headers = new Headers(request.headers.filter(([k]) => k.trim()));
|
|
129
|
+
if (endpoint.method === 'GET') {
|
|
74
130
|
try {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
},
|
|
80
|
-
signal: new AbortController().signal,
|
|
81
|
-
env: payload?.env ?? {},
|
|
82
|
-
});
|
|
83
|
-
return { ok: true, error: null, value, ms: elapsed(), invalidated: [], where: 'browser' };
|
|
131
|
+
const query = JSON.parse(request.body || '{}');
|
|
132
|
+
for (const [k, v] of Object.entries(query))
|
|
133
|
+
for (const one of Array.isArray(v) ? v : [v])
|
|
134
|
+
url.searchParams.append(k, String(one));
|
|
84
135
|
}
|
|
85
|
-
catch
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
136
|
+
catch { }
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
body = request.body;
|
|
140
|
+
if (!headers.has('content-type') && !endpoint.raw)
|
|
141
|
+
headers.set('content-type', 'application/json');
|
|
142
|
+
}
|
|
143
|
+
const base = {
|
|
144
|
+
ref: endpoint.ref,
|
|
145
|
+
kind: 'endpoint',
|
|
146
|
+
runs: 'server',
|
|
147
|
+
input: parsed(request.body),
|
|
148
|
+
where: 'server',
|
|
149
|
+
tags: [],
|
|
150
|
+
refreshed: false,
|
|
151
|
+
url: url.href,
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
const response = await win.fetch(url, { method: endpoint.method, headers, body, redirect: 'manual' });
|
|
155
|
+
if (response.type === 'opaqueredirect')
|
|
156
|
+
return {
|
|
157
|
+
...base,
|
|
158
|
+
ok: true,
|
|
159
|
+
error: null,
|
|
160
|
+
value: 'A redirect (the browser does not show where to; open the endpoint URL to follow it)',
|
|
161
|
+
ms: Math.round(performance.now() - started),
|
|
162
|
+
status: 0,
|
|
163
|
+
headers: [],
|
|
164
|
+
};
|
|
165
|
+
const text = await response.text();
|
|
166
|
+
let value = text;
|
|
167
|
+
try {
|
|
168
|
+
value = JSON.parse(text);
|
|
104
169
|
}
|
|
170
|
+
catch { }
|
|
171
|
+
return {
|
|
172
|
+
...base,
|
|
173
|
+
ok: response.ok,
|
|
174
|
+
error: response.ok ? null : `HTTP ${response.status}`,
|
|
175
|
+
value,
|
|
176
|
+
ms: Math.round(performance.now() - started),
|
|
177
|
+
status: response.status,
|
|
178
|
+
headers: [...response.headers],
|
|
179
|
+
};
|
|
105
180
|
}
|
|
106
|
-
|
|
107
|
-
const response = await fetch(query ? '/_hozu/query' : '/_hozu/effect', {
|
|
108
|
-
method: 'POST',
|
|
109
|
-
headers: { 'content-type': 'application/json' },
|
|
110
|
-
body: JSON.stringify(query ? { query: effect.ref, input } : { effect: effect.ref, input, keys: [] }),
|
|
111
|
-
});
|
|
112
|
-
if (!response.ok)
|
|
181
|
+
catch (error) {
|
|
113
182
|
return {
|
|
183
|
+
...base,
|
|
114
184
|
ok: false,
|
|
115
185
|
error: 'Unexpected',
|
|
116
|
-
value:
|
|
117
|
-
ms:
|
|
118
|
-
|
|
119
|
-
|
|
186
|
+
value: String(error),
|
|
187
|
+
ms: Math.round(performance.now() - started),
|
|
188
|
+
status: null,
|
|
189
|
+
headers: [],
|
|
120
190
|
};
|
|
121
|
-
|
|
122
|
-
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
export const hozuCall = (r, scope) => `npx hozu call ${r.ref} --input '${JSON.stringify(r.input).replace(/'/g, "'\\''")}'${scope === 'user' ? ` --session '<the session JSON>'` : ''}${r.kind === 'mutation' ? ' --write' : ''}`;
|
|
194
|
+
const KEY = `hozu-devtools-api:${location.host}`;
|
|
195
|
+
/** A record saved by this or an older DevTools, with every field present; null when it is not one. */
|
|
196
|
+
const normal = (r) => {
|
|
197
|
+
const o = r;
|
|
198
|
+
if (!o || typeof o.ref !== 'string' || typeof o.id !== 'number')
|
|
199
|
+
return null;
|
|
123
200
|
return {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
201
|
+
kind: 'query',
|
|
202
|
+
runs: 'server',
|
|
203
|
+
input: null,
|
|
204
|
+
ok: false,
|
|
205
|
+
error: null,
|
|
206
|
+
value: null,
|
|
207
|
+
ms: 0,
|
|
129
208
|
where: 'server',
|
|
209
|
+
tags: [],
|
|
210
|
+
refreshed: false,
|
|
211
|
+
status: null,
|
|
212
|
+
headers: [],
|
|
213
|
+
requests: [],
|
|
214
|
+
at: o.id,
|
|
215
|
+
...o,
|
|
130
216
|
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
217
|
+
};
|
|
218
|
+
export function drawer(host) {
|
|
219
|
+
const saved = read(KEY, {});
|
|
220
|
+
const ui = {
|
|
221
|
+
tab: saved.tab ?? 'query',
|
|
222
|
+
view: saved.view ?? 'table',
|
|
223
|
+
height: saved.height ?? 340,
|
|
224
|
+
open: saved.open ?? false,
|
|
225
|
+
history: (Array.isArray(saved.history) ? saved.history : []).flatMap((r) => normal(r) ?? []),
|
|
226
|
+
};
|
|
227
|
+
if (!['query', 'mutation', 'endpoint', 'history'].includes(ui.tab))
|
|
228
|
+
ui.tab = 'query';
|
|
229
|
+
const persist = () => write(KEY, { ...ui, history: ui.history.slice(0, 50) });
|
|
230
|
+
let list = null;
|
|
231
|
+
let endpoints = [];
|
|
232
|
+
let session = null;
|
|
233
|
+
let sessionOpen = false;
|
|
234
|
+
let sessionError = null;
|
|
235
|
+
let selected = ui.history[0]?.id ?? null;
|
|
236
|
+
let confirming = null;
|
|
237
|
+
const inputs = new Map();
|
|
238
|
+
const asJson = new Set();
|
|
239
|
+
const requests = new Map();
|
|
240
|
+
const running = new Set();
|
|
241
|
+
const el = h('section', { class: 'api', 'aria-label': 'API', hidden: true });
|
|
242
|
+
let shown = false;
|
|
243
|
+
const height = () => (shown ? Math.min(ui.height, innerHeight - 120) : 0);
|
|
244
|
+
const resize = () => {
|
|
245
|
+
el.style.height = `${height()}px`;
|
|
246
|
+
host.resized();
|
|
247
|
+
};
|
|
248
|
+
const where = (runs) => host.plain() ? { server: 'server', either: 'server, then browser', browser: 'browser' }[runs] : runs;
|
|
249
|
+
const remember = (outcome, sent) => {
|
|
250
|
+
const record = { ...outcome, requests: sent, id: Date.now(), at: Date.now() };
|
|
251
|
+
ui.history.unshift(record);
|
|
252
|
+
ui.history = ui.history.slice(0, 50);
|
|
253
|
+
selected = record.id;
|
|
254
|
+
persist();
|
|
255
|
+
};
|
|
256
|
+
async function run(effect) {
|
|
257
|
+
const input = inputs.get(effect.ref) ?? startInput(effect, host.win().document);
|
|
258
|
+
running.add(effect.ref);
|
|
259
|
+
confirming = null;
|
|
260
|
+
draw();
|
|
261
|
+
const { value, requests: sent } = await traced(host.win(), () => call(effect, input, host.win()));
|
|
262
|
+
running.delete(effect.ref);
|
|
263
|
+
remember(value, sent);
|
|
264
|
+
draw();
|
|
265
|
+
}
|
|
266
|
+
async function sendTo(endpoint) {
|
|
267
|
+
running.add(endpoint.ref);
|
|
268
|
+
draw();
|
|
269
|
+
const { value, requests: sent } = await traced(host.win(), () => send(endpoint, requestOf(endpoint), host.win()));
|
|
270
|
+
running.delete(endpoint.ref);
|
|
271
|
+
const { url: _, ...outcome } = value;
|
|
272
|
+
remember(outcome, sent.filter((r) => r.side === 'server'));
|
|
273
|
+
draw();
|
|
274
|
+
}
|
|
275
|
+
const requestOf = (endpoint) => {
|
|
276
|
+
let r = requests.get(endpoint.ref);
|
|
277
|
+
if (!r) {
|
|
278
|
+
r = {
|
|
279
|
+
params: Object.fromEntries(paramsOf(endpoint.path).map((p) => [p, ''])),
|
|
280
|
+
body: endpoint.raw ? '' : JSON.stringify(example(endpoint.input) ?? {}, null, 2),
|
|
281
|
+
headers: [['', '']],
|
|
282
|
+
};
|
|
283
|
+
requests.set(endpoint.ref, r);
|
|
284
|
+
}
|
|
285
|
+
return r;
|
|
286
|
+
};
|
|
287
|
+
const invalidOf = (last) => last && !last.ok && last.error === 'Invalid'
|
|
288
|
+
? (last.value?.fields ?? {})
|
|
289
|
+
: {};
|
|
290
|
+
const jsonEditor = (label, value, onValid, rows = 5) => {
|
|
291
|
+
const area = h('textarea', { class: 'api-json-input', rows: String(rows), spellcheck: 'false', 'aria-label': label }, [value]);
|
|
292
|
+
area.addEventListener('input', () => {
|
|
293
|
+
try {
|
|
294
|
+
JSON.parse(area.value);
|
|
295
|
+
area.removeAttribute('aria-invalid');
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
area.setAttribute('aria-invalid', 'true');
|
|
299
|
+
}
|
|
300
|
+
onValid(area.value);
|
|
301
|
+
});
|
|
302
|
+
return area;
|
|
303
|
+
};
|
|
304
|
+
const fieldsOf = (effect, last) => {
|
|
305
|
+
const schema = effect.input;
|
|
306
|
+
const props = (schema.properties ?? {});
|
|
307
|
+
const value = (inputs.get(effect.ref) ?? startInput(effect, host.win().document));
|
|
308
|
+
const invalid = invalidOf(last);
|
|
309
|
+
if (!flat(schema) || asJson.has(effect.ref))
|
|
310
|
+
return h('div', { class: 'api-fields' }, [
|
|
311
|
+
jsonEditor(`${effect.ref} input (JSON)`, JSON.stringify(value, null, 2), (text) => {
|
|
312
|
+
try {
|
|
313
|
+
inputs.set(effect.ref, JSON.parse(text));
|
|
314
|
+
}
|
|
315
|
+
catch { }
|
|
316
|
+
}),
|
|
317
|
+
Object.keys(invalid).length
|
|
318
|
+
? h('em', { class: 'api-invalid' }, Object.entries(invalid)
|
|
319
|
+
.filter(([, m]) => m)
|
|
320
|
+
.map(([k, m]) => `${k}: ${m}. `))
|
|
321
|
+
: null,
|
|
322
|
+
]);
|
|
148
323
|
if (!Object.keys(props).length)
|
|
149
|
-
return h('
|
|
150
|
-
|
|
151
|
-
|
|
324
|
+
return h('span', { class: 'api-none' }, ['no input']);
|
|
325
|
+
const current = { ...value };
|
|
326
|
+
return h('div', { class: 'api-fields' }, Object.entries(props).map(([name, s]) => {
|
|
327
|
+
const id = `api-${effect.ref}-${name}`;
|
|
152
328
|
const control = Array.isArray(s.enum)
|
|
153
|
-
? h('select', { id }, s.enum.map((o) => h('option', { value: String(o), selected:
|
|
329
|
+
? h('select', { id }, s.enum.map((o) => h('option', { value: String(o), selected: current[name] === o }, [String(o)])))
|
|
154
330
|
: h('input', {
|
|
155
331
|
id,
|
|
156
332
|
type: s.type === 'boolean'
|
|
@@ -158,103 +334,498 @@ function fields(effect, start, onChange) {
|
|
|
158
334
|
: s.type === 'number' || s.type === 'integer'
|
|
159
335
|
? 'number'
|
|
160
336
|
: 'text',
|
|
337
|
+
placeholder: s.type === 'string' ? name : '',
|
|
161
338
|
...(s.type === 'boolean'
|
|
162
|
-
? { checked:
|
|
163
|
-
: { value: String(
|
|
339
|
+
? { checked: current[name] === true }
|
|
340
|
+
: { value: String(current[name] ?? '') }),
|
|
341
|
+
'aria-invalid': invalid[name] ? 'true' : undefined,
|
|
164
342
|
});
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
343
|
+
const update = () => {
|
|
344
|
+
const c = control;
|
|
345
|
+
current[name] =
|
|
168
346
|
s.type === 'boolean'
|
|
169
|
-
?
|
|
347
|
+
? c.checked
|
|
170
348
|
: s.type === 'number' || s.type === 'integer'
|
|
171
|
-
? Number(
|
|
172
|
-
:
|
|
173
|
-
|
|
349
|
+
? Number(c.value)
|
|
350
|
+
: c.value;
|
|
351
|
+
inputs.set(effect.ref, { ...current });
|
|
352
|
+
};
|
|
353
|
+
control.addEventListener('input', update);
|
|
354
|
+
control.addEventListener('change', update);
|
|
355
|
+
control.addEventListener('keydown', (e) => {
|
|
356
|
+
if (e.key === 'Enter') {
|
|
357
|
+
e.preventDefault();
|
|
358
|
+
update();
|
|
359
|
+
ask(effect);
|
|
360
|
+
}
|
|
174
361
|
});
|
|
175
|
-
return h('label', { class: 'field', for: id }, [
|
|
362
|
+
return h('label', { class: 'api-field', for: id }, [
|
|
363
|
+
h('span', {}, [name]),
|
|
364
|
+
control,
|
|
365
|
+
invalid[name] ? h('em', { class: 'api-invalid' }, [invalid[name]]) : null,
|
|
366
|
+
]);
|
|
176
367
|
}));
|
|
177
|
-
}
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
onChange(JSON.parse(area.value));
|
|
184
|
-
area.removeAttribute('aria-invalid');
|
|
185
|
-
}
|
|
186
|
-
catch {
|
|
187
|
-
area.setAttribute('aria-invalid', 'true');
|
|
368
|
+
};
|
|
369
|
+
const ask = (effect) => {
|
|
370
|
+
if (effect.kind === 'mutation' && confirming !== effect.ref) {
|
|
371
|
+
confirming = effect.ref;
|
|
372
|
+
return draw();
|
|
188
373
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
374
|
+
void run(effect);
|
|
375
|
+
};
|
|
376
|
+
const row = (effect) => {
|
|
377
|
+
const last = ui.history.find((r) => r.ref === effect.ref);
|
|
378
|
+
const busy = running.has(effect.ref);
|
|
379
|
+
const asking = confirming === effect.ref;
|
|
380
|
+
const canForm = flat(effect.input) && Object.keys(effect.input.properties ?? {}).length > 0;
|
|
381
|
+
return h('div', { class: `api-row${last && last.id === selected ? ' on' : ''}`, 'data-effect': effect.ref }, [
|
|
382
|
+
h('span', { class: `api-dot ${last ? (last.ok ? 'ok' : 'bad') : ''}`, 'aria-hidden': 'true' }),
|
|
383
|
+
h('div', { class: 'api-name' }, [
|
|
384
|
+
h('b', {}, [host.plain() ? effect.label : effect.ref]),
|
|
385
|
+
h('span', { class: `api-kind ${effect.kind === 'query' ? 'read' : 'write'}` }, [
|
|
386
|
+
effect.kind === 'query' ? 'read' : 'write',
|
|
387
|
+
]),
|
|
388
|
+
h('span', { class: `api-runs ${effect.runs}`, title: `runs: ${effect.runs}` }, [
|
|
389
|
+
where(effect.runs),
|
|
390
|
+
]),
|
|
391
|
+
effect.kind === 'query' ? h('span', { class: 'api-tag' }, [effect.freshness]) : null,
|
|
392
|
+
effect.implemented
|
|
393
|
+
? h('span', { class: 'api-where', title: 'Where it is implemented' }, [
|
|
394
|
+
`${effect.implemented.file}:${effect.implemented.line}`,
|
|
395
|
+
])
|
|
396
|
+
: null,
|
|
397
|
+
canForm
|
|
398
|
+
? h('button', {
|
|
399
|
+
type: 'button',
|
|
400
|
+
class: 'api-switch',
|
|
401
|
+
title: 'JSON sends any value, also one the schema rejects',
|
|
402
|
+
onclick: () => {
|
|
403
|
+
if (asJson.has(effect.ref))
|
|
404
|
+
asJson.delete(effect.ref);
|
|
405
|
+
else
|
|
406
|
+
asJson.add(effect.ref);
|
|
407
|
+
draw();
|
|
408
|
+
},
|
|
409
|
+
}, [asJson.has(effect.ref) ? 'Form' : 'JSON'])
|
|
410
|
+
: null,
|
|
411
|
+
]),
|
|
412
|
+
fieldsOf(effect, last),
|
|
413
|
+
asking
|
|
414
|
+
? h('div', { class: 'api-confirm', role: 'alert' }, [
|
|
415
|
+
h('span', {}, ['Writes your development data.']),
|
|
416
|
+
h('button', {
|
|
417
|
+
type: 'button',
|
|
418
|
+
class: 'api-ghost',
|
|
419
|
+
onclick: () => {
|
|
420
|
+
confirming = null;
|
|
421
|
+
draw();
|
|
422
|
+
},
|
|
423
|
+
}, ['Cancel']),
|
|
424
|
+
h('button', { type: 'button', class: 'api-run warn', onclick: () => void run(effect) }, [
|
|
425
|
+
'Run',
|
|
426
|
+
]),
|
|
427
|
+
])
|
|
428
|
+
: h('button', {
|
|
429
|
+
type: 'button',
|
|
430
|
+
class: `api-run${effect.kind === 'mutation' ? ' warn' : ''}`,
|
|
431
|
+
disabled: busy,
|
|
432
|
+
onclick: () => ask(effect),
|
|
433
|
+
}, [busy ? 'Running…' : 'Run ▶']),
|
|
434
|
+
]);
|
|
435
|
+
};
|
|
436
|
+
const endpointRow = (endpoint) => {
|
|
437
|
+
const r = requestOf(endpoint);
|
|
438
|
+
const last = ui.history.find((x) => x.ref === endpoint.ref);
|
|
439
|
+
const busy = running.has(endpoint.ref);
|
|
440
|
+
const headerRows = r.headers.map((pair, i) => {
|
|
441
|
+
const key = h('input', {
|
|
442
|
+
value: pair[0],
|
|
443
|
+
placeholder: 'Header',
|
|
444
|
+
'aria-label': 'Header name',
|
|
445
|
+
});
|
|
446
|
+
const val = h('input', {
|
|
447
|
+
value: pair[1],
|
|
448
|
+
placeholder: 'Value',
|
|
449
|
+
'aria-label': 'Header value',
|
|
450
|
+
});
|
|
451
|
+
key.addEventListener('input', () => {
|
|
452
|
+
r.headers[i] = [key.value, val.value];
|
|
453
|
+
});
|
|
454
|
+
val.addEventListener('input', () => {
|
|
455
|
+
r.headers[i] = [key.value, val.value];
|
|
456
|
+
});
|
|
457
|
+
return h('div', { class: 'api-header' }, [key, val]);
|
|
458
|
+
});
|
|
459
|
+
return h('div', {
|
|
460
|
+
class: `api-row endpoint${last && last.id === selected ? ' on' : ''}`,
|
|
461
|
+
'data-endpoint': endpoint.ref,
|
|
462
|
+
}, [
|
|
463
|
+
h('span', { class: `api-dot ${last ? (last.ok ? 'ok' : 'bad') : ''}`, 'aria-hidden': 'true' }),
|
|
464
|
+
h('div', { class: 'api-name' }, [
|
|
465
|
+
h('span', { class: `api-method ${endpoint.method.toLowerCase()}` }, [endpoint.method]),
|
|
466
|
+
h('b', {}, [endpoint.path]),
|
|
467
|
+
h('span', { class: 'api-tag' }, [host.plain() ? endpoint.ref.split('.').pop() : endpoint.ref]),
|
|
468
|
+
]),
|
|
469
|
+
h('div', { class: 'api-fields' }, [
|
|
470
|
+
...Object.keys(r.params).map((p) => {
|
|
471
|
+
const input = h('input', {
|
|
472
|
+
value: r.params[p],
|
|
473
|
+
placeholder: p,
|
|
474
|
+
'aria-label': `Path :${p}`,
|
|
475
|
+
});
|
|
476
|
+
input.addEventListener('input', () => {
|
|
477
|
+
r.params[p] = input.value;
|
|
478
|
+
});
|
|
479
|
+
return h('label', { class: 'api-field' }, [h('span', {}, [`:${p}`]), input]);
|
|
480
|
+
}),
|
|
481
|
+
h('div', { class: 'api-field wide' }, [
|
|
482
|
+
h('span', {}, [
|
|
483
|
+
endpoint.method === 'GET' ? 'Query (JSON)' : endpoint.raw ? 'Body' : 'Body (JSON)',
|
|
484
|
+
]),
|
|
485
|
+
jsonEditor(`${endpoint.ref} body`, r.body, (text) => {
|
|
486
|
+
r.body = text;
|
|
487
|
+
}),
|
|
488
|
+
]),
|
|
489
|
+
h('div', { class: 'api-field wide' }, [
|
|
490
|
+
h('span', {}, ['Headers']),
|
|
491
|
+
...headerRows,
|
|
492
|
+
h('button', {
|
|
493
|
+
type: 'button',
|
|
494
|
+
class: 'api-ghost',
|
|
495
|
+
onclick: () => {
|
|
496
|
+
r.headers.push(['', '']);
|
|
497
|
+
draw();
|
|
498
|
+
},
|
|
499
|
+
}, ['+ Header']),
|
|
204
500
|
]),
|
|
205
|
-
])
|
|
206
|
-
:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
501
|
+
]),
|
|
502
|
+
h('button', { type: 'button', class: 'api-run', disabled: busy, onclick: () => void sendTo(endpoint) }, [busy ? 'Sending…' : 'Send ▶']),
|
|
503
|
+
]);
|
|
504
|
+
};
|
|
505
|
+
const cell = (v) => v === null || v === undefined ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
506
|
+
function table(value) {
|
|
507
|
+
if (Array.isArray(value) &&
|
|
508
|
+
value.length &&
|
|
509
|
+
value.every((r) => r && typeof r === 'object' && !Array.isArray(r))) {
|
|
510
|
+
const columns = [...new Set(value.flatMap((r) => Object.keys(r)))].slice(0, 8);
|
|
511
|
+
return h('table', { class: 'api-table' }, [
|
|
512
|
+
h('thead', {}, [
|
|
513
|
+
h('tr', {}, columns.map((c) => h('th', {}, [c]))),
|
|
514
|
+
]),
|
|
515
|
+
h('tbody', {}, value.map((r) => h('tr', {}, columns.map((c) => h('td', {}, [cell(r[c])]))))),
|
|
516
|
+
]);
|
|
517
|
+
}
|
|
518
|
+
if (value && typeof value === 'object' && !Array.isArray(value))
|
|
519
|
+
return h('table', { class: 'api-table kv' }, [
|
|
520
|
+
h('tbody', {}, Object.entries(value).map(([k, v]) => h('tr', {}, [h('th', {}, [k]), h('td', {}, [cell(v)])]))),
|
|
521
|
+
]);
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
const copyButton = (label, text, disabled = false, title = '') => {
|
|
525
|
+
const button = h('button', {
|
|
214
526
|
type: 'button',
|
|
527
|
+
class: 'api-ghost',
|
|
528
|
+
disabled,
|
|
529
|
+
title,
|
|
215
530
|
onclick: async () => {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
run.setAttribute('disabled', '');
|
|
219
|
-
out.replaceChildren(h('div', { class: 'plain' }, ['Running…']));
|
|
220
|
-
out.replaceChildren(outcome(await call(effect, input, host.doc)));
|
|
221
|
-
run.removeAttribute('disabled');
|
|
531
|
+
await navigator.clipboard.writeText(text()).catch(() => { });
|
|
532
|
+
button.textContent = 'Copied';
|
|
222
533
|
},
|
|
223
|
-
}, [
|
|
224
|
-
return
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
534
|
+
}, [label]);
|
|
535
|
+
return button;
|
|
536
|
+
};
|
|
537
|
+
const sentList = (r) => r.requests.length
|
|
538
|
+
? h('div', { class: 'api-sent' }, [
|
|
539
|
+
h('div', { class: 'api-label' }, ['Requests it sent']),
|
|
540
|
+
...r.requests.map((t) => h('details', { class: 'api-request' }, [
|
|
541
|
+
h('summary', {}, [
|
|
542
|
+
h('span', { class: `api-method ${t.method.toLowerCase()}` }, [t.method]),
|
|
543
|
+
h('code', {}, [t.url]),
|
|
544
|
+
h('span', { class: `api-code ${t.status && t.status < 400 ? 'ok' : 'bad'}` }, [
|
|
545
|
+
t.error ? 'failed' : String(t.status),
|
|
546
|
+
]),
|
|
547
|
+
h('span', { class: 'api-tag' }, [
|
|
548
|
+
`${t.ms} ms · ${t.side === 'server' ? 'from the server' : 'from this browser'}`,
|
|
549
|
+
]),
|
|
550
|
+
]),
|
|
551
|
+
h('div', { class: 'api-request-body' }, [
|
|
552
|
+
t.error ? h('em', { class: 'api-invalid' }, [t.error]) : null,
|
|
553
|
+
h('b', {}, ['Request headers']),
|
|
554
|
+
h('pre', {}, [t.requestHeaders.map(([k, v]) => `${k}: ${v}`).join('\n') || '(none)']),
|
|
555
|
+
t.requestBody ? h('b', {}, ['Request body']) : null,
|
|
556
|
+
t.requestBody ? h('pre', {}, [t.requestBody]) : null,
|
|
557
|
+
h('b', {}, ['Response headers']),
|
|
558
|
+
h('pre', {}, [t.responseHeaders.map(([k, v]) => `${k}: ${v}`).join('\n') || '(none)']),
|
|
559
|
+
t.responseBody ? h('b', {}, ['Response body']) : null,
|
|
560
|
+
t.responseBody ? h('pre', {}, [t.responseBody]) : null,
|
|
561
|
+
copyButton('Copy as curl', () => curl(t)),
|
|
562
|
+
]),
|
|
563
|
+
])),
|
|
564
|
+
])
|
|
565
|
+
: null;
|
|
566
|
+
function result(r) {
|
|
567
|
+
if (!r)
|
|
568
|
+
return h('div', { class: 'api-empty' }, ['Run something to see its answer here.']);
|
|
569
|
+
const effect = list?.find((e) => e.ref === r.ref);
|
|
570
|
+
const status = r.kind === 'endpoint'
|
|
571
|
+
? `${r.status === 0 ? 'Redirect' : (r.status ?? 'No answer')}${r.ok ? '' : ` · ${r.error}`}`
|
|
572
|
+
: r.ok
|
|
573
|
+
? 'OK'
|
|
574
|
+
: r.error === 'Unexpected'
|
|
575
|
+
? 'Unexpected error'
|
|
576
|
+
: `Declared error: ${r.error}`;
|
|
577
|
+
const grid = typeof r.value === 'object' ? table(r.value) : null;
|
|
578
|
+
return h('div', { class: 'api-result' }, [
|
|
579
|
+
h('div', { class: `api-status ${r.ok ? 'ok' : 'bad'}`, role: 'status' }, [
|
|
580
|
+
h('b', {}, [status]),
|
|
581
|
+
` · ${r.ms} ms · ${r.kind === 'endpoint' ? 'HTTP' : r.where === 'server' ? 'through the server' : 'in this browser'} · ${host.plain() ? (effect?.label ?? r.ref) : r.ref}`,
|
|
229
582
|
]),
|
|
230
|
-
|
|
231
|
-
?
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
583
|
+
r.tags.length
|
|
584
|
+
? h('div', { class: 'api-note' }, [
|
|
585
|
+
`Invalidated ${r.tags.join(', ')}`,
|
|
586
|
+
r.refreshed
|
|
587
|
+
? ' · the page re-read it'
|
|
588
|
+
: h('button', { type: 'button', class: 'api-link', onclick: () => host.win().location.reload() }, [' · reload the page to see it']),
|
|
589
|
+
])
|
|
590
|
+
: null,
|
|
591
|
+
h('div', { class: 'api-tools' }, [
|
|
592
|
+
h('div', { class: 'api-seg', role: 'group', 'aria-label': 'View' }, [
|
|
593
|
+
...['table', 'json'].map((v) => h('button', {
|
|
594
|
+
type: 'button',
|
|
595
|
+
'aria-pressed': ui.view === v ? 'true' : 'false',
|
|
596
|
+
disabled: v === 'table' && !grid,
|
|
597
|
+
onclick: () => {
|
|
598
|
+
ui.view = v;
|
|
599
|
+
persist();
|
|
600
|
+
draw();
|
|
601
|
+
},
|
|
602
|
+
}, [v === 'table' ? 'Table' : 'JSON'])),
|
|
236
603
|
]),
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
604
|
+
r.kind === 'endpoint'
|
|
605
|
+
? null
|
|
606
|
+
: copyButton('Copy as hozu call', () => hozuCall(r, effect?.scope ?? 'public'), r.runs === 'browser', r.runs === 'browser'
|
|
607
|
+
? 'hozu call runs server and either effects; this one needs the browser'
|
|
608
|
+
: ''),
|
|
609
|
+
]),
|
|
610
|
+
ui.view === 'table' && grid
|
|
611
|
+
? h('div', { class: 'api-scroll' }, [grid])
|
|
612
|
+
: h('pre', { class: 'api-scroll api-pre' }, [
|
|
613
|
+
typeof r.value === 'string' ? r.value : (JSON.stringify(r.value, null, 2) ?? 'undefined'),
|
|
614
|
+
]),
|
|
615
|
+
r.kind === 'endpoint' && r.headers.length
|
|
616
|
+
? h('details', { class: 'api-request' }, [
|
|
617
|
+
h('summary', {}, ['Response headers']),
|
|
618
|
+
h('pre', {}, [r.headers.map(([k, v]) => `${k}: ${v}`).join('\n')]),
|
|
619
|
+
])
|
|
620
|
+
: null,
|
|
621
|
+
sentList(r),
|
|
242
622
|
]);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
623
|
+
}
|
|
624
|
+
function historyList() {
|
|
625
|
+
if (!ui.history.length)
|
|
626
|
+
return [h('div', { class: 'api-empty' }, ['No calls yet.'])];
|
|
627
|
+
return ui.history.map((r) => h('div', { class: `api-row history${r.id === selected ? ' on' : ''}` }, [
|
|
628
|
+
h('span', { class: `api-dot ${r.ok ? 'ok' : 'bad'}`, 'aria-hidden': 'true' }),
|
|
629
|
+
h('button', {
|
|
630
|
+
type: 'button',
|
|
631
|
+
class: 'api-open',
|
|
632
|
+
onclick: () => {
|
|
633
|
+
selected = r.id;
|
|
634
|
+
draw();
|
|
635
|
+
},
|
|
636
|
+
}, [
|
|
637
|
+
h('b', {}, [r.ref]),
|
|
638
|
+
h('span', { class: 'api-tag' }, [new Date(r.at).toLocaleTimeString()]),
|
|
639
|
+
h('span', { class: 'api-tag' }, [`${r.ms} ms`]),
|
|
640
|
+
h('code', {}, [JSON.stringify(r.input)]),
|
|
641
|
+
]),
|
|
642
|
+
r.kind === 'endpoint'
|
|
643
|
+
? null
|
|
644
|
+
: h('button', {
|
|
645
|
+
type: 'button',
|
|
646
|
+
class: 'api-ghost',
|
|
647
|
+
onclick: () => {
|
|
648
|
+
const effect = list?.find((e) => e.ref === r.ref);
|
|
649
|
+
if (!effect)
|
|
650
|
+
return;
|
|
651
|
+
inputs.set(r.ref, r.input);
|
|
652
|
+
ui.tab = effect.kind;
|
|
653
|
+
ask(effect);
|
|
654
|
+
},
|
|
655
|
+
}, ['Run again']),
|
|
656
|
+
]));
|
|
657
|
+
}
|
|
658
|
+
function sessionBox() {
|
|
659
|
+
if (!session?.declared || !sessionOpen)
|
|
660
|
+
return null;
|
|
661
|
+
let text = JSON.stringify(session.current ?? example(session.schema), null, 2);
|
|
662
|
+
const apply = async (value) => {
|
|
663
|
+
const response = await fetch('/_hozu/dev/session', {
|
|
664
|
+
method: 'POST',
|
|
665
|
+
headers: { 'content-type': 'application/json' },
|
|
666
|
+
body: JSON.stringify({ session: value }),
|
|
667
|
+
});
|
|
668
|
+
if (!response.ok) {
|
|
669
|
+
sessionError =
|
|
670
|
+
(await response.json().catch(() => ({}))).error ?? response.statusText;
|
|
671
|
+
return draw();
|
|
672
|
+
}
|
|
673
|
+
sessionError = null;
|
|
674
|
+
sessionOpen = false;
|
|
675
|
+
host.win().location.reload();
|
|
676
|
+
};
|
|
677
|
+
return h('div', { class: 'api-row session' }, [
|
|
678
|
+
h('span', {}),
|
|
679
|
+
h('div', { class: 'api-name' }, [
|
|
680
|
+
h('b', {}, ['Act as']),
|
|
681
|
+
h('span', { class: 'api-tag' }, ['development only: sets this browser’s session cookie']),
|
|
682
|
+
]),
|
|
683
|
+
h('div', { class: 'api-fields' }, [
|
|
684
|
+
jsonEditor('Session (JSON)', text, (t) => {
|
|
685
|
+
text = t;
|
|
686
|
+
}),
|
|
687
|
+
sessionError ? h('em', { class: 'api-invalid' }, [sessionError]) : null,
|
|
688
|
+
]),
|
|
689
|
+
h('div', { class: 'api-confirm' }, [
|
|
690
|
+
h('button', { type: 'button', class: 'api-ghost', onclick: () => void apply(null) }, ['Sign out']),
|
|
691
|
+
h('button', {
|
|
692
|
+
type: 'button',
|
|
693
|
+
class: 'api-run',
|
|
694
|
+
onclick: () => {
|
|
695
|
+
try {
|
|
696
|
+
void apply(JSON.parse(text));
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
sessionError = 'The session is not valid JSON';
|
|
700
|
+
draw();
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
}, ['Act as']),
|
|
704
|
+
]),
|
|
705
|
+
]);
|
|
706
|
+
}
|
|
707
|
+
function draw() {
|
|
708
|
+
try {
|
|
709
|
+
paint();
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
el.replaceChildren(h('div', { class: 'api-empty', role: 'alert' }, [
|
|
713
|
+
`The API drawer could not draw: ${error instanceof Error ? error.message : String(error)}. `,
|
|
714
|
+
h('button', {
|
|
715
|
+
type: 'button',
|
|
716
|
+
class: 'api-link',
|
|
717
|
+
onclick: () => {
|
|
718
|
+
ui.history = [];
|
|
719
|
+
selected = null;
|
|
720
|
+
persist();
|
|
721
|
+
draw();
|
|
722
|
+
},
|
|
723
|
+
}, ['Clear its saved state']),
|
|
724
|
+
]));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function paint() {
|
|
728
|
+
const queries = (list ?? []).filter((e) => e.kind === 'query');
|
|
729
|
+
const mutations = (list ?? []).filter((e) => e.kind === 'mutation');
|
|
730
|
+
const tab = (id, label, count) => h('button', {
|
|
731
|
+
type: 'button',
|
|
732
|
+
role: 'tab',
|
|
733
|
+
'aria-selected': ui.tab === id ? 'true' : 'false',
|
|
734
|
+
onclick: () => {
|
|
735
|
+
ui.tab = id;
|
|
736
|
+
persist();
|
|
737
|
+
draw();
|
|
738
|
+
},
|
|
739
|
+
}, [label, h('span', { class: 'count' }, [String(count)])]);
|
|
740
|
+
const grip = h('div', { class: 'api-grip', title: 'Drag to resize', 'aria-hidden': 'true' });
|
|
741
|
+
grip.addEventListener('pointerdown', (event) => {
|
|
742
|
+
event.preventDefault();
|
|
743
|
+
grip.setPointerCapture(event.pointerId);
|
|
744
|
+
const move = (e) => {
|
|
745
|
+
ui.height = Math.max(160, Math.min(innerHeight - 120, innerHeight - e.clientY));
|
|
746
|
+
resize();
|
|
747
|
+
};
|
|
748
|
+
const up = () => {
|
|
749
|
+
grip.removeEventListener('pointermove', move);
|
|
750
|
+
grip.removeEventListener('pointerup', up);
|
|
751
|
+
persist();
|
|
752
|
+
};
|
|
753
|
+
grip.addEventListener('pointermove', move);
|
|
754
|
+
grip.addEventListener('pointerup', up);
|
|
755
|
+
});
|
|
756
|
+
const rows = ui.tab === 'history'
|
|
757
|
+
? historyList()
|
|
758
|
+
: ui.tab === 'endpoint'
|
|
759
|
+
? endpoints.map(endpointRow)
|
|
760
|
+
: list === null
|
|
761
|
+
? [h('div', { class: 'api-empty' }, ['This page is not a Hozu page.'])]
|
|
762
|
+
: (ui.tab === 'query' ? queries : mutations).map(row);
|
|
763
|
+
const who = session?.declared &&
|
|
764
|
+
h('button', {
|
|
765
|
+
type: 'button',
|
|
766
|
+
class: `api-session${sessionOpen ? ' on' : ''}`,
|
|
767
|
+
title: 'The session this browser sends; act as someone else',
|
|
768
|
+
onclick: () => {
|
|
769
|
+
sessionOpen = !sessionOpen;
|
|
770
|
+
draw();
|
|
771
|
+
},
|
|
772
|
+
}, [session.current === null ? 'Signed out' : `As ${JSON.stringify(session.current)}`]);
|
|
773
|
+
el.replaceChildren(grip, h('div', { class: 'api-head' }, [
|
|
774
|
+
h('b', { class: 'api-title' }, ['API']),
|
|
775
|
+
h('div', { class: 'api-tabs', role: 'tablist' }, [
|
|
776
|
+
tab('query', host.plain() ? 'Reads' : 'Queries', queries.length),
|
|
777
|
+
tab('mutation', host.plain() ? 'Changes' : 'Mutations', mutations.length),
|
|
778
|
+
endpoints.length ? tab('endpoint', 'Endpoints', endpoints.length) : null,
|
|
779
|
+
tab('history', 'History', ui.history.length),
|
|
780
|
+
]),
|
|
781
|
+
h('span', { class: 'api-path' }, [host.path()]),
|
|
782
|
+
who || null,
|
|
783
|
+
h('button', {
|
|
784
|
+
type: 'button',
|
|
785
|
+
class: 'api-close',
|
|
786
|
+
'aria-label': 'Close the API drawer',
|
|
787
|
+
onclick: () => show(false),
|
|
788
|
+
}, ['×']),
|
|
789
|
+
]), h('div', { class: 'api-body' }, [
|
|
790
|
+
h('div', { class: 'api-list' }, [
|
|
791
|
+
sessionBox(),
|
|
792
|
+
...(rows.length ? rows : [h('div', { class: 'api-empty' }, ['None on this page.'])]),
|
|
793
|
+
]),
|
|
794
|
+
result(ui.history.find((r) => r.id === selected)),
|
|
258
795
|
]));
|
|
796
|
+
}
|
|
797
|
+
async function reload() {
|
|
798
|
+
if (!shown)
|
|
799
|
+
return;
|
|
800
|
+
[list, endpoints, session] = await Promise.all([
|
|
801
|
+
effects(host.path()),
|
|
802
|
+
getJson('/_hozu/dev/endpoints').then((e) => e ?? []),
|
|
803
|
+
getJson('/_hozu/dev/session'),
|
|
804
|
+
]);
|
|
805
|
+
inputs.clear();
|
|
806
|
+
if (ui.tab === 'endpoint' && !endpoints.length)
|
|
807
|
+
ui.tab = 'query';
|
|
808
|
+
draw();
|
|
809
|
+
}
|
|
810
|
+
function show(open) {
|
|
811
|
+
shown = open;
|
|
812
|
+
ui.open = open;
|
|
813
|
+
persist();
|
|
814
|
+
el.hidden = !open;
|
|
815
|
+
resize();
|
|
816
|
+
if (open)
|
|
817
|
+
void reload();
|
|
818
|
+
else
|
|
819
|
+
host.closed();
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
el,
|
|
823
|
+
show,
|
|
824
|
+
reload,
|
|
825
|
+
toggle: () => show(!shown),
|
|
826
|
+
isOpen: () => shown,
|
|
827
|
+
height,
|
|
828
|
+
restore: () => ui.open && show(true),
|
|
829
|
+
};
|
|
259
830
|
}
|
|
260
831
|
//# sourceMappingURL=effects.js.map
|