@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.
- package/README.md +5 -6
- package/lib/debugservice.service.d.ts +134 -23
- package/lib/debugservice.service.js +417 -73
- package/lib/debugservice.service.js.map +1 -1
- package/lib/introspection.d.ts +17 -0
- package/lib/introspection.js +52 -6
- package/lib/introspection.js.map +1 -1
- package/lib/security.d.ts +155 -0
- package/lib/security.js +217 -0
- package/lib/security.js.map +1 -0
- package/lib/tui/client.d.ts +4 -1
- package/lib/tui/client.js +10 -3
- package/lib/tui/client.js.map +1 -1
- package/lib/tui/tui.d.ts +2 -1
- package/lib/tui/tui.js +6 -3
- package/lib/tui/tui.js.map +1 -1
- package/package.json +13 -8
- package/webda.module.json +129 -0
- package/webui/assets/favicon-CVPPk1M-.svg +1 -0
- package/webui/assets/index-BHmmE2oC.css +1 -0
- package/webui/assets/index-RYFIlyiZ.js +51 -0
- package/webui/index.html +13 -20
- package/webui/app.js +0 -177
- package/webui/components/logs.js +0 -128
- package/webui/components/model-graph.js +0 -292
- package/webui/components/models.js +0 -213
- package/webui/components/operations.js +0 -315
- package/webui/components/requests.js +0 -264
- package/webui/components/schema-form.js +0 -268
- package/webui/components/services.js +0 -248
- package/webui/logo.svg +0 -1
- package/webui/styles.css +0 -685
- package/webui/vendor/htm.js +0 -1
- package/webui/vendor/preact-hooks.js +0 -2
- package/webui/vendor/preact.js +0 -2
- package/webui/webda.png +0 -0
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { h } from "preact";
|
|
2
|
-
import { useState } from "preact/hooks";
|
|
3
|
-
import htm from "htm";
|
|
4
|
-
import { ModelGraph } from "./model-graph.js";
|
|
5
|
-
|
|
6
|
-
const html = htm.bind(h);
|
|
7
|
-
|
|
8
|
-
export function ModelsPanel({ data }) {
|
|
9
|
-
const [selected, setSelected] = useState("__graph");
|
|
10
|
-
const [filter, setFilter] = useState("");
|
|
11
|
-
const models = data || [];
|
|
12
|
-
const detail = selected && selected !== "__graph" ? models.find((m) => m.id === selected) || null : null;
|
|
13
|
-
|
|
14
|
-
const filtered = models.filter(
|
|
15
|
-
(m) => (m.id || m.name || "").toLowerCase().includes(filter.toLowerCase())
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
return html`
|
|
19
|
-
<div class="split-panel">
|
|
20
|
-
<div class="split-left">
|
|
21
|
-
<input
|
|
22
|
-
class="search-input"
|
|
23
|
-
placeholder="Filter models..."
|
|
24
|
-
value=${filter}
|
|
25
|
-
onInput=${(e) => setFilter(e.target.value)}
|
|
26
|
-
/>
|
|
27
|
-
<div
|
|
28
|
-
class="list-item ${selected === "__graph" ? "active" : ""}"
|
|
29
|
-
onClick=${() => setSelected("__graph")}
|
|
30
|
-
style="display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border);margin-bottom:0.5rem;padding-bottom:0.625rem"
|
|
31
|
-
>
|
|
32
|
-
<span style="font-weight:500">Model Graph</span>
|
|
33
|
-
<span class="badge badge-muted">${models.length}</span>
|
|
34
|
-
</div>
|
|
35
|
-
${filtered.map(
|
|
36
|
-
(m) => html`
|
|
37
|
-
<div
|
|
38
|
-
key=${m.id || m.name}
|
|
39
|
-
class="list-item ${selected === (m.id || m.name) ? "active" : ""}"
|
|
40
|
-
onClick=${() => setSelected(m.id || m.name)}
|
|
41
|
-
>
|
|
42
|
-
${m.id || m.name}
|
|
43
|
-
</div>
|
|
44
|
-
`
|
|
45
|
-
)}
|
|
46
|
-
${filtered.length === 0 && html`<div style="color: var(--text-muted); padding: 0.5rem;">No models found</div>`}
|
|
47
|
-
</div>
|
|
48
|
-
<div class="split-right">
|
|
49
|
-
${selected === "__graph"
|
|
50
|
-
? html`<${ModelGraph} models=${models} selectedId=${null} onSelect=${id => setSelected(id)} />`
|
|
51
|
-
: detail ? html`<${ModelDetail} model=${detail} onSelect=${id => setSelected(id)} />`
|
|
52
|
-
: html`
|
|
53
|
-
<div style="color: var(--text-muted); padding: 2rem; text-align: center;">
|
|
54
|
-
Select a model to view details
|
|
55
|
-
</div>
|
|
56
|
-
`}
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function SchemaTab({ schemas }) {
|
|
63
|
-
const [activeTab, setActiveTab] = useState("Input");
|
|
64
|
-
const tabs = ["Input", "Output", "Stored"].filter((t) => schemas?.[t]);
|
|
65
|
-
|
|
66
|
-
if (tabs.length === 0) {
|
|
67
|
-
return html`<div style="color: var(--text-muted); padding: 0.5rem;">No schemas available</div>`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Default to first available tab if active isn't available
|
|
71
|
-
const current = tabs.includes(activeTab) ? activeTab : tabs[0];
|
|
72
|
-
|
|
73
|
-
return html`
|
|
74
|
-
<div>
|
|
75
|
-
<div style="display: flex; gap: 0; margin-bottom: 0.75rem; border-bottom: 1px solid var(--border);">
|
|
76
|
-
${tabs.map(
|
|
77
|
-
(tab) => html`
|
|
78
|
-
<button
|
|
79
|
-
key=${tab}
|
|
80
|
-
onClick=${() => setActiveTab(tab)}
|
|
81
|
-
style="
|
|
82
|
-
padding: 6px 16px;
|
|
83
|
-
background: ${current === tab ? "var(--bg-tertiary)" : "transparent"};
|
|
84
|
-
color: ${current === tab ? "var(--accent)" : "var(--text-muted)"};
|
|
85
|
-
border: none;
|
|
86
|
-
border-bottom: 2px solid ${current === tab ? "var(--accent)" : "transparent"};
|
|
87
|
-
cursor: pointer;
|
|
88
|
-
font-size: 0.875rem;
|
|
89
|
-
font-weight: ${current === tab ? "600" : "400"};
|
|
90
|
-
"
|
|
91
|
-
>
|
|
92
|
-
${tab}
|
|
93
|
-
</button>
|
|
94
|
-
`
|
|
95
|
-
)}
|
|
96
|
-
</div>
|
|
97
|
-
<pre class="mono" style="
|
|
98
|
-
background: var(--bg-secondary);
|
|
99
|
-
padding: 1rem;
|
|
100
|
-
border-radius: 4px;
|
|
101
|
-
overflow: auto;
|
|
102
|
-
max-height: 400px;
|
|
103
|
-
font-size: 0.8125rem;
|
|
104
|
-
line-height: 1.5;
|
|
105
|
-
">${JSON.stringify(schemas[current], null, 2)}</pre>
|
|
106
|
-
</div>
|
|
107
|
-
`;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function ModelLink({ id, onSelect }) {
|
|
111
|
-
return html`<a href="#" class="mono" style="color:var(--accent);text-decoration:none" onClick=${e => { e.preventDefault(); onSelect(id); }}>${id.split("/").pop()}</a>`;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function RelationsTable({ relations, onSelect }) {
|
|
115
|
-
const rows = [];
|
|
116
|
-
if (relations.parent) {
|
|
117
|
-
rows.push({ name: relations.parent.attribute, type: "parent", target: relations.parent.model });
|
|
118
|
-
}
|
|
119
|
-
(relations.links || []).forEach(l => rows.push({ name: l.attribute, type: l.type || "link", target: l.model }));
|
|
120
|
-
(relations.queries || []).forEach(q => rows.push({ name: q.attribute, type: "query", target: q.model }));
|
|
121
|
-
(relations.maps || []).forEach(m => rows.push({ name: m.attribute, type: "map", target: m.model }));
|
|
122
|
-
(relations.children || []).forEach(c => rows.push({ name: "", type: "child", target: c }));
|
|
123
|
-
(relations.binaries || []).forEach(b => rows.push({ name: b.attribute, type: `binary (${b.cardinality})`, target: "" }));
|
|
124
|
-
(relations.behaviors || []).forEach(b => rows.push({ name: b.attribute, type: "behavior", target: b.behavior }));
|
|
125
|
-
if (rows.length === 0) return null;
|
|
126
|
-
return html`
|
|
127
|
-
<div class="detail-section">
|
|
128
|
-
<h3>Relations</h3>
|
|
129
|
-
<div class="table-container">
|
|
130
|
-
<table>
|
|
131
|
-
<thead><tr><th>Attribute</th><th>Type</th><th>Target</th></tr></thead>
|
|
132
|
-
<tbody>
|
|
133
|
-
${rows.map((r, i) => html`
|
|
134
|
-
<tr key=${i}>
|
|
135
|
-
<td class="mono">${r.name || "-"}</td>
|
|
136
|
-
<td><span class="badge ${r.type === "parent" ? "badge-purple" : r.type === "child" ? "badge-purple" : r.type === "query" ? "badge-green" : r.type.startsWith("binary") ? "badge-orange" : r.type === "behavior" ? "badge-orange" : "badge-blue"}">${r.type}</span></td>
|
|
137
|
-
<td>${r.target ? html`<${ModelLink} id=${r.target} onSelect=${onSelect} />` : "-"}</td>
|
|
138
|
-
</tr>
|
|
139
|
-
`)}
|
|
140
|
-
</tbody>
|
|
141
|
-
</table>
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function ModelDetail({ model, onSelect }) {
|
|
148
|
-
const ancestors = model.metadata?.Ancestors || [];
|
|
149
|
-
const subclasses = model.metadata?.Subclasses || [];
|
|
150
|
-
|
|
151
|
-
return html`
|
|
152
|
-
<div>
|
|
153
|
-
<h2 style="margin-bottom:0.5rem;color:var(--accent)">${model.id?.split("/").pop()}</h2>
|
|
154
|
-
<div class="mono" style="font-size:0.75rem;color:var(--text-muted);margin-bottom:1rem">${model.id}</div>
|
|
155
|
-
|
|
156
|
-
<div style="display:flex;gap:0.5rem;margin-bottom:1rem;flex-wrap:wrap;align-items:center">
|
|
157
|
-
${model.store && html`
|
|
158
|
-
<div style="background:var(--bg-tertiary);padding:0.375rem 0.625rem;border-radius:4px;font-size:0.8125rem">
|
|
159
|
-
<span style="color:var(--text-muted)">Store: </span>
|
|
160
|
-
<span class="mono">${model.store}</span>
|
|
161
|
-
${model.storeType && html`<span style="color:var(--text-muted)"> (${model.storeType})</span>`}
|
|
162
|
-
</div>
|
|
163
|
-
`}
|
|
164
|
-
${model.plural && html`
|
|
165
|
-
<div style="background:var(--bg-tertiary);padding:0.375rem 0.625rem;border-radius:4px;font-size:0.8125rem">
|
|
166
|
-
<span style="color:var(--text-muted)">Plural: </span>
|
|
167
|
-
<span class="mono">${model.plural}</span>
|
|
168
|
-
</div>
|
|
169
|
-
`}
|
|
170
|
-
</div>
|
|
171
|
-
|
|
172
|
-
${ancestors.length > 0 && html`
|
|
173
|
-
<div class="detail-section">
|
|
174
|
-
<h3>Inheritance</h3>
|
|
175
|
-
<div style="display:flex;align-items:center;gap:0.375rem;flex-wrap:wrap;font-size:0.8125rem">
|
|
176
|
-
${[...ancestors].reverse().map(a => html`
|
|
177
|
-
<${ModelLink} key=${a} id=${a} onSelect=${onSelect} />
|
|
178
|
-
<span style="color:var(--text-muted)">\u2192</span>
|
|
179
|
-
`)}
|
|
180
|
-
<span class="mono" style="font-weight:600;color:var(--accent)">${model.id?.split("/").pop()}</span>
|
|
181
|
-
${subclasses.length > 0 && html`
|
|
182
|
-
<span style="color:var(--text-muted)">\u2192</span>
|
|
183
|
-
${subclasses.map((s, i) => html`
|
|
184
|
-
<${ModelLink} key=${s} id=${s} onSelect=${onSelect} />
|
|
185
|
-
${i < subclasses.length - 1 && html`<span style="color:var(--text-muted)">,</span>`}
|
|
186
|
-
`)}
|
|
187
|
-
`}
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
190
|
-
`}
|
|
191
|
-
|
|
192
|
-
<${RelationsTable} relations=${model.relations || {}} onSelect=${onSelect} />
|
|
193
|
-
|
|
194
|
-
${model.actions && model.actions.length > 0 && html`
|
|
195
|
-
<div class="detail-section">
|
|
196
|
-
<h3>Actions</h3>
|
|
197
|
-
<div style="display:flex;gap:0.375rem;flex-wrap:wrap">
|
|
198
|
-
${model.actions.map(a => html`
|
|
199
|
-
<span key=${typeof a === "string" ? a : a.name} class="badge badge-blue">${typeof a === "string" ? a : a.name}</span>
|
|
200
|
-
`)}
|
|
201
|
-
</div>
|
|
202
|
-
</div>
|
|
203
|
-
`}
|
|
204
|
-
|
|
205
|
-
${model.schemas && html`
|
|
206
|
-
<div class="detail-section">
|
|
207
|
-
<h3>Schemas</h3>
|
|
208
|
-
<${SchemaTab} schemas=${model.schemas} />
|
|
209
|
-
</div>
|
|
210
|
-
`}
|
|
211
|
-
</div>
|
|
212
|
-
`;
|
|
213
|
-
}
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
import { h } from "preact";
|
|
2
|
-
import { useState, useMemo } from "preact/hooks";
|
|
3
|
-
import htm from "htm";
|
|
4
|
-
import { SchemaForm } from "./schema-form.js";
|
|
5
|
-
|
|
6
|
-
const html = htm.bind(h);
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Generate a random example value from a JSON Schema property.
|
|
10
|
-
*/
|
|
11
|
-
function randomValue(schema, name) {
|
|
12
|
-
if (!schema) return null;
|
|
13
|
-
if (schema.enum) return schema.enum[Math.floor(Math.random() * schema.enum.length)];
|
|
14
|
-
if (schema.const) return schema.const;
|
|
15
|
-
switch (schema.type) {
|
|
16
|
-
case "string":
|
|
17
|
-
if (schema.format === "email") return "user@example.com";
|
|
18
|
-
if (schema.format === "uri" || schema.format === "url") return "https://example.com";
|
|
19
|
-
if (schema.format === "date-time") return new Date().toISOString();
|
|
20
|
-
if (schema.format === "date") return new Date().toISOString().split("T")[0];
|
|
21
|
-
if (schema.format === "uuid") return crypto.randomUUID();
|
|
22
|
-
if (schema.pattern) return `match-${name || "value"}`;
|
|
23
|
-
if (schema.minLength) return (name || "text").padEnd(schema.minLength, "x");
|
|
24
|
-
return name || "string-value";
|
|
25
|
-
case "number":
|
|
26
|
-
case "integer":
|
|
27
|
-
const min = schema.minimum ?? 0;
|
|
28
|
-
const max = schema.maximum ?? 100;
|
|
29
|
-
return schema.type === "integer" ? Math.floor(Math.random() * (max - min) + min) : +(Math.random() * (max - min) + min).toFixed(2);
|
|
30
|
-
case "boolean":
|
|
31
|
-
return Math.random() > 0.5;
|
|
32
|
-
case "array":
|
|
33
|
-
const item = schema.items ? randomValue(schema.items, name) : "item";
|
|
34
|
-
return [item, randomValue(schema.items, name)].filter(Boolean);
|
|
35
|
-
case "object":
|
|
36
|
-
if (!schema.properties) return {};
|
|
37
|
-
const obj = {};
|
|
38
|
-
for (const [k, v] of Object.entries(schema.properties)) {
|
|
39
|
-
obj[k] = randomValue(v, k);
|
|
40
|
-
}
|
|
41
|
-
return obj;
|
|
42
|
-
default:
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function OperationsPanel({ data }) {
|
|
48
|
-
const [filter, setFilter] = useState("");
|
|
49
|
-
const [selected, setSelected] = useState(null);
|
|
50
|
-
const [activeTab, setActiveTab] = useState("form");
|
|
51
|
-
const [formValues, setFormValues] = useState({});
|
|
52
|
-
const operations = data || [];
|
|
53
|
-
|
|
54
|
-
const filtered = operations.filter(
|
|
55
|
-
(o) => (o.id || "").toLowerCase().includes(filter.toLowerCase())
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
const detail = selected ? operations.find(o => o.id === selected) : null;
|
|
59
|
-
|
|
60
|
-
const exampleOutput = useMemo(() => {
|
|
61
|
-
if (!detail?.outputSchema) return null;
|
|
62
|
-
return randomValue(detail.outputSchema, "result");
|
|
63
|
-
}, [detail?.id, detail?.outputSchema]);
|
|
64
|
-
|
|
65
|
-
return html`
|
|
66
|
-
<div class="split-panel">
|
|
67
|
-
<div class="split-left">
|
|
68
|
-
<input
|
|
69
|
-
class="search-input"
|
|
70
|
-
placeholder="Filter operations..."
|
|
71
|
-
value=${filter}
|
|
72
|
-
onInput=${(e) => setFilter(e.target.value)}
|
|
73
|
-
/>
|
|
74
|
-
${filtered.map(o => html`
|
|
75
|
-
<div
|
|
76
|
-
key=${o.id}
|
|
77
|
-
class="list-item ${selected === o.id ? "active" : ""}"
|
|
78
|
-
onClick=${() => { setSelected(o.id); setFormValues({}); setActiveTab("form"); }}
|
|
79
|
-
>
|
|
80
|
-
<div>${o.id}</div>
|
|
81
|
-
<div style="font-size:0.6875rem;color:var(--text-muted)">
|
|
82
|
-
${o.input && o.input !== "void" ? o.input.split("/").pop().split(".").pop() : "void"}
|
|
83
|
-
${" → "}
|
|
84
|
-
${o.output && o.output !== "void" ? o.output.split("/").pop().split(".").pop() : "void"}
|
|
85
|
-
</div>
|
|
86
|
-
${o.rest?.method && html`
|
|
87
|
-
<div style="font-size:0.625rem;margin-top:0.125rem;display:flex;align-items:center;gap:0.25rem;min-width:0">
|
|
88
|
-
<span class="badge method-${o.rest.method.toLowerCase()}" style="font-size:0.5625rem;padding:0.0625rem 0.25rem;flex-shrink:0">${o.rest.method.toUpperCase()}</span>
|
|
89
|
-
<span class="mono" style="color:var(--text-muted);opacity:0.7;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${o.rest.url || o.rest.path || "/"}</span>
|
|
90
|
-
</div>
|
|
91
|
-
`}
|
|
92
|
-
</div>
|
|
93
|
-
`)}
|
|
94
|
-
${filtered.length === 0 && html`<div style="color:var(--text-muted);padding:0.5rem">No operations found</div>`}
|
|
95
|
-
</div>
|
|
96
|
-
<div class="split-right">
|
|
97
|
-
${detail ? html`<${OperationDetail}
|
|
98
|
-
op=${detail}
|
|
99
|
-
activeTab=${activeTab}
|
|
100
|
-
setActiveTab=${setActiveTab}
|
|
101
|
-
formValues=${formValues}
|
|
102
|
-
setFormValues=${setFormValues}
|
|
103
|
-
exampleOutput=${exampleOutput}
|
|
104
|
-
/>` : html`
|
|
105
|
-
<div style="color:var(--text-muted);padding:2rem;text-align:center">
|
|
106
|
-
Select an operation to view details
|
|
107
|
-
</div>
|
|
108
|
-
`}
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
|
-
`;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Lightweight JS syntax highlighter — no external dependencies.
|
|
116
|
-
* Tokenizes into comments, strings, keywords, numbers, and punctuation.
|
|
117
|
-
*/
|
|
118
|
-
function highlightJS(code) {
|
|
119
|
-
const keywords = new Set([
|
|
120
|
-
"async","await","break","case","catch","class","const","continue","debugger","default",
|
|
121
|
-
"delete","do","else","export","extends","finally","for","function","if","import","in",
|
|
122
|
-
"instanceof","let","new","of","return","static","super","switch","this","throw","try",
|
|
123
|
-
"typeof","var","void","while","with","yield","from","true","false","null","undefined"
|
|
124
|
-
]);
|
|
125
|
-
const tokens = [];
|
|
126
|
-
let i = 0;
|
|
127
|
-
while (i < code.length) {
|
|
128
|
-
// Line comments
|
|
129
|
-
if (code[i] === "/" && code[i + 1] === "/") {
|
|
130
|
-
const end = code.indexOf("\n", i);
|
|
131
|
-
tokens.push({ type: "comment", value: code.slice(i, end === -1 ? undefined : end) });
|
|
132
|
-
i = end === -1 ? code.length : end;
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
// Block comments
|
|
136
|
-
if (code[i] === "/" && code[i + 1] === "*") {
|
|
137
|
-
const end = code.indexOf("*/", i + 2);
|
|
138
|
-
tokens.push({ type: "comment", value: code.slice(i, end === -1 ? undefined : end + 2) });
|
|
139
|
-
i = end === -1 ? code.length : end + 2;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
// Strings
|
|
143
|
-
if (code[i] === '"' || code[i] === "'" || code[i] === "`") {
|
|
144
|
-
const q = code[i];
|
|
145
|
-
let j = i + 1;
|
|
146
|
-
while (j < code.length && code[j] !== q) { if (code[j] === "\\") j++; j++; }
|
|
147
|
-
tokens.push({ type: "string", value: code.slice(i, j + 1) });
|
|
148
|
-
i = j + 1;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
// Numbers
|
|
152
|
-
if (/[0-9]/.test(code[i]) && (i === 0 || /[^a-zA-Z_$]/.test(code[i - 1]))) {
|
|
153
|
-
let j = i;
|
|
154
|
-
while (j < code.length && /[0-9a-fA-FxXoObBeE._n]/.test(code[j])) j++;
|
|
155
|
-
tokens.push({ type: "number", value: code.slice(i, j) });
|
|
156
|
-
i = j;
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
// Words (identifiers / keywords)
|
|
160
|
-
if (/[a-zA-Z_$]/.test(code[i])) {
|
|
161
|
-
let j = i;
|
|
162
|
-
while (j < code.length && /[a-zA-Z0-9_$]/.test(code[j])) j++;
|
|
163
|
-
const word = code.slice(i, j);
|
|
164
|
-
tokens.push({ type: keywords.has(word) ? "keyword" : "ident", value: word });
|
|
165
|
-
i = j;
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
// Punctuation / operators
|
|
169
|
-
tokens.push({ type: "punct", value: code[i] });
|
|
170
|
-
i++;
|
|
171
|
-
}
|
|
172
|
-
return tokens;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function CodeBlock({ code }) {
|
|
176
|
-
const tokens = useMemo(() => highlightJS(code), [code]);
|
|
177
|
-
return html`<pre class="code-block"><code>${tokens.map((t, i) =>
|
|
178
|
-
t.type === "comment" ? html`<span key=${i} class="hl-comment">${t.value}</span>`
|
|
179
|
-
: t.type === "string" ? html`<span key=${i} class="hl-string">${t.value}</span>`
|
|
180
|
-
: t.type === "keyword" ? html`<span key=${i} class="hl-keyword">${t.value}</span>`
|
|
181
|
-
: t.type === "number" ? html`<span key=${i} class="hl-number">${t.value}</span>`
|
|
182
|
-
: t.value
|
|
183
|
-
)}</code></pre>`;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function OperationDetail({ op, activeTab, setActiveTab, formValues, setFormValues, exampleOutput }) {
|
|
187
|
-
const tabs = [];
|
|
188
|
-
if (op.inputSchema) tabs.push("form");
|
|
189
|
-
tabs.push("input-schema", "output-schema");
|
|
190
|
-
if (exampleOutput !== null) tabs.push("example");
|
|
191
|
-
if (op.implementor?.code) tabs.push("code");
|
|
192
|
-
|
|
193
|
-
const currentTab = tabs.includes(activeTab) ? activeTab : tabs[0];
|
|
194
|
-
|
|
195
|
-
const hasInput = op.input && op.input !== "void";
|
|
196
|
-
const hasOutput = op.output && op.output !== "void";
|
|
197
|
-
|
|
198
|
-
return html`
|
|
199
|
-
<div>
|
|
200
|
-
<h2 style="margin-bottom:0.5rem;color:var(--accent)">${op.id}</h2>
|
|
201
|
-
|
|
202
|
-
<div style="display:flex;gap:0.75rem;margin-bottom:1rem;flex-wrap:wrap">
|
|
203
|
-
${op.summary && html`<div style="color:var(--text-muted);font-size:0.875rem">${op.summary}</div>`}
|
|
204
|
-
</div>
|
|
205
|
-
|
|
206
|
-
<div style="display:flex;gap:0.5rem;margin-bottom:0.75rem;flex-wrap:wrap;align-items:center">
|
|
207
|
-
<div style="background:var(--bg-tertiary);padding:0.375rem 0.625rem;border-radius:4px;font-size:0.8125rem">
|
|
208
|
-
<span style="color:var(--text-muted)">Input: </span>
|
|
209
|
-
<span class="mono">${hasInput ? op.input : "void"}</span>
|
|
210
|
-
</div>
|
|
211
|
-
<div style="background:var(--bg-tertiary);padding:0.375rem 0.625rem;border-radius:4px;font-size:0.8125rem">
|
|
212
|
-
<span style="color:var(--text-muted)">Output: </span>
|
|
213
|
-
<span class="mono">${hasOutput ? op.output : "void"}</span>
|
|
214
|
-
</div>
|
|
215
|
-
${op.implementor && html`
|
|
216
|
-
<div style="background:var(--bg-tertiary);padding:0.375rem 0.625rem;border-radius:4px;font-size:0.8125rem">
|
|
217
|
-
<span style="color:var(--text-muted)">${op.implementor.type === "model" ? "Model" : "Service"}: </span>
|
|
218
|
-
<span class="mono">${op.implementor.name}</span>
|
|
219
|
-
${op.implementor.method && html`<span class="mono" style="color:var(--text-muted)">.${op.implementor.method}()</span>`}
|
|
220
|
-
</div>
|
|
221
|
-
`}
|
|
222
|
-
${op.tags?.length > 0 && op.tags.map(t => html`
|
|
223
|
-
<span key=${t} class="badge badge-purple">${t}</span>
|
|
224
|
-
`)}
|
|
225
|
-
${op.rest && typeof op.rest === "object" && op.rest.method && html`
|
|
226
|
-
<span class="badge method-${op.rest.method.toLowerCase()}" style="padding:0.125rem 0.5rem;line-height:1.2">${op.rest.method.toUpperCase()}</span>
|
|
227
|
-
<span class="mono" style="font-size:0.8125rem;color:var(--text-muted)">${op.rest.url || op.rest.path || "/"}</span>
|
|
228
|
-
`}
|
|
229
|
-
</div>
|
|
230
|
-
|
|
231
|
-
<!-- Tabs -->
|
|
232
|
-
<div style="display:flex;gap:0;margin-bottom:0.75rem;border-bottom:1px solid var(--border)">
|
|
233
|
-
${tabs.map(tab => {
|
|
234
|
-
const label = tab === "form" ? "Try It" : tab === "input-schema" ? "Input Schema" : tab === "output-schema" ? "Output Schema" : tab === "code" ? "Code" : "Example Output";
|
|
235
|
-
return html`
|
|
236
|
-
<button key=${tab} onClick=${() => setActiveTab(tab)} style="
|
|
237
|
-
padding:6px 16px;
|
|
238
|
-
background:${currentTab === tab ? "var(--bg-tertiary)" : "transparent"};
|
|
239
|
-
color:${currentTab === tab ? "var(--accent)" : "var(--text-muted)"};
|
|
240
|
-
border:none;
|
|
241
|
-
border-bottom:2px solid ${currentTab === tab ? "var(--accent)" : "transparent"};
|
|
242
|
-
cursor:pointer;font-size:0.875rem;
|
|
243
|
-
font-weight:${currentTab === tab ? "600" : "400"};
|
|
244
|
-
">${label}</button>
|
|
245
|
-
`;
|
|
246
|
-
})}
|
|
247
|
-
</div>
|
|
248
|
-
|
|
249
|
-
<!-- Tab Content -->
|
|
250
|
-
${currentTab === "form" && op.inputSchema && html`
|
|
251
|
-
<div>
|
|
252
|
-
<${SchemaForm} schema=${op.inputSchema} values=${formValues} onChange=${setFormValues} />
|
|
253
|
-
<div style="margin-top:1rem">
|
|
254
|
-
<button class="btn btn-primary" disabled>Execute (coming soon)</button>
|
|
255
|
-
</div>
|
|
256
|
-
${Object.keys(formValues).length > 0 && html`
|
|
257
|
-
<div style="margin-top:0.75rem">
|
|
258
|
-
<strong style="color:var(--text-muted);font-size:0.75rem;text-transform:uppercase">Request Body</strong>
|
|
259
|
-
<pre class="mono" style="
|
|
260
|
-
background:var(--bg-secondary);padding:0.75rem;border-radius:4px;
|
|
261
|
-
margin-top:0.25rem;font-size:0.8125rem;overflow:auto;max-height:200px;
|
|
262
|
-
">${JSON.stringify(formValues, null, 2)}</pre>
|
|
263
|
-
</div>
|
|
264
|
-
`}
|
|
265
|
-
</div>
|
|
266
|
-
`}
|
|
267
|
-
|
|
268
|
-
${currentTab === "input-schema" && html`
|
|
269
|
-
<div>
|
|
270
|
-
${hasInput && op.inputSchema
|
|
271
|
-
? html`<pre class="mono" style="
|
|
272
|
-
background:var(--bg-secondary);padding:1rem;border-radius:4px;
|
|
273
|
-
overflow:auto;max-height:400px;font-size:0.8125rem;line-height:1.5;
|
|
274
|
-
">${JSON.stringify(op.inputSchema, null, 2)}</pre>`
|
|
275
|
-
: html`<div style="color:var(--text-muted);padding:0.5rem">${hasInput ? `Schema ref: ${op.input} (not resolved)` : "No input (void)"}</div>`
|
|
276
|
-
}
|
|
277
|
-
</div>
|
|
278
|
-
`}
|
|
279
|
-
|
|
280
|
-
${currentTab === "output-schema" && html`
|
|
281
|
-
<div>
|
|
282
|
-
${hasOutput && op.outputSchema
|
|
283
|
-
? html`<pre class="mono" style="
|
|
284
|
-
background:var(--bg-secondary);padding:1rem;border-radius:4px;
|
|
285
|
-
overflow:auto;max-height:400px;font-size:0.8125rem;line-height:1.5;
|
|
286
|
-
">${JSON.stringify(op.outputSchema, null, 2)}</pre>`
|
|
287
|
-
: html`<div style="color:var(--text-muted);padding:0.5rem">${hasOutput ? `Schema ref: ${op.output} (not resolved)` : "No output (void)"}</div>`
|
|
288
|
-
}
|
|
289
|
-
</div>
|
|
290
|
-
`}
|
|
291
|
-
|
|
292
|
-
${currentTab === "example" && html`
|
|
293
|
-
<div>
|
|
294
|
-
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
|
|
295
|
-
<strong style="color:var(--text-muted);font-size:0.75rem;text-transform:uppercase">Random Example</strong>
|
|
296
|
-
<button class="btn btn-ghost btn-sm" onClick=${() => { /* force re-render */ setActiveTab("input-schema"); setTimeout(() => setActiveTab("example"), 0); }}>Regenerate</button>
|
|
297
|
-
</div>
|
|
298
|
-
<pre class="mono" style="
|
|
299
|
-
background:var(--bg-secondary);padding:1rem;border-radius:4px;
|
|
300
|
-
overflow:auto;max-height:400px;font-size:0.8125rem;line-height:1.5;
|
|
301
|
-
">${JSON.stringify(exampleOutput, null, 2)}</pre>
|
|
302
|
-
</div>
|
|
303
|
-
`}
|
|
304
|
-
|
|
305
|
-
${currentTab === "code" && op.implementor?.code && html`
|
|
306
|
-
<div>
|
|
307
|
-
<div style="margin-bottom:0.5rem;font-size:0.8125rem;color:var(--text-muted)">
|
|
308
|
-
<span class="mono">${op.implementor.name}.${op.implementor.method}()</span>
|
|
309
|
-
</div>
|
|
310
|
-
<${CodeBlock} code=${op.implementor.code} />
|
|
311
|
-
</div>
|
|
312
|
-
`}
|
|
313
|
-
</div>
|
|
314
|
-
`;
|
|
315
|
-
}
|