@webda/debug 4.0.0-beta.5 → 4.0.0-beta.7

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.
@@ -1,264 +0,0 @@
1
- import { h } from "preact";
2
- import { useEffect, useMemo, useState } from "preact/hooks";
3
- import htm from "htm";
4
-
5
- const html = htm.bind(h);
6
-
7
- function statusClass(code) {
8
- if (!code) return "status-pending";
9
- if (code < 300) return "status-2xx";
10
- if (code < 400) return "status-3xx";
11
- if (code < 500) return "status-4xx";
12
- return "status-5xx";
13
- }
14
-
15
- function methodBadge(method) {
16
- if (!method) return "";
17
- const m = method.toUpperCase();
18
- return html`<span class="badge method-${m.toLowerCase()}">${m}</span>`;
19
- }
20
-
21
- function formatTime(ts) {
22
- if (!ts) return "-";
23
- const d = new Date(ts);
24
- return d.toLocaleTimeString();
25
- }
26
-
27
- function formatBytes(n) {
28
- if (n == null) return "-";
29
- if (n < 1024) return `${n} B`;
30
- if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
31
- return `${(n / (1024 * 1024)).toFixed(2)} MB`;
32
- }
33
-
34
- /**
35
- * Render a request/response body in the most useful format we can:
36
- * - empty -> grey placeholder
37
- * - text -> preformatted block (pretty-printed JSON when possible)
38
- * - text-truncated -> same plus a [truncated] notice
39
- * - binary -> "Binary, N bytes (preview: 0xHEX)"
40
- */
41
- function BodyView({ body }) {
42
- if (!body) return html`<div style="color:var(--text-muted);font-style:italic">(not captured)</div>`;
43
- if (body.kind === "empty") {
44
- return html`<div style="color:var(--text-muted);font-style:italic">(empty)</div>`;
45
- }
46
- if (body.kind === "binary") {
47
- return html`
48
- <div style="color:var(--text-muted);font-family:monospace;font-size:13px">
49
- Binary, ${formatBytes(body.size)} (preview: 0x${body.preview || ""})
50
- </div>
51
- `;
52
- }
53
- // text or text-truncated
54
- let display = body.content || "";
55
- // Try to pretty-print JSON
56
- try {
57
- const parsed = JSON.parse(display);
58
- display = JSON.stringify(parsed, null, 2);
59
- } catch {
60
- // not JSON — leave as-is
61
- }
62
- return html`
63
- <pre class="mono" style="background:var(--bg-secondary);padding:0.75rem;border-radius:4px;overflow:auto;max-height:400px;font-size:12px;white-space:pre-wrap;word-break:break-all">${display}</pre>
64
- ${body.kind === "text-truncated"
65
- ? html`<div style="color:var(--text-muted);font-size:12px;margin-top:0.25rem">[truncated — total ${formatBytes(body.size)}]</div>`
66
- : ""}
67
- `;
68
- }
69
-
70
- /**
71
- * Render a flat list of headers as a small two-column table.
72
- */
73
- function HeadersView({ headers }) {
74
- const keys = headers ? Object.keys(headers) : [];
75
- if (keys.length === 0) {
76
- return html`<div style="color:var(--text-muted);font-style:italic">(none)</div>`;
77
- }
78
- return html`
79
- <table style="width:100%;font-size:12px">
80
- <tbody>
81
- ${keys.sort().map(
82
- k => html`
83
- <tr key=${k}>
84
- <td class="mono" style="color:var(--text-muted);padding:2px 8px 2px 0;vertical-align:top;white-space:nowrap">${k}</td>
85
- <td class="mono" style="word-break:break-all">${headers[k]}</td>
86
- </tr>
87
- `
88
- )}
89
- </tbody>
90
- </table>
91
- `;
92
- }
93
-
94
- /**
95
- * Detail panel for a single request: status line, headers, bodies, error.
96
- * Loads the full entry from /api/requests/:id when `id` changes.
97
- */
98
- function RequestDetail({ id, onClose }) {
99
- const [entry, setEntry] = useState(null);
100
- const [error, setError] = useState(null);
101
-
102
- useEffect(() => {
103
- if (!id) {
104
- setEntry(null);
105
- return;
106
- }
107
- let cancelled = false;
108
- setError(null);
109
- fetch(`/api/requests/${encodeURIComponent(id)}`)
110
- .then(r => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
111
- .then(data => {
112
- if (!cancelled) setEntry(data);
113
- })
114
- .catch(err => {
115
- if (!cancelled) setError(err.message);
116
- });
117
- return () => {
118
- cancelled = true;
119
- };
120
- }, [id]);
121
-
122
- if (!id) return null;
123
- if (error) {
124
- return html`
125
- <div style="padding:1rem;border:1px solid var(--border);border-radius:4px;margin-top:1rem;background:var(--bg-secondary)">
126
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
127
- <strong>Failed to load request detail</strong>
128
- <button onClick=${onClose} style="background:transparent;border:1px solid var(--border);color:var(--text-primary);padding:2px 8px;border-radius:4px;cursor:pointer">Close</button>
129
- </div>
130
- <div style="color:var(--text-muted)">${error}</div>
131
- </div>
132
- `;
133
- }
134
- if (!entry) {
135
- return html`
136
- <div style="padding:1rem;color:var(--text-muted)">Loading request detail...</div>
137
- `;
138
- }
139
- return html`
140
- <div style="padding:1rem;border:1px solid var(--border);border-radius:4px;margin-top:1rem;background:var(--bg-secondary)">
141
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem">
142
- <div>
143
- ${methodBadge(entry.method)}
144
- <span class="mono" style="margin-left:0.5rem">${entry.url}</span>
145
- </div>
146
- <button onClick=${onClose} style="background:transparent;border:1px solid var(--border);color:var(--text-primary);padding:2px 8px;border-radius:4px;cursor:pointer">Close</button>
147
- </div>
148
- <div style="display:flex;gap:1.5rem;font-size:12px;color:var(--text-muted);margin-bottom:1rem">
149
- <div>Time: ${formatTime(entry.timestamp)}</div>
150
- <div>Status: <span class="mono ${statusClass(entry.statusCode)}" style="font-weight:600">${entry.statusCode != null ? entry.statusCode : "pending"}</span></div>
151
- <div>Duration: ${entry.duration != null ? `${entry.duration}ms` : "-"}</div>
152
- </div>
153
-
154
- ${entry.error
155
- ? html`
156
- <div style="margin-bottom:1rem;padding:0.5rem 0.75rem;background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.3);border-radius:4px">
157
- <div style="color:#ef4444;font-weight:600;margin-bottom:0.25rem">Error</div>
158
- <div class="mono" style="font-size:12px">${entry.error.message}</div>
159
- ${entry.error.stack
160
- ? html`<pre class="mono" style="font-size:11px;color:var(--text-muted);margin-top:0.5rem;white-space:pre-wrap">${entry.error.stack}</pre>`
161
- : ""}
162
- </div>
163
- `
164
- : ""}
165
-
166
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
167
- <div>
168
- <h4 style="margin:0 0 0.5rem 0;font-size:13px">Request Headers</h4>
169
- <${HeadersView} headers=${entry.requestHeaders} />
170
- <h4 style="margin:1rem 0 0.5rem 0;font-size:13px">Request Body</h4>
171
- <${BodyView} body=${entry.requestBody} />
172
- </div>
173
- <div>
174
- <h4 style="margin:0 0 0.5rem 0;font-size:13px">Response Headers</h4>
175
- <${HeadersView} headers=${entry.responseHeaders} />
176
- <h4 style="margin:1rem 0 0.5rem 0;font-size:13px">Response Body</h4>
177
- <${BodyView} body=${entry.responseBody} />
178
- </div>
179
- </div>
180
- </div>
181
- `;
182
- }
183
-
184
- export function RequestsPanel({ data, wsEvents }) {
185
- const historical = data || [];
186
- const [selectedId, setSelectedId] = useState(null);
187
-
188
- // Merge websocket events with historical data
189
- // wsEvents are newest-first; historical may be oldest-first or newest-first
190
- const entries = useMemo(() => {
191
- // Build a map of all requests by ID
192
- const map = new Map();
193
-
194
- // Add historical entries
195
- for (const entry of historical) {
196
- if (entry.id) map.set(entry.id, { ...entry });
197
- }
198
-
199
- // Apply websocket events (they update existing or add new)
200
- for (const evt of [...wsEvents].reverse()) {
201
- if (!evt.id) continue;
202
- const existing = map.get(evt.id) || {};
203
-
204
- if (evt.type === "request") {
205
- map.set(evt.id, { ...existing, id: evt.id, method: evt.method, url: evt.url, timestamp: evt.timestamp });
206
- } else if (evt.type === "result") {
207
- map.set(evt.id, { ...existing, id: evt.id, statusCode: evt.statusCode, duration: evt.duration });
208
- } else if (evt.type === "404") {
209
- map.set(evt.id, { ...existing, id: evt.id, method: evt.method, url: evt.url, statusCode: 404 });
210
- }
211
- }
212
-
213
- // Sort by timestamp, newest first
214
- return Array.from(map.values()).sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
215
- }, [historical, wsEvents]);
216
-
217
- return html`
218
- <div>
219
- <div style="margin-bottom: 0.75rem; color: var(--text-muted); font-size: 0.8125rem;">
220
- ${entries.length} request${entries.length !== 1 ? "s" : ""} recorded${selectedId ? " — click a row to inspect" : ""}
221
- </div>
222
- <div class="table-container">
223
- <table>
224
- <thead>
225
- <tr>
226
- <th>Time</th>
227
- <th>Method</th>
228
- <th>URL</th>
229
- <th>Status</th>
230
- <th>Duration</th>
231
- </tr>
232
- </thead>
233
- <tbody>
234
- ${entries.map(
235
- (r) => html`
236
- <tr
237
- key=${r.id}
238
- onClick=${() => setSelectedId(r.id === selectedId ? null : r.id)}
239
- style="cursor:pointer;${r.id === selectedId ? "background:var(--bg-tertiary)" : ""}"
240
- >
241
- <td style="white-space: nowrap;">${formatTime(r.timestamp)}</td>
242
- <td>${methodBadge(r.method)}</td>
243
- <td class="mono" style="max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${r.url || "-"}</td>
244
- <td>
245
- ${r.statusCode != null
246
- ? html`<span class="mono ${statusClass(r.statusCode)}" style="font-weight: 600;">${r.statusCode}</span>`
247
- : html`<span class="status-pending">pending...</span>`}
248
- </td>
249
- <td class="mono" style="color: var(--text-muted);">
250
- ${r.duration != null ? `${r.duration}ms` : "-"}
251
- </td>
252
- </tr>
253
- `
254
- )}
255
- ${entries.length === 0 && html`
256
- <tr><td colspan="5" style="color: var(--text-muted); text-align: center; padding: 2rem;">No requests yet. Requests will appear here in real-time.</td></tr>
257
- `}
258
- </tbody>
259
- </table>
260
- </div>
261
- ${selectedId ? html`<${RequestDetail} id=${selectedId} onClose=${() => setSelectedId(null)} />` : ""}
262
- </div>
263
- `;
264
- }
@@ -1,268 +0,0 @@
1
- import { h } from "preact";
2
- import { useState } from "preact/hooks";
3
- import htm from "htm";
4
-
5
- const html = htm.bind(h);
6
-
7
- /**
8
- * Resolve a $ref in a schema against a definitions map.
9
- * Merges the referenced schema with any sibling properties (description, etc).
10
- */
11
- function resolveRef(prop, definitions) {
12
- if (!prop) return prop;
13
- if (prop.$ref) {
14
- const refName = prop.$ref.replace("#/definitions/", "");
15
- const resolved = definitions?.[refName] || definitions?.[decodeURIComponent(refName)];
16
- if (resolved) {
17
- // Merge: sibling properties (like description) override the ref
18
- const siblings = Object.fromEntries(Object.entries(prop).filter(([k]) => k !== "$ref"));
19
- return { ...resolved, ...siblings, definitions: { ...resolved.definitions, ...definitions } };
20
- }
21
- }
22
- return { ...prop, definitions: { ...prop.definitions, ...definitions } };
23
- }
24
-
25
- /**
26
- * Collapsible section wrapper.
27
- */
28
- function Collapsible({ title, badge, defaultOpen, children }) {
29
- const [open, setOpen] = useState(defaultOpen ?? true);
30
- return html`
31
- <div class="sf-collapsible">
32
- <div class="sf-collapsible-header" onClick=${() => setOpen(!open)}>
33
- <span class="sf-chevron ${open ? "sf-open" : ""}">\u25B6</span>
34
- <span class="sf-collapsible-title">${title}</span>
35
- ${badge && html`<span class="sf-badge">${badge}</span>`}
36
- </div>
37
- ${open && html`<div class="sf-collapsible-body">${children}</div>`}
38
- </div>
39
- `;
40
- }
41
-
42
- /**
43
- * Render a single field based on its schema type.
44
- */
45
- function SchemaField({ name, prop, value, onChangeValue, readOnly, definitions, path }) {
46
- const resolved = resolveRef(prop, definitions);
47
- if (!resolved) return null;
48
- const defs = resolved.definitions || definitions;
49
-
50
- // Nested object with properties → recursive form
51
- if (resolved.type === "object" && resolved.properties) {
52
- const objValue = value || {};
53
- return html`
54
- <${Collapsible} title=${name} badge="object" defaultOpen=${Object.keys(objValue).length > 0}>
55
- <${SchemaFields}
56
- schema=${resolved}
57
- definitions=${defs}
58
- values=${objValue}
59
- onChange=${v => onChangeValue(v)}
60
- readOnly=${readOnly}
61
- path=${path}
62
- />
63
- <//>
64
- `;
65
- }
66
-
67
- // Array field
68
- if (resolved.type === "array") {
69
- return html`<${ArrayField}
70
- name=${name} prop=${resolved} value=${value} onChangeValue=${onChangeValue}
71
- readOnly=${readOnly} definitions=${defs} path=${path}
72
- />`;
73
- }
74
-
75
- // Enum → select
76
- if (resolved.enum) {
77
- return html`
78
- <select class="sf-input" value=${value ?? ""} disabled=${readOnly}
79
- onChange=${e => onChangeValue(e.target.value || undefined)}>
80
- <option value="">Select...</option>
81
- ${resolved.enum.map(v => html`<option key=${v} value=${v}>${v}</option>`)}
82
- </select>
83
- `;
84
- }
85
-
86
- // Boolean → toggle-style select
87
- if (resolved.type === "boolean") {
88
- return html`
89
- <select class="sf-input" value=${value == null ? "" : String(value)} disabled=${readOnly}
90
- onChange=${e => onChangeValue(e.target.value === "" ? undefined : e.target.value === "true")}>
91
- <option value="">-</option>
92
- <option value="true">true</option>
93
- <option value="false">false</option>
94
- </select>
95
- `;
96
- }
97
-
98
- // Number / integer
99
- if (resolved.type === "number" || resolved.type === "integer") {
100
- return html`
101
- <input class="sf-input" type="number" value=${value ?? ""}
102
- step=${resolved.type === "integer" ? "1" : "any"}
103
- min=${resolved.minimum} max=${resolved.maximum} readOnly=${readOnly}
104
- onInput=${e => {
105
- const v = e.target.value;
106
- onChangeValue(v === "" ? undefined : resolved.type === "integer" ? parseInt(v) : parseFloat(v));
107
- }}
108
- placeholder=${resolved.default != null ? `Default: ${resolved.default}` : name} />
109
- `;
110
- }
111
-
112
- // Object without properties (map) or unknown → JSON textarea
113
- if (resolved.type === "object") {
114
- return html`
115
- <textarea class="sf-input sf-textarea" rows="3"
116
- value=${typeof value === "object" && value !== null ? JSON.stringify(value, null, 2) : value ?? ""}
117
- readOnly=${readOnly}
118
- onInput=${e => { try { onChangeValue(JSON.parse(e.target.value)); } catch {} }}
119
- placeholder="JSON object" />
120
- `;
121
- }
122
-
123
- // String (default)
124
- const inputType = resolved.format === "email" ? "email"
125
- : resolved.format === "uri" || resolved.format === "url" ? "url"
126
- : resolved.format === "password" ? "password"
127
- : "text";
128
-
129
- return html`
130
- <input class="sf-input" type=${inputType}
131
- value=${value ?? ""} readOnly=${readOnly}
132
- onInput=${e => onChangeValue(e.target.value || undefined)}
133
- placeholder=${resolved.default != null ? `Default: ${resolved.default}` : (resolved.pattern ? `Pattern: ${resolved.pattern}` : name)}
134
- minlength=${resolved.minLength} maxlength=${resolved.maxLength} />
135
- `;
136
- }
137
-
138
- /**
139
- * Render an array field with add/remove controls.
140
- */
141
- function ArrayField({ name, prop, value, onChangeValue, readOnly, definitions, path }) {
142
- const items = Array.isArray(value) ? value : [];
143
- const itemSchema = resolveRef(prop.items, definitions) || { type: "string" };
144
- const isSimple = itemSchema.type === "string" || itemSchema.type === "number" || itemSchema.type === "integer";
145
-
146
- return html`
147
- <${Collapsible} title=${name} badge=${`array[${items.length}]`} defaultOpen=${items.length > 0}>
148
- <div class="sf-array">
149
- ${items.map((item, i) => html`
150
- <div class="sf-array-item" key=${i}>
151
- <div class="sf-array-item-content">
152
- ${isSimple
153
- ? html`<input class="sf-input" type=${itemSchema.type === "string" ? "text" : "number"}
154
- value=${item ?? ""} readOnly=${readOnly}
155
- onInput=${e => {
156
- const v = e.target.value;
157
- const newItems = [...items];
158
- newItems[i] = itemSchema.type === "string" ? v : (itemSchema.type === "integer" ? parseInt(v) : parseFloat(v));
159
- onChangeValue(newItems);
160
- }}
161
- placeholder=${`Item ${i + 1}`} />`
162
- : itemSchema.type === "object" && itemSchema.properties
163
- ? html`<${SchemaFields}
164
- schema=${itemSchema} definitions=${definitions}
165
- values=${item || {}}
166
- onChange=${v => { const newItems = [...items]; newItems[i] = v; onChangeValue(newItems); }}
167
- readOnly=${readOnly} path=${[...path, i]}
168
- />`
169
- : html`<textarea class="sf-input sf-textarea" rows="2"
170
- value=${typeof item === "object" ? JSON.stringify(item, null, 2) : String(item ?? "")}
171
- readOnly=${readOnly}
172
- onInput=${e => { try { const newItems = [...items]; newItems[i] = JSON.parse(e.target.value); onChangeValue(newItems); } catch {} }}
173
- placeholder="JSON" />`
174
- }
175
- </div>
176
- ${!readOnly && html`
177
- <button class="sf-btn sf-btn-remove" onClick=${() => { const newItems = items.filter((_, j) => j !== i); onChangeValue(newItems.length ? newItems : undefined); }}
178
- title="Remove item">\u00D7</button>
179
- `}
180
- </div>
181
- `)}
182
- ${!readOnly && html`
183
- <button class="sf-btn sf-btn-add" onClick=${() => {
184
- const empty = isSimple ? (itemSchema.type === "string" ? "" : 0) : {};
185
- onChangeValue([...items, empty]);
186
- }}>+ Add item</button>
187
- `}
188
- </div>
189
- <//>
190
- `;
191
- }
192
-
193
- /**
194
- * Render all fields for a schema's properties.
195
- */
196
- function SchemaFields({ schema, definitions, values, onChange, readOnly, path }) {
197
- const defs = { ...definitions, ...schema.definitions };
198
- const required = new Set(schema.required || []);
199
- const props = schema.properties || {};
200
-
201
- return html`
202
- <div class="sf-fields">
203
- ${Object.entries(props).map(([name, prop]) => {
204
- const resolved = resolveRef(prop, defs);
205
- if (!resolved) return null;
206
- const isReq = required.has(name);
207
- const hasValue = values[name] !== undefined && values[name] !== null && values[name] !== "";
208
- const fieldPath = [...(path || []), name];
209
-
210
- return html`
211
- <div class="sf-field ${hasValue ? "sf-has-value" : ""}" key=${name}>
212
- <div class="sf-label-row">
213
- <label class="sf-label">
214
- ${name}
215
- ${isReq && html`<span class="sf-required">*</span>`}
216
- </label>
217
- <span class="sf-type">${resolved.type || "ref"}${resolved.format ? `:${resolved.format}` : ""}</span>
218
- </div>
219
- ${resolved.description && html`<div class="sf-description">${resolved.description}</div>`}
220
- <${SchemaField}
221
- name=${name} prop=${prop} value=${values[name]} definitions=${defs}
222
- readOnly=${readOnly} path=${fieldPath}
223
- onChangeValue=${v => {
224
- const next = { ...values };
225
- if (v === undefined || v === null || v === "") delete next[name];
226
- else next[name] = v;
227
- onChange(next);
228
- }}
229
- />
230
- ${resolved.default !== undefined && !hasValue && html`
231
- <div class="sf-default">Default: <code>${JSON.stringify(resolved.default)}</code></div>
232
- `}
233
- </div>
234
- `;
235
- })}
236
- </div>
237
- `;
238
- }
239
-
240
- /**
241
- * Top-level SchemaForm component.
242
- *
243
- * @param {object} props
244
- * @param {object} props.schema - JSON Schema object
245
- * @param {object} props.values - current form values
246
- * @param {function} props.onChange - called with updated values
247
- * @param {boolean} props.readOnly - disable all inputs
248
- * @param {object} props.definitions - external definitions map for $ref resolution
249
- */
250
- export function SchemaForm({ schema, values, onChange, readOnly, definitions }) {
251
- if (!schema?.properties) {
252
- return html`<div class="sf-empty">No fields defined in schema</div>`;
253
- }
254
-
255
- const allDefs = { ...definitions, ...schema.definitions };
256
-
257
- return html`
258
- <div class="sf-root">
259
- ${schema.title && html`<div class="sf-title">${schema.title}</div>`}
260
- ${schema.description && html`<div class="sf-schema-desc">${schema.description}</div>`}
261
- <${SchemaFields}
262
- schema=${schema} definitions=${allDefs}
263
- values=${values || {}} onChange=${onChange}
264
- readOnly=${readOnly} path=${[]}
265
- />
266
- </div>
267
- `;
268
- }