@jsenv/server 15.5.3 → 16.0.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/README.md +79 -86
- package/dist/html/4xx.html +196 -0
- package/dist/html/500.html +17 -0
- package/dist/html/route_inspector.html +2435 -0
- package/dist/js/formidable.js +17 -0
- package/dist/js/vendors.js +2001 -0
- package/dist/js/ws.js +6863 -0
- package/dist/jsenv_server.js +8829 -0
- package/package.json +20 -9
- package/src/content_negotiation/pick_content_version.js +63 -0
- package/src/{fetch_filesystem.js → filesystem/fetch_filesystem.js} +57 -72
- package/src/{serve_directory.js → filesystem/serve_directory.js} +1 -2
- package/src/interfacing_with_node/from_node_request.js +455 -0
- package/src/interfacing_with_node/get_observable_value_type.js +43 -0
- package/src/interfacing_with_node/observable.js +91 -75
- package/src/interfacing_with_node/observable_from.js +34 -0
- package/src/interfacing_with_node/observable_from_file_handle.js +30 -0
- package/src/interfacing_with_node/observable_from_node_stream.js +44 -46
- package/src/interfacing_with_node/observable_from_node_web_readable_stream.js +32 -0
- package/src/interfacing_with_node/observable_from_promise.js +13 -0
- package/src/interfacing_with_node/write_node_response.js +80 -35
- package/src/internal/colorizeResponseStatus.js +2 -0
- package/src/internal/headers_composition.js +42 -15
- package/src/internal/multiple-header.js +27 -12
- package/src/internal/object_composition.js +2 -2
- package/src/internal/response_composition.js +16 -3
- package/src/main.js +15 -11
- package/src/progressive_response.js +38 -0
- package/src/replace_placeholder_in_html.js +12 -0
- package/src/request_body_handling.js +22 -0
- package/src/router/router.js +878 -0
- package/src/server_events.js +431 -0
- package/src/service_controller.js +79 -73
- package/src/services/autoreload_on_server_restart/client/autoreload_on_server_restart.js +125 -0
- package/src/services/autoreload_on_server_restart/jsenv_service_autoreload_on_server_restart.js +45 -0
- package/src/services/cors/jsenv_service_cors.js +10 -18
- package/src/services/default_body_4xx_5xx/client/4xx.html +65 -0
- package/src/services/default_body_4xx_5xx/jsenv_service_default_body_4xx_5xx.js +82 -0
- package/src/services/error_handler/client/500.html +17 -0
- package/src/services/error_handler/jsenv_service_error_handler.js +47 -28
- package/src/services/internal_client_files/jsenv_service_internal_client_files.js +34 -0
- package/src/services/open_file/jsenv_service_open_file.js +37 -0
- package/src/services/route_inspector/client/code_block.js +215 -0
- package/src/services/route_inspector/client/route_inspector.html +1363 -0
- package/src/services/route_inspector/jsenv_service_route_inspector.js +39 -0
- package/src/start_server.js +575 -607
- package/src/web_socket_response.js +53 -0
- package/src/from_fetch_response.js +0 -15
- package/src/interfacing_with_node/body.js +0 -81
- package/src/interfacing_with_node/request_factory.js +0 -125
- package/src/readRequestBody.js +0 -32
- package/src/server_timing/timing_measure.js +0 -26
- package/src/service_composition/path_to_regexp.js +0 -436
- package/src/service_composition/routing.js +0 -31
- package/src/sse/sse_room.js +0 -313
|
@@ -0,0 +1,1363 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Route inspector</title>
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{
|
|
9
|
+
"imports": {
|
|
10
|
+
"preact": "https://esm.sh/preact@10.23.1",
|
|
11
|
+
"preact/": "https://esm.sh/preact@10.23.1/",
|
|
12
|
+
"@preact/signals": "https://esm.sh/@preact/signals@1.3.0?external=preact",
|
|
13
|
+
"htm/preact": "https://esm.sh/htm@3.1.1/preact?external=preact"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
<script src="/@jsenv/server/src/services/route_inspector/client/code_block.js?inline"></script>
|
|
18
|
+
<script src="/@jsenv/server/src/services/autoreload_on_server_restart/client/autoreload_on_server_restart.js?inline"></script>
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<style>
|
|
22
|
+
:root {
|
|
23
|
+
--background-color: #fff;
|
|
24
|
+
--text-color: #333;
|
|
25
|
+
--border-color: #e0e0e0;
|
|
26
|
+
--primary-color: #1976d2;
|
|
27
|
+
--secondary-color: #f5f5f5;
|
|
28
|
+
--success-color: #4caf50;
|
|
29
|
+
--warning-color: #ff9800;
|
|
30
|
+
--method-get-color: #61affe;
|
|
31
|
+
--method-post-color: #49cc90;
|
|
32
|
+
--method-put-color: #ff9800;
|
|
33
|
+
--method-delete-color: #f44336;
|
|
34
|
+
--method-patch-color: #9c27b0;
|
|
35
|
+
--method-options-color: #607d8b;
|
|
36
|
+
--method-head-color: #00bcd4;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:root[data-theme="light"] {
|
|
40
|
+
--background-color: #fff;
|
|
41
|
+
--text-color: #333;
|
|
42
|
+
--border-color: #e0e0e0;
|
|
43
|
+
--primary-color: #1976d2;
|
|
44
|
+
--secondary-color: #f5f5f5;
|
|
45
|
+
}
|
|
46
|
+
:root[data-theme="dark"] {
|
|
47
|
+
--background-color: #121212;
|
|
48
|
+
--text-color: #e0e0e0;
|
|
49
|
+
--border-color: #333;
|
|
50
|
+
--primary-color: #4f9eff;
|
|
51
|
+
--secondary-color: #1e1e1e;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:root[data-theme="dark"] a {
|
|
55
|
+
color: dodgerblue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
body {
|
|
59
|
+
font-family:
|
|
60
|
+
system-ui,
|
|
61
|
+
-apple-system,
|
|
62
|
+
BlinkMacSystemFont,
|
|
63
|
+
"Segoe UI",
|
|
64
|
+
Roboto,
|
|
65
|
+
Arial,
|
|
66
|
+
sans-serif;
|
|
67
|
+
line-height: 1.5;
|
|
68
|
+
color: var(--text-color);
|
|
69
|
+
background-color: var(--background-color);
|
|
70
|
+
margin: 0;
|
|
71
|
+
|
|
72
|
+
transition:
|
|
73
|
+
background-color 0.3s ease,
|
|
74
|
+
color 0.3s ease;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.container {
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: row;
|
|
80
|
+
}
|
|
81
|
+
aside {
|
|
82
|
+
width: 250px;
|
|
83
|
+
flex-shrink: 0;
|
|
84
|
+
border-radius: 6px;
|
|
85
|
+
overflow: hidden;
|
|
86
|
+
background: var(--background-color);
|
|
87
|
+
transition:
|
|
88
|
+
width 0.3s ease,
|
|
89
|
+
margin-left 0.3s ease;
|
|
90
|
+
max-height: calc(100vh - 40px);
|
|
91
|
+
overflow-y: auto;
|
|
92
|
+
margin-right: 20px;
|
|
93
|
+
padding-top: 20px;
|
|
94
|
+
padding-left: 10px;
|
|
95
|
+
}
|
|
96
|
+
main {
|
|
97
|
+
flex-grow: 1;
|
|
98
|
+
flex-basis: 0;
|
|
99
|
+
min-width: 0;
|
|
100
|
+
transition: margin-left 0.3s ease;
|
|
101
|
+
padding-top: 10px;
|
|
102
|
+
padding-right: 20px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.header_row {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: 10px;
|
|
109
|
+
}
|
|
110
|
+
.note_button_open {
|
|
111
|
+
background: none;
|
|
112
|
+
border: none;
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
border-radius: 50%;
|
|
115
|
+
transition: background-color 0.2s;
|
|
116
|
+
padding: 0;
|
|
117
|
+
color: var(--text-color);
|
|
118
|
+
opacity: 0.7;
|
|
119
|
+
height: 24px;
|
|
120
|
+
}
|
|
121
|
+
.note_button_open:hover {
|
|
122
|
+
background-color: color-mix(
|
|
123
|
+
in srgb,
|
|
124
|
+
var(--primary-color) 15%,
|
|
125
|
+
transparent
|
|
126
|
+
);
|
|
127
|
+
opacity: 1;
|
|
128
|
+
}
|
|
129
|
+
.note_title {
|
|
130
|
+
font-weight: bold;
|
|
131
|
+
font-size: 1.1rem;
|
|
132
|
+
margin-bottom: 10px;
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
}
|
|
136
|
+
.note_button_close {
|
|
137
|
+
position: absolute;
|
|
138
|
+
top: 15px;
|
|
139
|
+
right: 15px;
|
|
140
|
+
background: none;
|
|
141
|
+
border: none;
|
|
142
|
+
cursor: pointer;
|
|
143
|
+
padding: 5px;
|
|
144
|
+
border-radius: 50%;
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
148
|
+
}
|
|
149
|
+
.note_button_close:hover {
|
|
150
|
+
background-color: var(--secondary-color);
|
|
151
|
+
}
|
|
152
|
+
.note_content {
|
|
153
|
+
background-color: var(--background-color);
|
|
154
|
+
border-radius: 8px;
|
|
155
|
+
padding: 20px;
|
|
156
|
+
max-width: 500px;
|
|
157
|
+
width: 90%;
|
|
158
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
|
159
|
+
position: relative;
|
|
160
|
+
transform: translateY(-20px);
|
|
161
|
+
transition: transform 0.3s ease;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.theme-toggle {
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#theme-toggle-btn {
|
|
170
|
+
background: none;
|
|
171
|
+
border: none;
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
width: 40px;
|
|
174
|
+
height: 40px;
|
|
175
|
+
border-radius: 50%;
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
justify-content: center;
|
|
179
|
+
transition: background-color 0.2s;
|
|
180
|
+
background-color: var(--secondary-color);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#theme-toggle-btn:hover {
|
|
184
|
+
background-color: color-mix(
|
|
185
|
+
in srgb,
|
|
186
|
+
var(--primary-color) 15%,
|
|
187
|
+
transparent
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#theme-toggle-btn svg {
|
|
192
|
+
fill: var(--text-color);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.sun-icon {
|
|
196
|
+
display: none;
|
|
197
|
+
}
|
|
198
|
+
.moon-icon {
|
|
199
|
+
display: none;
|
|
200
|
+
}
|
|
201
|
+
:root[data-theme="dark"] .sun-icon {
|
|
202
|
+
display: block;
|
|
203
|
+
}
|
|
204
|
+
:root[data-theme="light"] .moon-icon {
|
|
205
|
+
display: block;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.progress-container {
|
|
209
|
+
width: 100%;
|
|
210
|
+
height: 100%;
|
|
211
|
+
background-color: rgba(25, 118, 210, 0.2);
|
|
212
|
+
border-radius: 4px;
|
|
213
|
+
overflow: hidden;
|
|
214
|
+
position: relative;
|
|
215
|
+
margin: 20px 0;
|
|
216
|
+
}
|
|
217
|
+
.progress-indeterminate .progress-bar-long,
|
|
218
|
+
.progress-indeterminate .progress-bar-short {
|
|
219
|
+
position: absolute;
|
|
220
|
+
top: 0;
|
|
221
|
+
height: 100%;
|
|
222
|
+
background-color: var(--primary-color);
|
|
223
|
+
border-radius: 4px;
|
|
224
|
+
animation-duration: 2.2s;
|
|
225
|
+
animation-iteration-count: infinite;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.progress-indeterminate .progress-bar-long {
|
|
229
|
+
animation-name: indeterminate-ltr;
|
|
230
|
+
width: 75%;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.progress-indeterminate .progress-bar-short {
|
|
234
|
+
animation-name: indeterminate-short-ltr;
|
|
235
|
+
width: 20%;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@keyframes indeterminate-ltr {
|
|
239
|
+
0% {
|
|
240
|
+
left: -90%;
|
|
241
|
+
right: 100%;
|
|
242
|
+
}
|
|
243
|
+
60% {
|
|
244
|
+
left: -90%;
|
|
245
|
+
right: 100%;
|
|
246
|
+
}
|
|
247
|
+
100% {
|
|
248
|
+
left: 100%;
|
|
249
|
+
right: -35%;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@keyframes indeterminate-short-ltr {
|
|
254
|
+
0% {
|
|
255
|
+
left: -200%;
|
|
256
|
+
right: 100%;
|
|
257
|
+
}
|
|
258
|
+
60% {
|
|
259
|
+
left: 107%;
|
|
260
|
+
right: -8%;
|
|
261
|
+
}
|
|
262
|
+
100% {
|
|
263
|
+
left: 107%;
|
|
264
|
+
right: -8%;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* Styles pour les routes */
|
|
269
|
+
.auto_head_note {
|
|
270
|
+
margin-top: 10px;
|
|
271
|
+
font-size: 14px;
|
|
272
|
+
color: var(--method-head-color);
|
|
273
|
+
display: flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
}
|
|
276
|
+
.note {
|
|
277
|
+
background-color: rgba(255, 152, 0, 0.1);
|
|
278
|
+
border-left: 4px solid var(--warning-color);
|
|
279
|
+
padding: 12px;
|
|
280
|
+
margin-bottom: 20px;
|
|
281
|
+
border-radius: 4px;
|
|
282
|
+
}
|
|
283
|
+
.info_pill {
|
|
284
|
+
display: inline-flex;
|
|
285
|
+
align-items: center;
|
|
286
|
+
background-color: rgba(0, 0, 0, 0.05);
|
|
287
|
+
border-radius: 14px;
|
|
288
|
+
padding: 4px 10px;
|
|
289
|
+
font-size: 13px;
|
|
290
|
+
margin-left: 10px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.route_container {
|
|
294
|
+
display: flex;
|
|
295
|
+
gap: 20px;
|
|
296
|
+
align-items: flex-start;
|
|
297
|
+
}
|
|
298
|
+
.filters_sidebar {
|
|
299
|
+
width: 250px;
|
|
300
|
+
flex-shrink: 0;
|
|
301
|
+
border-radius: 6px;
|
|
302
|
+
overflow: hidden;
|
|
303
|
+
border: 1px solid var(--border-color);
|
|
304
|
+
background: var(--secondary-color);
|
|
305
|
+
}
|
|
306
|
+
.sidebar_header {
|
|
307
|
+
padding: 12px 15px;
|
|
308
|
+
font-weight: 500;
|
|
309
|
+
border-bottom: 1px solid var(--border-color);
|
|
310
|
+
display: flex;
|
|
311
|
+
align-items: center;
|
|
312
|
+
justify-content: space-between;
|
|
313
|
+
}
|
|
314
|
+
.filters_content {
|
|
315
|
+
padding: 15px;
|
|
316
|
+
background: var(--background-color);
|
|
317
|
+
}
|
|
318
|
+
.filters_count {
|
|
319
|
+
background-color: var(--primary-color);
|
|
320
|
+
color: white;
|
|
321
|
+
padding: 2px 8px;
|
|
322
|
+
border-radius: 12px;
|
|
323
|
+
font-size: 0.8rem;
|
|
324
|
+
font-weight: normal;
|
|
325
|
+
}
|
|
326
|
+
@media (max-width: 768px) {
|
|
327
|
+
.route_container {
|
|
328
|
+
flex-direction: column;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.filters_sidebar {
|
|
332
|
+
width: 100%;
|
|
333
|
+
margin-bottom: 20px;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.route_list {
|
|
338
|
+
margin-top: 20px;
|
|
339
|
+
flex-grow: 1;
|
|
340
|
+
min-width: 0; /* Prevent flexbox from expanding beyond available space */
|
|
341
|
+
}
|
|
342
|
+
.route_details {
|
|
343
|
+
margin-bottom: 8px;
|
|
344
|
+
box-shadow: 0 0 3px rgba(0, 0, 0, 0.19);
|
|
345
|
+
border: 1px solid #000;
|
|
346
|
+
border-radius: 4px;
|
|
347
|
+
margin: 0 0 15px;
|
|
348
|
+
border-color: var(--method-color);
|
|
349
|
+
background: color-mix(in srgb, var(--method-color), transparent 90%);
|
|
350
|
+
}
|
|
351
|
+
.route_details > summary {
|
|
352
|
+
cursor: pointer;
|
|
353
|
+
list-style: none;
|
|
354
|
+
}
|
|
355
|
+
.route_details > summary::marker {
|
|
356
|
+
display: none;
|
|
357
|
+
}
|
|
358
|
+
.route_summary_container {
|
|
359
|
+
display: flex;
|
|
360
|
+
align-items: center;
|
|
361
|
+
padding: 5px;
|
|
362
|
+
}
|
|
363
|
+
.route_method {
|
|
364
|
+
border-radius: 3px;
|
|
365
|
+
color: #fff;
|
|
366
|
+
font-family: sans-serif;
|
|
367
|
+
font-size: 14px;
|
|
368
|
+
font-weight: 700;
|
|
369
|
+
min-width: 80px;
|
|
370
|
+
padding: 6px 0;
|
|
371
|
+
text-align: center;
|
|
372
|
+
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
|
|
373
|
+
background: var(--method-color);
|
|
374
|
+
}
|
|
375
|
+
.route_resource {
|
|
376
|
+
margin-left: 8px;
|
|
377
|
+
font-family: monospace;
|
|
378
|
+
font-size: 16px;
|
|
379
|
+
font-weight: 600;
|
|
380
|
+
word-break: break-word;
|
|
381
|
+
margin-top: 0;
|
|
382
|
+
margin-bottom: 0;
|
|
383
|
+
}
|
|
384
|
+
.route_details[data-method="HEAD"] {
|
|
385
|
+
--method-color: var(--method-head-color);
|
|
386
|
+
}
|
|
387
|
+
.route_details[data-method="OPTIONS"] {
|
|
388
|
+
--method-color: var(--method-options-color);
|
|
389
|
+
}
|
|
390
|
+
.route_details[data-method="GET"] {
|
|
391
|
+
--method-color: var(--method-get-color);
|
|
392
|
+
}
|
|
393
|
+
.route_details[data-method="POST"] {
|
|
394
|
+
--method-color: var(--method-post-color);
|
|
395
|
+
}
|
|
396
|
+
.route_details[data-method="PUT"] {
|
|
397
|
+
--method-color: var(--method-put-color);
|
|
398
|
+
}
|
|
399
|
+
.route_details[data-method="PATCH"] {
|
|
400
|
+
--method-color: var(--method-patch-color);
|
|
401
|
+
}
|
|
402
|
+
.route_details[data-method="DELETE"] {
|
|
403
|
+
--method-color: var(--method-delete-color);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.route_owner_name {
|
|
407
|
+
background-color: #ffefd2;
|
|
408
|
+
color: #666;
|
|
409
|
+
padding: 4px 8px;
|
|
410
|
+
text-align: center;
|
|
411
|
+
border-radius: 5px;
|
|
412
|
+
}
|
|
413
|
+
:root[data-theme="dark"] .route_owner_name {
|
|
414
|
+
background-color: rgb(96, 59, 13);
|
|
415
|
+
color: white;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.route_details_content {
|
|
419
|
+
border-top: 1px solid var(--method-color);
|
|
420
|
+
padding: 10px;
|
|
421
|
+
}
|
|
422
|
+
.route_description {
|
|
423
|
+
margin: 0;
|
|
424
|
+
}
|
|
425
|
+
.route_content_nego_title {
|
|
426
|
+
font-weight: bold;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.filter_container {
|
|
430
|
+
margin-bottom: 20px;
|
|
431
|
+
border-radius: 4px;
|
|
432
|
+
overflow: hidden;
|
|
433
|
+
border: 1px solid var(--border-color);
|
|
434
|
+
background: var(--secondary-color);
|
|
435
|
+
}
|
|
436
|
+
.filter_container::marker {
|
|
437
|
+
display: none;
|
|
438
|
+
}
|
|
439
|
+
.filter_summary {
|
|
440
|
+
cursor: pointer;
|
|
441
|
+
font-weight: 500;
|
|
442
|
+
display: flex;
|
|
443
|
+
flex-direction: row;
|
|
444
|
+
align-items: center;
|
|
445
|
+
justify-content: space-between;
|
|
446
|
+
user-select: none;
|
|
447
|
+
}
|
|
448
|
+
.filter:open .arrow_down {
|
|
449
|
+
transform: rotate(180deg);
|
|
450
|
+
}
|
|
451
|
+
.filter_list {
|
|
452
|
+
background: var(--background-color);
|
|
453
|
+
border-top: 1px solid var(--border-color);
|
|
454
|
+
overflow: hidden;
|
|
455
|
+
}
|
|
456
|
+
.filter_item {
|
|
457
|
+
margin: 15px;
|
|
458
|
+
}
|
|
459
|
+
.filter_badge {
|
|
460
|
+
background-color: var(--primary-color);
|
|
461
|
+
color: white;
|
|
462
|
+
padding: 2px 8px;
|
|
463
|
+
border-radius: 12px;
|
|
464
|
+
font-size: 0.8rem;
|
|
465
|
+
margin-left: 10px;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
select {
|
|
469
|
+
min-width: 200px;
|
|
470
|
+
padding: 8px;
|
|
471
|
+
border-radius: 4px;
|
|
472
|
+
border: 1px solid var(--border-color);
|
|
473
|
+
background-color: var(--background-color);
|
|
474
|
+
color: var(--text-color);
|
|
475
|
+
font-family: inherit;
|
|
476
|
+
}
|
|
477
|
+
select option {
|
|
478
|
+
padding: 8px;
|
|
479
|
+
}
|
|
480
|
+
select option:checked {
|
|
481
|
+
background-color: var(--primary-color);
|
|
482
|
+
color: white;
|
|
483
|
+
}
|
|
484
|
+
.filter_form {
|
|
485
|
+
display: flex;
|
|
486
|
+
flex-direction: column;
|
|
487
|
+
flex-wrap: wrap;
|
|
488
|
+
gap: 8px;
|
|
489
|
+
margin-top: 15px;
|
|
490
|
+
margin-bottom: 15px;
|
|
491
|
+
user-select: none;
|
|
492
|
+
}
|
|
493
|
+
.filter_form label {
|
|
494
|
+
display: flex;
|
|
495
|
+
align-items: center;
|
|
496
|
+
gap: 5px;
|
|
497
|
+
cursor: pointer;
|
|
498
|
+
width: 100%;
|
|
499
|
+
word-break: keep-all;
|
|
500
|
+
overflow: hidden;
|
|
501
|
+
text-overflow: ellipsis;
|
|
502
|
+
white-space: nowrap;
|
|
503
|
+
}
|
|
504
|
+
label .filter_text {
|
|
505
|
+
text-overflow: ellipsis;
|
|
506
|
+
overflow: hidden;
|
|
507
|
+
}
|
|
508
|
+
.filter_text[data-method] {
|
|
509
|
+
border-left: 5px solid var(--method-color);
|
|
510
|
+
padding-left: 4px;
|
|
511
|
+
}
|
|
512
|
+
.filter_text[data-method="all"] {
|
|
513
|
+
--method-color: transparent;
|
|
514
|
+
border-image-slice: 1;
|
|
515
|
+
}
|
|
516
|
+
.filter_text[data-method="HEAD"] {
|
|
517
|
+
--method-color: var(--method-head-color);
|
|
518
|
+
}
|
|
519
|
+
.filter_text[data-method="OPTIONS"] {
|
|
520
|
+
--method-color: var(--method-options-color);
|
|
521
|
+
}
|
|
522
|
+
.filter_text[data-method="GET"] {
|
|
523
|
+
--method-color: var(--method-get-color);
|
|
524
|
+
}
|
|
525
|
+
.filter_text[data-method="POST"] {
|
|
526
|
+
--method-color: var(--method-post-color);
|
|
527
|
+
}
|
|
528
|
+
.filter_text[data-method="PUT"] {
|
|
529
|
+
--method-color: var(--method-put-color);
|
|
530
|
+
}
|
|
531
|
+
.filter_text[data-method="PATCH"] {
|
|
532
|
+
--method-color: var(--method-patch-color);
|
|
533
|
+
}
|
|
534
|
+
.filter_text[data-method="DELETE"] {
|
|
535
|
+
--method-color: var(--method-delete-color);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.filter_count {
|
|
539
|
+
background-color: #eee;
|
|
540
|
+
color: #666;
|
|
541
|
+
opacity: 0.8;
|
|
542
|
+
padding: 2px 8px;
|
|
543
|
+
border-radius: 12px;
|
|
544
|
+
font-size: 0.8rem;
|
|
545
|
+
margin-left: auto;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.filter_actions {
|
|
549
|
+
display: flex;
|
|
550
|
+
gap: 10px;
|
|
551
|
+
}
|
|
552
|
+
.filter_button {
|
|
553
|
+
background-color: var(--secondary-color);
|
|
554
|
+
border: 1px solid var(--border-color);
|
|
555
|
+
color: var(--text-color);
|
|
556
|
+
padding: 6px 12px;
|
|
557
|
+
border-radius: 4px;
|
|
558
|
+
cursor: pointer;
|
|
559
|
+
font-size: 0.9rem;
|
|
560
|
+
transition: all 0.2s;
|
|
561
|
+
}
|
|
562
|
+
.filter_button:hover {
|
|
563
|
+
background-color: color-mix(
|
|
564
|
+
in srgb,
|
|
565
|
+
var(--primary-color) 15%,
|
|
566
|
+
transparent
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
</style>
|
|
570
|
+
<div class="container">
|
|
571
|
+
<aside></aside>
|
|
572
|
+
<main>
|
|
573
|
+
<div class="header_row">
|
|
574
|
+
<h1 style="margin: 0">Route inspector</h1>
|
|
575
|
+
<button
|
|
576
|
+
class="note_button_open"
|
|
577
|
+
aria-label="Show information"
|
|
578
|
+
title="About HTTP Methods"
|
|
579
|
+
>
|
|
580
|
+
<svg
|
|
581
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
582
|
+
viewBox="0 0 24 24"
|
|
583
|
+
width="24"
|
|
584
|
+
height="24"
|
|
585
|
+
fill="none"
|
|
586
|
+
stroke="currentColor"
|
|
587
|
+
stroke-width="2"
|
|
588
|
+
stroke-linecap="round"
|
|
589
|
+
stroke-linejoin="round"
|
|
590
|
+
>
|
|
591
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
592
|
+
<line x1="12" y1="16" x2="12" y2="12"></line>
|
|
593
|
+
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
|
594
|
+
</svg>
|
|
595
|
+
</button>
|
|
596
|
+
<div style="flex: 1"></div>
|
|
597
|
+
<div class="theme-toggle">
|
|
598
|
+
<button id="theme-toggle-btn" aria-label="Toggle dark/light mode">
|
|
599
|
+
<svg
|
|
600
|
+
class="sun-icon"
|
|
601
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
602
|
+
height="24"
|
|
603
|
+
viewBox="0 -960 960 960"
|
|
604
|
+
width="24"
|
|
605
|
+
>
|
|
606
|
+
<path
|
|
607
|
+
d="M480-360q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35Zm0 80q-83 0-141.5-58.5T280-480q0-83 58.5-141.5T480-680q83 0 141.5 58.5T680-480q0 83-58.5 141.5T480-280ZM80-440q0 17 11.5 28.5T120-400h80q17 0 28.5-11.5T240-440q0-17-11.5-28.5T200-480h-80q-17 0-28.5 11.5T80-440Zm680 0q0 17 11.5 28.5T800-400h80q17 0 28.5-11.5T920-440q0-17-11.5-28.5T880-480h-80q-17 0-28.5 11.5T760-440ZM440-760q0 17 11.5 28.5T480-720q17 0 28.5-11.5T520-760v-80q0-17-11.5-28.5T480-880q-17 0-28.5 11.5T440-840v80Zm0 680q0 17 11.5 28.5T480-40q17 0 28.5-11.5T520-80v-80q0-17-11.5-28.5T480-200q-17 0-28.5 11.5T440-160v80ZM226-678l56 56q12 12 28.5 12t28.5-12q12-12 12-28.5T339-679l-56-56q-12-12-28.5-12T226-735q-12 12-12 28.5t12 28.5Zm452 452 56 56q12 12 28.5 12t28.5-12q12-12 12-28.5T791-227l-56-56q-12-12-28.5-12T678-283q-12 12-12 28.5t12 28.5ZM791-678q12 12 28.5 12t28.5-12q12-12 12-28.5T848-735l-56-56q-12-12-28.5-12T735-791q-12 12-12 28.5t12 28.5l56 56Zm-508 452q12 12 28.5 12t28.5-12q12-12 12-28.5T340-283l-56-56q-12-12-28.5-12T227-339q-12 12-12 28.5t12 28.5l56 56Z"
|
|
608
|
+
/>
|
|
609
|
+
</svg>
|
|
610
|
+
<svg
|
|
611
|
+
class="moon-icon"
|
|
612
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
613
|
+
height="24"
|
|
614
|
+
viewBox="0 -960 960 960"
|
|
615
|
+
width="24"
|
|
616
|
+
>
|
|
617
|
+
<path
|
|
618
|
+
d="M480-120q-150 0-255-105T120-480q0-150 105-255t255-105q8 0 17 .5t23 1.5q-36 32-56 79t-20 99q0 83 58.5 141.5T643-460q52 0 99-20t79-56q1 14 1.5 23t.5 17q0 150-105 255T480-120Zm0-80q88 0 158-48.5T740-375q-20 5-40 8t-40 3q-123 0-209.5-86.5T364-660q0-20 3-40t8-40q-78 32-126.5 102T200-480q0 116 82 198t198 82Zm-10-270Z"
|
|
619
|
+
/>
|
|
620
|
+
</svg>
|
|
621
|
+
</button>
|
|
622
|
+
</div>
|
|
623
|
+
</div>
|
|
624
|
+
<dialog class="node_dialog">
|
|
625
|
+
<button class="note_button_close" aria-label="Close">
|
|
626
|
+
<svg
|
|
627
|
+
width="24"
|
|
628
|
+
height="24"
|
|
629
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
630
|
+
viewBox="0 0 24 24"
|
|
631
|
+
fill="none"
|
|
632
|
+
stroke="currentColor"
|
|
633
|
+
stroke-width="2"
|
|
634
|
+
stroke-linecap="round"
|
|
635
|
+
stroke-linejoin="round"
|
|
636
|
+
>
|
|
637
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
638
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
639
|
+
</svg>
|
|
640
|
+
</button>
|
|
641
|
+
<div class="note_title">Explanation about this page</div>
|
|
642
|
+
<div class="note_text">
|
|
643
|
+
<p>This page displays all routes configured in your server.</p>
|
|
644
|
+
<p>
|
|
645
|
+
Every GET route also automatically supports HEAD requests without
|
|
646
|
+
additional configuration.
|
|
647
|
+
</p>
|
|
648
|
+
</div>
|
|
649
|
+
</dialog>
|
|
650
|
+
<script>
|
|
651
|
+
const noteDialog = document.querySelector(".node_dialog");
|
|
652
|
+
const noteButtonOpen = document.querySelector(".note_button_open");
|
|
653
|
+
const nodeButtonClose = document.querySelector(".note_button_close");
|
|
654
|
+
|
|
655
|
+
noteDialog.onclick = (e) => {
|
|
656
|
+
if (e.target === noteDialog) {
|
|
657
|
+
noteDialog.close();
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
nodeButtonClose.onclick = () => {
|
|
661
|
+
noteDialog.close();
|
|
662
|
+
};
|
|
663
|
+
noteButtonOpen.onclick = () => {
|
|
664
|
+
noteDialog.showModal();
|
|
665
|
+
};
|
|
666
|
+
</script>
|
|
667
|
+
<div id="app"></div>
|
|
668
|
+
</main>
|
|
669
|
+
</div>
|
|
670
|
+
<script>
|
|
671
|
+
// Theme toggling functionality
|
|
672
|
+
const toggleThemeButton = document.getElementById("theme-toggle-btn");
|
|
673
|
+
|
|
674
|
+
// Check for saved theme preference or use system preference
|
|
675
|
+
const savedTheme = localStorage.getItem("theme");
|
|
676
|
+
const prefersDark = window.matchMedia(
|
|
677
|
+
"(prefers-color-scheme: dark)",
|
|
678
|
+
).matches;
|
|
679
|
+
|
|
680
|
+
// Apply theme based on saved preference or system preference
|
|
681
|
+
if (savedTheme === "dark" || (savedTheme === null && prefersDark)) {
|
|
682
|
+
document.documentElement.setAttribute("data-theme", "dark");
|
|
683
|
+
} else {
|
|
684
|
+
document.documentElement.setAttribute("data-theme", "light");
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Toggle theme when button is clicked
|
|
688
|
+
toggleThemeButton.addEventListener("click", () => {
|
|
689
|
+
if (document.documentElement.getAttribute("data-theme") === "dark") {
|
|
690
|
+
document.documentElement.setAttribute("data-theme", "light");
|
|
691
|
+
localStorage.setItem("theme", "light");
|
|
692
|
+
} else {
|
|
693
|
+
document.documentElement.setAttribute("data-theme", "dark");
|
|
694
|
+
localStorage.setItem("theme", "dark");
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
</script>
|
|
698
|
+
<script type="module">
|
|
699
|
+
// https://preactjs.com/guide/v10/no-build-workflows/
|
|
700
|
+
import { render } from "preact";
|
|
701
|
+
import { html } from "htm/preact";
|
|
702
|
+
import { signal, computed } from "@preact/signals";
|
|
703
|
+
import { useState, useRef, useCallback } from "preact/hooks";
|
|
704
|
+
|
|
705
|
+
const routesLoadingSignal = signal(false);
|
|
706
|
+
const routeLoadingErrorSignal = signal(null);
|
|
707
|
+
const routesSignal = signal([]);
|
|
708
|
+
const NO_PACKAGE_NAME = "unknown";
|
|
709
|
+
|
|
710
|
+
const repartitionsSignal = computed(() => {
|
|
711
|
+
const routes = routesSignal.value;
|
|
712
|
+
const methodRepartition = {};
|
|
713
|
+
const packageRepartition = {};
|
|
714
|
+
for (const route of routes) {
|
|
715
|
+
const method = route.method;
|
|
716
|
+
if (!methodRepartition[method]) {
|
|
717
|
+
methodRepartition[method] = 0;
|
|
718
|
+
}
|
|
719
|
+
methodRepartition[method]++;
|
|
720
|
+
|
|
721
|
+
const packageName = route.meta.packageName || NO_PACKAGE_NAME;
|
|
722
|
+
if (!packageRepartition[packageName]) {
|
|
723
|
+
packageRepartition[packageName] = 0;
|
|
724
|
+
}
|
|
725
|
+
packageRepartition[packageName]++;
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
methodRepartition,
|
|
729
|
+
packageRepartition,
|
|
730
|
+
};
|
|
731
|
+
});
|
|
732
|
+
const methodRepartitionSignal = computed(() => {
|
|
733
|
+
return repartitionsSignal.value.methodRepartition;
|
|
734
|
+
});
|
|
735
|
+
const packageRepartitionSignal = computed(() => {
|
|
736
|
+
return repartitionsSignal.value.packageRepartition;
|
|
737
|
+
});
|
|
738
|
+
const availableMethodNamesSignal = computed(() => {
|
|
739
|
+
return Object.keys(methodRepartitionSignal.value);
|
|
740
|
+
});
|
|
741
|
+
const availablePackageNamesSignal = computed(() => {
|
|
742
|
+
return Object.keys(packageRepartitionSignal.value);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
const loadRoutes = async () => {
|
|
746
|
+
routesLoadingSignal.value = true;
|
|
747
|
+
routeLoadingErrorSignal.value = null;
|
|
748
|
+
try {
|
|
749
|
+
const response = await fetch("/.internal/routes.json");
|
|
750
|
+
const routes = await response.json();
|
|
751
|
+
routesLoadingSignal.value = false;
|
|
752
|
+
routesSignal.value = routes;
|
|
753
|
+
} catch (e) {
|
|
754
|
+
routesLoadingSignal.value = false;
|
|
755
|
+
routeLoadingErrorSignal.value = e;
|
|
756
|
+
throw e;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
loadRoutes();
|
|
760
|
+
|
|
761
|
+
const Sidebar = () => {
|
|
762
|
+
const routes = routesSignal.value;
|
|
763
|
+
if (routes.length === 0) {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
return html` <${FiltersPanel} routes=${routes} /> `;
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
const App = () => {
|
|
770
|
+
return html` <${RouteList} /> `;
|
|
771
|
+
};
|
|
772
|
+
const ProgressBar = ({ width = "100%", height = 6, loading }) => {
|
|
773
|
+
if (!loading) {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
return html`
|
|
777
|
+
<div
|
|
778
|
+
class="progress-bar"
|
|
779
|
+
style=${{
|
|
780
|
+
width: typeof Number(width) === "number" ? `${width}px` : width,
|
|
781
|
+
height:
|
|
782
|
+
typeof Number(height) === "number" ? `${height}px` : height,
|
|
783
|
+
}}
|
|
784
|
+
>
|
|
785
|
+
<div class="progress-container progress-indeterminate">
|
|
786
|
+
<div class="progress-bar-long"></div>
|
|
787
|
+
<div class="progress-bar-short"></div>
|
|
788
|
+
</div>
|
|
789
|
+
</div>
|
|
790
|
+
`;
|
|
791
|
+
};
|
|
792
|
+
const RouteList = () => {
|
|
793
|
+
const routes = routesSignal.value;
|
|
794
|
+
const loading = routesLoadingSignal.value;
|
|
795
|
+
const routeLoadingError = routeLoadingErrorSignal.value;
|
|
796
|
+
|
|
797
|
+
if (loading) {
|
|
798
|
+
return html`<${ProgressBar} width="150" loading=${true} />`;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (routeLoadingError) {
|
|
802
|
+
return html`<p>
|
|
803
|
+
Error loading routes: ${routeLoadingError.message}
|
|
804
|
+
</p>`;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (!routes || routes.length === 0) {
|
|
808
|
+
return html`<p>No routes configured.</p>`;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return html`<div class="route_container">
|
|
812
|
+
<div class="route_list">
|
|
813
|
+
${routes.map((route) => html`<${RouteDetails} ...${route} />`)}
|
|
814
|
+
</div>
|
|
815
|
+
</div>`;
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
const documentUrlSignal = signal(window.location.href);
|
|
819
|
+
window.addEventListener("popstate", async (popstateEvent) => {
|
|
820
|
+
documentUrlSignal.value = popstateEvent.target.location.href;
|
|
821
|
+
});
|
|
822
|
+
const goTo = (url, { replace } = {}) => {
|
|
823
|
+
if (replace) {
|
|
824
|
+
window.history.replaceState(null, null, url);
|
|
825
|
+
documentUrlSignal.value = url;
|
|
826
|
+
} else {
|
|
827
|
+
window.history.pushState(null, null, url);
|
|
828
|
+
documentUrlSignal.value = url;
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
goTo(window.location.href, { replace: true });
|
|
832
|
+
|
|
833
|
+
let useUrlStringParam;
|
|
834
|
+
url_string_param: {
|
|
835
|
+
useUrlStringParam = (name, { availableValues } = {}) => {
|
|
836
|
+
const urlStringParamSignal = signalForUrlParam(name, (url) => {
|
|
837
|
+
return new URL(url).searchParams.get(name);
|
|
838
|
+
});
|
|
839
|
+
const urlStringParam = urlStringParamSignal.value;
|
|
840
|
+
let value;
|
|
841
|
+
if (urlStringParam) {
|
|
842
|
+
value = urlStringParam;
|
|
843
|
+
if (availableValues && !availableValues.includes(value)) {
|
|
844
|
+
value = "";
|
|
845
|
+
}
|
|
846
|
+
} else {
|
|
847
|
+
value = "";
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const set = useCallback(
|
|
851
|
+
(newValue) => {
|
|
852
|
+
const urlObject = new URL(documentUrlSignal.peek());
|
|
853
|
+
const { searchParams } = urlObject;
|
|
854
|
+
if (!newValue) {
|
|
855
|
+
searchParams.delete(name);
|
|
856
|
+
const urlWithoutStringParam = urlObject.href;
|
|
857
|
+
if (urlWithoutStringParam !== window.location.href) {
|
|
858
|
+
goTo(urlWithoutStringParam);
|
|
859
|
+
}
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (availableValues && !availableValues.includes(newValue)) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
searchParams.set(name, newValue);
|
|
866
|
+
const urlWithStringParam = urlObject.href;
|
|
867
|
+
if (urlWithStringParam !== window.location.href) {
|
|
868
|
+
goTo(urlWithStringParam);
|
|
869
|
+
}
|
|
870
|
+
return;
|
|
871
|
+
},
|
|
872
|
+
[name],
|
|
873
|
+
);
|
|
874
|
+
return [value, set];
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
const urlParamSignalMap = new Map();
|
|
878
|
+
// can be called multiple times by hooks
|
|
879
|
+
const signalForUrlParam = (name, getter) => {
|
|
880
|
+
const existingSignal = urlParamSignalMap.get(name);
|
|
881
|
+
if (existingSignal) {
|
|
882
|
+
return existingSignal;
|
|
883
|
+
}
|
|
884
|
+
const signalForFirstCall = computed(() => {
|
|
885
|
+
const url = documentUrlSignal.value;
|
|
886
|
+
const valueInUrl = getter(url);
|
|
887
|
+
return valueInUrl;
|
|
888
|
+
});
|
|
889
|
+
urlParamSignalMap.set(name, signalForFirstCall);
|
|
890
|
+
return signalForFirstCall;
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const useMethodUrlStringParam = () => {
|
|
895
|
+
const availableMethodNames = availableMethodNamesSignal.value;
|
|
896
|
+
return useUrlStringParam("method", {
|
|
897
|
+
availableValues: availableMethodNames,
|
|
898
|
+
});
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
const usePackageUrlStringParam = () => {
|
|
902
|
+
const availablePackageNames = availablePackageNamesSignal.value;
|
|
903
|
+
return useUrlStringParam("package", {
|
|
904
|
+
availableValues: availablePackageNames,
|
|
905
|
+
});
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
let FiltersPanel;
|
|
909
|
+
filters: {
|
|
910
|
+
FiltersPanel = ({ routes }) => {
|
|
911
|
+
return html`
|
|
912
|
+
<div class="filter_container">
|
|
913
|
+
<div class="filter_list">
|
|
914
|
+
<div class="filter_item">
|
|
915
|
+
<${MethodFilter} routes=${routes} />
|
|
916
|
+
</div>
|
|
917
|
+
<div class="filter_item">
|
|
918
|
+
<${PackageFilter} routes=${routes} />
|
|
919
|
+
</div>
|
|
920
|
+
</div>
|
|
921
|
+
</div>
|
|
922
|
+
`;
|
|
923
|
+
};
|
|
924
|
+
const MethodFilter = ({ routes }) => {
|
|
925
|
+
const availableMethodNames = availableMethodNamesSignal.value;
|
|
926
|
+
const availableMethodRepartition = methodRepartitionSignal.value;
|
|
927
|
+
const [selectedMethod, setSelectedMethod] = useMethodUrlStringParam();
|
|
928
|
+
|
|
929
|
+
// "all" border image is a combination of available method colors
|
|
930
|
+
const getMethodColor = (methodName) => {
|
|
931
|
+
const bodyStyle = window.getComputedStyle(document.body);
|
|
932
|
+
const methodColor = bodyStyle.getPropertyValue(
|
|
933
|
+
`--method-${methodName.toLowerCase()}-color`,
|
|
934
|
+
);
|
|
935
|
+
return methodColor;
|
|
936
|
+
};
|
|
937
|
+
const percentagePerColor = Math.floor(
|
|
938
|
+
100 / (availableMethodNames.length - 1),
|
|
939
|
+
);
|
|
940
|
+
const gradientSteps = [];
|
|
941
|
+
let progress = 0;
|
|
942
|
+
let index = 0;
|
|
943
|
+
for (const methodName of availableMethodNames) {
|
|
944
|
+
if (index === availableMethodNames.length - 1) {
|
|
945
|
+
progress = 100;
|
|
946
|
+
}
|
|
947
|
+
gradientSteps.push(`${getMethodColor(methodName)} ${progress}%`);
|
|
948
|
+
progress += percentagePerColor;
|
|
949
|
+
index++;
|
|
950
|
+
}
|
|
951
|
+
const gradient = `linear-gradient(to bottom, ${gradientSteps.join(", ")})`;
|
|
952
|
+
|
|
953
|
+
const options = [
|
|
954
|
+
{
|
|
955
|
+
value: "",
|
|
956
|
+
text: html`
|
|
957
|
+
<span
|
|
958
|
+
class="filter_text"
|
|
959
|
+
data-method="all"
|
|
960
|
+
style=${{
|
|
961
|
+
borderImageSource: gradient,
|
|
962
|
+
}}
|
|
963
|
+
>all</span
|
|
964
|
+
>
|
|
965
|
+
<span class="filter_count">${routes.length}</span>
|
|
966
|
+
`,
|
|
967
|
+
selected: !selectedMethod,
|
|
968
|
+
},
|
|
969
|
+
...availableMethodNames.map((availableMethodName) => {
|
|
970
|
+
return {
|
|
971
|
+
value: availableMethodName,
|
|
972
|
+
text: html`
|
|
973
|
+
<span class="filter_text" data-method="${availableMethodName}"
|
|
974
|
+
>${availableMethodName}
|
|
975
|
+
</span>
|
|
976
|
+
<span class="filter_count">
|
|
977
|
+
${availableMethodRepartition[availableMethodName]}
|
|
978
|
+
</span>
|
|
979
|
+
`,
|
|
980
|
+
selected: availableMethodName === selectedMethod,
|
|
981
|
+
};
|
|
982
|
+
}),
|
|
983
|
+
];
|
|
984
|
+
|
|
985
|
+
return html`
|
|
986
|
+
<${FilterSelector}
|
|
987
|
+
label="Methods"
|
|
988
|
+
options=${options}
|
|
989
|
+
onChange=${(methodToSelect) => {
|
|
990
|
+
setSelectedMethod(methodToSelect);
|
|
991
|
+
}}
|
|
992
|
+
/>
|
|
993
|
+
`;
|
|
994
|
+
};
|
|
995
|
+
const PackageFilter = ({ routes }) => {
|
|
996
|
+
const availablePackageNames = availablePackageNamesSignal.value;
|
|
997
|
+
const packageRepartition = packageRepartitionSignal.value;
|
|
998
|
+
const [selectedPackageName, setSelectedPackageName] =
|
|
999
|
+
usePackageUrlStringParam();
|
|
1000
|
+
|
|
1001
|
+
const options = [
|
|
1002
|
+
{
|
|
1003
|
+
value: "",
|
|
1004
|
+
text: html`<span>all</span>
|
|
1005
|
+
<span class="filter_count">${routes.length}</span>`,
|
|
1006
|
+
selected: !selectedPackageName,
|
|
1007
|
+
},
|
|
1008
|
+
...availablePackageNames.map((availablePackageName) => {
|
|
1009
|
+
return {
|
|
1010
|
+
value: availablePackageName,
|
|
1011
|
+
text: html` <span
|
|
1012
|
+
class="filter_text"
|
|
1013
|
+
title="${availablePackageName}"
|
|
1014
|
+
>${availablePackageName}</span
|
|
1015
|
+
>
|
|
1016
|
+
<span class="filter_count"
|
|
1017
|
+
>${packageRepartition[availablePackageName]}</span
|
|
1018
|
+
>`,
|
|
1019
|
+
selected: availablePackageName === selectedPackageName,
|
|
1020
|
+
};
|
|
1021
|
+
}),
|
|
1022
|
+
];
|
|
1023
|
+
return html`
|
|
1024
|
+
<${FilterSelector}
|
|
1025
|
+
label="Packages"
|
|
1026
|
+
options=${options}
|
|
1027
|
+
onChange=${(packageNameToSelect) => {
|
|
1028
|
+
setSelectedPackageName(packageNameToSelect);
|
|
1029
|
+
}}
|
|
1030
|
+
/>
|
|
1031
|
+
`;
|
|
1032
|
+
};
|
|
1033
|
+
const FilterSelector = ({
|
|
1034
|
+
label,
|
|
1035
|
+
multiple,
|
|
1036
|
+
options,
|
|
1037
|
+
onChange,
|
|
1038
|
+
actions,
|
|
1039
|
+
}) => {
|
|
1040
|
+
const formRef = useRef();
|
|
1041
|
+
|
|
1042
|
+
return html`<details open class="filter">
|
|
1043
|
+
<summary class="filter_summary">
|
|
1044
|
+
${label}
|
|
1045
|
+
<${ArrowDown} />
|
|
1046
|
+
</summary>
|
|
1047
|
+
<form ref=${formRef} class="filter_form">
|
|
1048
|
+
${options.map((option) => {
|
|
1049
|
+
return html`<label>
|
|
1050
|
+
<input
|
|
1051
|
+
type="${multiple ? "checkbox" : "radio"}"
|
|
1052
|
+
name="${label}"
|
|
1053
|
+
checked=${option.selected}
|
|
1054
|
+
value=${option.value}
|
|
1055
|
+
onChange=${multiple
|
|
1056
|
+
? () => {
|
|
1057
|
+
const formNode = formRef.current;
|
|
1058
|
+
// even if we create a checkbox list the HTML spec
|
|
1059
|
+
// return a radio node list and we have to gather the checked values
|
|
1060
|
+
const radioNodeList = formNode.elements[label];
|
|
1061
|
+
const selectedValues = [];
|
|
1062
|
+
for (const checkboxNode of Array.from(
|
|
1063
|
+
radioNodeList,
|
|
1064
|
+
)) {
|
|
1065
|
+
if (checkboxNode.checked) {
|
|
1066
|
+
selectedValues.push(checkboxNode.value);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
onChange(selectedValues);
|
|
1070
|
+
}
|
|
1071
|
+
: () => {
|
|
1072
|
+
const formNode = formRef.current;
|
|
1073
|
+
const radioNodeList = formNode.elements[label];
|
|
1074
|
+
const selectedValue = radioNodeList.value;
|
|
1075
|
+
onChange(selectedValue);
|
|
1076
|
+
}}
|
|
1077
|
+
/>
|
|
1078
|
+
${option.text}
|
|
1079
|
+
</label>`;
|
|
1080
|
+
})}
|
|
1081
|
+
</form>
|
|
1082
|
+
${
|
|
1083
|
+
actions &&
|
|
1084
|
+
html` <div class="filter_actions">
|
|
1085
|
+
<button
|
|
1086
|
+
type="button"
|
|
1087
|
+
class="filter_button"
|
|
1088
|
+
onClick=${() => {
|
|
1089
|
+
onChange(options.map((option) => option.value));
|
|
1090
|
+
}}
|
|
1091
|
+
>
|
|
1092
|
+
Select All
|
|
1093
|
+
</button>
|
|
1094
|
+
<button
|
|
1095
|
+
type="button"
|
|
1096
|
+
class="filter_button"
|
|
1097
|
+
onClick=${() => {
|
|
1098
|
+
onChange([]);
|
|
1099
|
+
}}
|
|
1100
|
+
>
|
|
1101
|
+
Clear All
|
|
1102
|
+
</button>
|
|
1103
|
+
</div>`
|
|
1104
|
+
}
|
|
1105
|
+
</div>`;
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const RouteDetails = ({
|
|
1110
|
+
meta,
|
|
1111
|
+
method,
|
|
1112
|
+
resource,
|
|
1113
|
+
description,
|
|
1114
|
+
availableMediaTypes,
|
|
1115
|
+
availableLanguages,
|
|
1116
|
+
availableVersions,
|
|
1117
|
+
availableEncodings,
|
|
1118
|
+
acceptedMediaTypes,
|
|
1119
|
+
websocket,
|
|
1120
|
+
clientCodeExample,
|
|
1121
|
+
declarationSource,
|
|
1122
|
+
}) => {
|
|
1123
|
+
const [opened, setOpened] = useState(false);
|
|
1124
|
+
const [selectedMethod] = useUrlStringParam("method");
|
|
1125
|
+
const [selectedPackageName] = usePackageUrlStringParam();
|
|
1126
|
+
|
|
1127
|
+
if (selectedMethod && method !== selectedMethod) {
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
const packageName = meta.packageName || NO_PACKAGE_NAME;
|
|
1131
|
+
if (selectedPackageName && packageName !== selectedPackageName) {
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
return html`<details
|
|
1136
|
+
class="route_details"
|
|
1137
|
+
onToggle=${(e) => {
|
|
1138
|
+
setOpened(e.newState === "open");
|
|
1139
|
+
}}
|
|
1140
|
+
data-method=${method}
|
|
1141
|
+
>
|
|
1142
|
+
<summary>
|
|
1143
|
+
<div class="route_summary_container">
|
|
1144
|
+
<span class="route_method">${method}</span>
|
|
1145
|
+
<p class="route_resource">${resource}</p>
|
|
1146
|
+
<span style="flex:1"></span>
|
|
1147
|
+
<${RouteBadge} meta=${meta} />
|
|
1148
|
+
${opened ? html`<${ArrowUp} />` : html`<${ArrowDown} />`}
|
|
1149
|
+
</div>
|
|
1150
|
+
</summary>
|
|
1151
|
+
<${RouteDetailsContent}
|
|
1152
|
+
method=${method}
|
|
1153
|
+
resource=${resource}
|
|
1154
|
+
description=${description}
|
|
1155
|
+
availableMediaTypes=${availableMediaTypes}
|
|
1156
|
+
availableLanguages=${availableLanguages}
|
|
1157
|
+
availableVersions=${availableVersions}
|
|
1158
|
+
availableEncodings=${availableEncodings}
|
|
1159
|
+
acceptedMediaTypes=${acceptedMediaTypes}
|
|
1160
|
+
websocket=${websocket}
|
|
1161
|
+
clientCodeExample=${clientCodeExample}
|
|
1162
|
+
declarationSource=${declarationSource}
|
|
1163
|
+
meta=${meta}
|
|
1164
|
+
/>
|
|
1165
|
+
</details>`;
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
const RouteBadge = ({ meta }) => {
|
|
1169
|
+
if (!meta.packageName) {
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
let text = meta.packageName;
|
|
1173
|
+
let title = `This endpoint is declared by ${meta.packageName}`;
|
|
1174
|
+
return html`<span title=${title} class="route_owner_name"
|
|
1175
|
+
>${text}</span
|
|
1176
|
+
>`;
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
const RouteDetailsContent = ({
|
|
1180
|
+
method,
|
|
1181
|
+
resource,
|
|
1182
|
+
description,
|
|
1183
|
+
availableMediaTypes,
|
|
1184
|
+
availableLanguages,
|
|
1185
|
+
availableVersions,
|
|
1186
|
+
availableEncodings,
|
|
1187
|
+
acceptedMediaTypes,
|
|
1188
|
+
websocket,
|
|
1189
|
+
clientCodeExample,
|
|
1190
|
+
declarationSource,
|
|
1191
|
+
meta,
|
|
1192
|
+
}) => {
|
|
1193
|
+
return html`<div class="route_details_content">
|
|
1194
|
+
<${RoutedeclarationSource}
|
|
1195
|
+
meta=${meta}
|
|
1196
|
+
declarationSource=${declarationSource}
|
|
1197
|
+
/>
|
|
1198
|
+
<p style="margin-top: 0"><strong>Description:</strong></p>
|
|
1199
|
+
<p class="route_description">
|
|
1200
|
+
${description || "No description available"}
|
|
1201
|
+
</p>
|
|
1202
|
+
${availableMediaTypes.length > 0 &&
|
|
1203
|
+
html`<${RouteContentNegotiationSection}
|
|
1204
|
+
type="content-type"
|
|
1205
|
+
title="Available media types"
|
|
1206
|
+
availableValues=${availableMediaTypes}
|
|
1207
|
+
/>`}
|
|
1208
|
+
${availableLanguages.length > 0 &&
|
|
1209
|
+
html`<${RouteContentNegotiationSection}
|
|
1210
|
+
type="language"
|
|
1211
|
+
title="Available languages"
|
|
1212
|
+
availableValues=${availableLanguages}
|
|
1213
|
+
/>`}
|
|
1214
|
+
${availableVersions.length > 0 &&
|
|
1215
|
+
html`<${RouteContentNegotiationSection}
|
|
1216
|
+
type="version"
|
|
1217
|
+
title="Available versions"
|
|
1218
|
+
availableValues=${availableVersions}
|
|
1219
|
+
/>`}
|
|
1220
|
+
${availableEncodings.length > 0 &&
|
|
1221
|
+
html`<${RouteContentNegotiationSection}
|
|
1222
|
+
type="encoding"
|
|
1223
|
+
title="Available encodings"
|
|
1224
|
+
availableValues=${availableEncodings}
|
|
1225
|
+
/>`}
|
|
1226
|
+
${acceptedMediaTypes.length > 0 &&
|
|
1227
|
+
html`<${RouteAcceptableContentSection}
|
|
1228
|
+
acceptedMediaTypes=${acceptedMediaTypes}
|
|
1229
|
+
/>`}
|
|
1230
|
+
<p style="margin-bottom:0; font-weight:bold">Example:</p>
|
|
1231
|
+
<${RouteCodeExample}
|
|
1232
|
+
method=${method}
|
|
1233
|
+
resource=${resource}
|
|
1234
|
+
availableMediaTypes=${availableMediaTypes}
|
|
1235
|
+
websocket=${websocket}
|
|
1236
|
+
clientCodeExample=${clientCodeExample}
|
|
1237
|
+
/>
|
|
1238
|
+
</div>`;
|
|
1239
|
+
};
|
|
1240
|
+
const RoutedeclarationSource = ({ meta, declarationSource }) => {
|
|
1241
|
+
if (!declarationSource) {
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
return html`<p style="margin-top: 0"><strong>Declaration:</strong></p>
|
|
1245
|
+
<p>
|
|
1246
|
+
<${RouteDeclarationText}
|
|
1247
|
+
ownerLink=${meta.ownerLink}
|
|
1248
|
+
declarationLink=${meta.declarationLink}
|
|
1249
|
+
/>
|
|
1250
|
+
</p>`;
|
|
1251
|
+
};
|
|
1252
|
+
const RouteDeclarationText = ({ declarationLink }) => {
|
|
1253
|
+
return html`This route is declared in${" "}
|
|
1254
|
+
<a href="${declarationLink.url}">${declarationLink.text}</a>`;
|
|
1255
|
+
};
|
|
1256
|
+
const RouteContentNegotiationSection = ({
|
|
1257
|
+
type,
|
|
1258
|
+
title,
|
|
1259
|
+
availableValues,
|
|
1260
|
+
}) => {
|
|
1261
|
+
return html`<div class="route_content_nego" data-nego-type="${type}">
|
|
1262
|
+
<p class="route_content_nego_title">${title}:</p>
|
|
1263
|
+
<ul>
|
|
1264
|
+
${availableValues.map(
|
|
1265
|
+
(availableValue) => html`<li>${availableValue}</li>`,
|
|
1266
|
+
)}
|
|
1267
|
+
</ul>
|
|
1268
|
+
</div>`;
|
|
1269
|
+
};
|
|
1270
|
+
const RouteAcceptableContentSection = ({ acceptedMediaTypes }) => {
|
|
1271
|
+
return html`<div class="route_accepted_content_type">
|
|
1272
|
+
<p><strong>Accepted content types:</strong></p>
|
|
1273
|
+
<ul>
|
|
1274
|
+
${acceptedMediaTypes.map(
|
|
1275
|
+
(acceptedMediaType) => html`<li>${acceptedMediaType}</li>`,
|
|
1276
|
+
)}
|
|
1277
|
+
</ul>
|
|
1278
|
+
</div>`;
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
const RouteCodeExample = ({
|
|
1282
|
+
method,
|
|
1283
|
+
resource,
|
|
1284
|
+
availableMediaTypes,
|
|
1285
|
+
websocket,
|
|
1286
|
+
clientCodeExample,
|
|
1287
|
+
}) => {
|
|
1288
|
+
let jsSource;
|
|
1289
|
+
if (clientCodeExample) {
|
|
1290
|
+
jsSource = clientCodeExample;
|
|
1291
|
+
} else if (websocket) {
|
|
1292
|
+
const websocketScheme =
|
|
1293
|
+
self.location.protocol === "https:" ? "wss" : "ws";
|
|
1294
|
+
const websocketOrigin = `${websocketScheme}://${self.location.host}`;
|
|
1295
|
+
jsSource = /*js */ `
|
|
1296
|
+
const websocket = new WebSocket("${websocketOrigin}${resource}");
|
|
1297
|
+
websocket.onerror = () => {
|
|
1298
|
+
console.error("Failed to connect to websocket endpoint");
|
|
1299
|
+
};
|
|
1300
|
+
websocket.onopen = () => {
|
|
1301
|
+
console.info("Connected to websocket endpoint")
|
|
1302
|
+
};
|
|
1303
|
+
websocket.onmessage = (event) => {
|
|
1304
|
+
console.log("Received message", event.data);
|
|
1305
|
+
};
|
|
1306
|
+
`;
|
|
1307
|
+
} else {
|
|
1308
|
+
let fetchLine;
|
|
1309
|
+
if (method === "GET") {
|
|
1310
|
+
fetchLine = `const response = await window.fetch("${resource}");`;
|
|
1311
|
+
} else {
|
|
1312
|
+
fetchLine = `const response = await window.fetch("${resource}", {
|
|
1313
|
+
method: "${method}",
|
|
1314
|
+
}`;
|
|
1315
|
+
}
|
|
1316
|
+
let readResponseLine;
|
|
1317
|
+
if (availableMediaTypes.includes("application/json")) {
|
|
1318
|
+
readResponseLine = `const json = await response.json();`;
|
|
1319
|
+
} else {
|
|
1320
|
+
readResponseLine = `const text = await response.text();`;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
jsSource = /*js */ `
|
|
1324
|
+
${fetchLine}
|
|
1325
|
+
${readResponseLine}
|
|
1326
|
+
`;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
return html`<code-block lang="js" data-escaped data-copy-button
|
|
1330
|
+
>${jsSource}</code-block
|
|
1331
|
+
>`;
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
const ArrowDown = () => {
|
|
1335
|
+
return html`<svg
|
|
1336
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1337
|
+
viewBox="0 -960 960 960"
|
|
1338
|
+
width="24"
|
|
1339
|
+
height="24"
|
|
1340
|
+
fill="currentColor"
|
|
1341
|
+
class="arrow_down"
|
|
1342
|
+
>
|
|
1343
|
+
<path d="M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z" />
|
|
1344
|
+
</svg>`;
|
|
1345
|
+
};
|
|
1346
|
+
const ArrowUp = () => {
|
|
1347
|
+
return html`<svg
|
|
1348
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1349
|
+
viewBox="0 -960 960 960"
|
|
1350
|
+
width="24"
|
|
1351
|
+
height="24"
|
|
1352
|
+
fill="currentColor"
|
|
1353
|
+
class="arrow_up"
|
|
1354
|
+
>
|
|
1355
|
+
<path d="M480-528 296-344l-56-56 240-240 240 240-56 56-184-184Z" />
|
|
1356
|
+
</svg>`;
|
|
1357
|
+
};
|
|
1358
|
+
|
|
1359
|
+
render(html`<${Sidebar} />`, document.querySelector("aside"));
|
|
1360
|
+
render(html`<${App} />`, document.querySelector("#app"));
|
|
1361
|
+
</script>
|
|
1362
|
+
</body>
|
|
1363
|
+
</html>
|