mockaton 10.6.5 → 10.6.6
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/package.json +1 -1
- package/src/Api.js +1 -1
- package/src/ApiCommander.js +7 -4
- package/src/ApiConstants.js +1 -1
- package/src/Dashboard.css +41 -51
- package/src/Dashboard.js +165 -432
- package/src/DashboardDom.js +83 -0
- package/src/DashboardHtml.js +2 -0
- package/src/DashboardStore.js +257 -0
- package/src/MockBroker.js +3 -3
- package/src/mockBrokersCollection.js +2 -2
package/src/Dashboard.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createElement as r, createSvgElement as s, className, restoreFocus, deferred, Defer, Fragment, useRef } from './DashboardDom.js'
|
|
2
|
+
import { AUTO_500_COMMENT, HEADER_FOR_502 } from './ApiConstants.js'
|
|
2
3
|
import { parseFilename, extractComments } from './Filename.js'
|
|
3
|
-
import {
|
|
4
|
+
import { store, dittoSplitPaths } from './DashboardStore.js'
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
const CSS = {
|
|
@@ -29,10 +30,8 @@ const CSS = {
|
|
|
29
30
|
|
|
30
31
|
chosen: null,
|
|
31
32
|
dittoDir: null,
|
|
32
|
-
empty: null,
|
|
33
33
|
leftSide: null,
|
|
34
34
|
nonDefault: null,
|
|
35
|
-
red: null,
|
|
36
35
|
rightSide: null,
|
|
37
36
|
status4xx: null,
|
|
38
37
|
|
|
@@ -56,171 +55,31 @@ const FocusGroup = {
|
|
|
56
55
|
PreviewLink: 3
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
staticBrokers: {},
|
|
64
|
-
cookies: [],
|
|
65
|
-
comments: [],
|
|
66
|
-
delay: 0,
|
|
67
|
-
|
|
68
|
-
collectProxied: false,
|
|
69
|
-
proxyFallback: '',
|
|
70
|
-
get canProxy() {
|
|
71
|
-
return Boolean(store.proxyFallback)
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
leftSideWidth: window.innerWidth / 2,
|
|
75
|
-
|
|
76
|
-
groupByMethod: initPreference('groupByMethod'),
|
|
77
|
-
toggleGroupByMethod() {
|
|
78
|
-
store.groupByMethod = !store.groupByMethod
|
|
79
|
-
togglePreference('groupByMethod', store.groupByMethod)
|
|
80
|
-
render()
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
chosenLink: {
|
|
84
|
-
method: '',
|
|
85
|
-
urlMask: ''
|
|
86
|
-
},
|
|
87
|
-
get hasChosenLink() {
|
|
88
|
-
return store.chosenLink.method
|
|
89
|
-
&& store.chosenLink.urlMask
|
|
90
|
-
},
|
|
91
|
-
setChosenLink(method, urlMask) {
|
|
92
|
-
store.chosenLink = { method, urlMask }
|
|
93
|
-
},
|
|
94
|
-
|
|
95
|
-
reset() {
|
|
96
|
-
store.setChosenLink('', '')
|
|
97
|
-
mockaton.reset()
|
|
98
|
-
.then(fetchState)
|
|
99
|
-
},
|
|
100
|
-
|
|
101
|
-
bulkSelectByComment(value) {
|
|
102
|
-
mockaton.bulkSelectByComment(value)
|
|
103
|
-
.then(fetchState)
|
|
104
|
-
},
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
setGlobalDelay(value) {
|
|
108
|
-
store.delay = value
|
|
109
|
-
mockaton.setGlobalDelay(value)
|
|
110
|
-
},
|
|
111
|
-
|
|
112
|
-
selectCookie(name) {
|
|
113
|
-
store.cookies = store.cookies.map(([n]) => [n, n === name])
|
|
114
|
-
mockaton.selectCookie(name)
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
setProxyFallback(value) {
|
|
118
|
-
store.proxyFallback = value
|
|
119
|
-
mockaton.setProxyFallback(value)
|
|
120
|
-
.then(render)
|
|
121
|
-
},
|
|
122
|
-
|
|
123
|
-
setCollectProxied(checked) {
|
|
124
|
-
store.collectProxied = checked
|
|
125
|
-
mockaton.setCollectProxied(checked)
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
brokerFor(method, urlMask) { return store.brokersByMethod[method]?.[urlMask] },
|
|
129
|
-
staticBrokerFor(route) { return store.staticBrokers[route] },
|
|
130
|
-
|
|
131
|
-
previewLink(method, urlMask) {
|
|
132
|
-
store.setChosenLink(method, urlMask)
|
|
133
|
-
renderRow(method, urlMask)
|
|
134
|
-
},
|
|
135
|
-
|
|
136
|
-
selectFile(file) {
|
|
137
|
-
mockaton.select(file).then(async response => {
|
|
138
|
-
const { method, urlMask } = parseFilename(file)
|
|
139
|
-
store.brokerFor(method, urlMask).currentMock = await response.json()
|
|
140
|
-
store.setChosenLink(method, urlMask)
|
|
141
|
-
renderRow(method, urlMask)
|
|
142
|
-
})
|
|
143
|
-
},
|
|
144
|
-
|
|
145
|
-
toggle500(method, urlMask) {
|
|
146
|
-
mockaton.toggle500(method, urlMask).then(async response => {
|
|
147
|
-
store.brokerFor(method, urlMask).currentMock = await response.json()
|
|
148
|
-
store.setChosenLink(method, urlMask)
|
|
149
|
-
renderRow(method, urlMask)
|
|
150
|
-
})
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
toggleProxied(method, urlMask, checked) {
|
|
154
|
-
mockaton.setRouteIsProxied(method, urlMask, checked).then(() => {
|
|
155
|
-
store.brokerFor(method, urlMask).currentMock.proxied = checked
|
|
156
|
-
store.setChosenLink(method, urlMask)
|
|
157
|
-
renderRow(method, urlMask)
|
|
158
|
-
})
|
|
159
|
-
},
|
|
160
|
-
|
|
161
|
-
setDelayed(method, urlMask, checked) {
|
|
162
|
-
mockaton.setRouteIsDelayed(method, urlMask, checked).then(() => {
|
|
163
|
-
store.brokerFor(method, urlMask).currentMock.delayed = checked
|
|
164
|
-
})
|
|
165
|
-
},
|
|
166
|
-
|
|
58
|
+
store.render = render
|
|
59
|
+
store.renderRow = renderRow
|
|
60
|
+
store.setupPatchCallbacks(parseError, onError)
|
|
61
|
+
store.fetchState().catch(onError)
|
|
167
62
|
|
|
168
|
-
setDelayedStatic(route, checked) {
|
|
169
|
-
store.staticBrokerFor(route).delayed = checked
|
|
170
|
-
mockaton.setStaticRouteIsDelayed(route, checked)
|
|
171
|
-
},
|
|
172
|
-
|
|
173
|
-
setStaticRouteStatus(route, status) {
|
|
174
|
-
store.staticBrokerFor(route).status = status
|
|
175
|
-
mockaton.setStaticRouteStatus(route, status)
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
fetchState()
|
|
181
63
|
initRealTimeUpdates()
|
|
182
64
|
initKeyboardNavigation()
|
|
183
65
|
|
|
184
|
-
async function fetchState() {
|
|
185
|
-
try {
|
|
186
|
-
const response = await mockaton.getState()
|
|
187
|
-
if (!response.ok)
|
|
188
|
-
throw response.status
|
|
189
|
-
Object.assign(store, await response.json())
|
|
190
|
-
render()
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
onError(error)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
66
|
function render() {
|
|
198
67
|
restoreFocus(() => document.body.replaceChildren(...App()))
|
|
199
68
|
if (store.hasChosenLink)
|
|
200
69
|
previewMock(store.chosenLink.method, store.chosenLink.urlMask)
|
|
201
70
|
}
|
|
202
71
|
|
|
203
|
-
function restoreFocus(cb) {
|
|
204
|
-
const focusQuery = selectorFor(document.activeElement)
|
|
205
|
-
cb()
|
|
206
|
-
if (focusQuery)
|
|
207
|
-
document.querySelector(focusQuery)?.focus()
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const r = createElement
|
|
211
|
-
const s = createSvgElement
|
|
212
72
|
const t = translation => translation[0]
|
|
213
73
|
|
|
214
74
|
const leftSideRef = useRef()
|
|
215
75
|
|
|
216
76
|
function App() {
|
|
217
|
-
const { leftSideWidth } = store
|
|
218
77
|
return [
|
|
219
78
|
Header(),
|
|
220
79
|
r('main', null,
|
|
221
80
|
r('div', {
|
|
222
81
|
ref: leftSideRef,
|
|
223
|
-
style: { width: leftSideWidth + 'px' },
|
|
82
|
+
style: { width: store.leftSideWidth + 'px' },
|
|
224
83
|
className: CSS.leftSide
|
|
225
84
|
},
|
|
226
85
|
r('table', null,
|
|
@@ -232,7 +91,6 @@ function App() {
|
|
|
232
91
|
]
|
|
233
92
|
}
|
|
234
93
|
|
|
235
|
-
|
|
236
94
|
function Header() {
|
|
237
95
|
return (
|
|
238
96
|
r('header', null,
|
|
@@ -302,7 +160,7 @@ function BulkSelector() {
|
|
|
302
160
|
r('hr'),
|
|
303
161
|
comments.map(value => r('option', { value }, value)),
|
|
304
162
|
r('hr'),
|
|
305
|
-
r('option', { value:
|
|
163
|
+
r('option', { value: AUTO_500_COMMENT }, t`Auto500`)
|
|
306
164
|
)))
|
|
307
165
|
}
|
|
308
166
|
|
|
@@ -322,11 +180,11 @@ function CookieSelector() {
|
|
|
322
180
|
r('option', { value, selected }, value)))))
|
|
323
181
|
}
|
|
324
182
|
|
|
183
|
+
|
|
325
184
|
function ProxyFallbackField() {
|
|
326
185
|
const checkboxRef = useRef()
|
|
327
186
|
function onChange() {
|
|
328
187
|
checkboxRef.current.disabled = !this.validity.valid || !this.value.trim()
|
|
329
|
-
|
|
330
188
|
if (!this.validity.valid)
|
|
331
189
|
this.reportValidity()
|
|
332
190
|
else
|
|
@@ -359,6 +217,7 @@ function SaveProxiedCheckbox(ref) {
|
|
|
359
217
|
r('span', null, t`Save Mocks`)))
|
|
360
218
|
}
|
|
361
219
|
|
|
220
|
+
|
|
362
221
|
function ResetButton() {
|
|
363
222
|
return (
|
|
364
223
|
r('button', {
|
|
@@ -367,6 +226,7 @@ function ResetButton() {
|
|
|
367
226
|
}, t`Reset`))
|
|
368
227
|
}
|
|
369
228
|
|
|
229
|
+
|
|
370
230
|
function SettingsMenuTrigger() {
|
|
371
231
|
const id = '_settings_menu_'
|
|
372
232
|
return (
|
|
@@ -381,16 +241,15 @@ function SettingsMenuTrigger() {
|
|
|
381
241
|
|
|
382
242
|
function SettingsMenu(id) {
|
|
383
243
|
const firstInputRef = useRef()
|
|
384
|
-
function onToggle(event) {
|
|
385
|
-
if (event.newState === 'open')
|
|
386
|
-
firstInputRef.current.focus()
|
|
387
|
-
}
|
|
388
244
|
return (
|
|
389
245
|
r('menu', {
|
|
390
246
|
id,
|
|
391
247
|
popover: '',
|
|
392
248
|
className: CSS.SettingsMenu,
|
|
393
|
-
onToggle
|
|
249
|
+
onToggle(event) {
|
|
250
|
+
if (event.newState === 'open')
|
|
251
|
+
firstInputRef.current.focus()
|
|
252
|
+
}
|
|
394
253
|
},
|
|
395
254
|
|
|
396
255
|
r('label', className(CSS.GroupByMethod),
|
|
@@ -415,9 +274,7 @@ function SettingsMenu(id) {
|
|
|
415
274
|
|
|
416
275
|
function MockList() {
|
|
417
276
|
if (!Object.keys(store.brokersByMethod).length)
|
|
418
|
-
return (
|
|
419
|
-
r('div', className(CSS.empty),
|
|
420
|
-
t`No mocks found`))
|
|
277
|
+
return r('div', null, t`No mocks found`)
|
|
421
278
|
|
|
422
279
|
if (store.groupByMethod)
|
|
423
280
|
return Object.keys(store.brokersByMethod).map(method => Fragment(
|
|
@@ -430,13 +287,7 @@ function MockList() {
|
|
|
430
287
|
}
|
|
431
288
|
|
|
432
289
|
function rowsFor(targetMethod) {
|
|
433
|
-
const
|
|
434
|
-
for (const [method, brokers] of Object.entries(store.brokersByMethod))
|
|
435
|
-
if (targetMethod === '*' || targetMethod === method)
|
|
436
|
-
for (const [urlMask, broker] of Object.entries(brokers))
|
|
437
|
-
rows.push({ method, urlMask, broker })
|
|
438
|
-
|
|
439
|
-
const sorted = rows.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
|
|
290
|
+
const sorted = store.brokersByMethodAsArray(targetMethod)
|
|
440
291
|
const urlMasksDittoed = dittoSplitPaths(sorted.map(r => r.urlMask))
|
|
441
292
|
return sorted.map((r, i) => ({
|
|
442
293
|
...r,
|
|
@@ -447,14 +298,26 @@ function rowsFor(targetMethod) {
|
|
|
447
298
|
function Row({ method, urlMask, urlMaskDittoed, broker }, i) {
|
|
448
299
|
const key = Row.key(method, urlMask)
|
|
449
300
|
Row.ditto.set(key, urlMaskDittoed)
|
|
301
|
+
const { proxied, delayed, file } = broker.currentMock
|
|
450
302
|
return (
|
|
451
303
|
r('tr', { key },
|
|
452
|
-
store.canProxy && r('td', null,
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
304
|
+
store.canProxy && r('td', null,
|
|
305
|
+
ProxyToggler(method, urlMask, proxied)),
|
|
306
|
+
|
|
307
|
+
r('td', null,
|
|
308
|
+
DelayRouteToggler(method, urlMask, delayed)),
|
|
309
|
+
|
|
310
|
+
r('td', null,
|
|
311
|
+
InternalServerErrorToggler(method, urlMask, parseFilename(file).status === 500)),
|
|
312
|
+
|
|
313
|
+
!store.groupByMethod && r('td', className(CSS.Method),
|
|
314
|
+
method),
|
|
315
|
+
|
|
316
|
+
r('td', null,
|
|
317
|
+
PreviewLink(method, urlMask, urlMaskDittoed, i === 0)),
|
|
318
|
+
|
|
319
|
+
r('td', null,
|
|
320
|
+
MockSelector(broker))))
|
|
458
321
|
}
|
|
459
322
|
Row.key = (method, urlMask) => method + '::' + urlMask
|
|
460
323
|
Row.ditto = new Map()
|
|
@@ -473,10 +336,10 @@ function renderRow(method, urlMask) {
|
|
|
473
336
|
})
|
|
474
337
|
|
|
475
338
|
function trFor(key) {
|
|
476
|
-
return
|
|
339
|
+
return leftSideRef.current.querySelector(`tr[key="${key}"]`)
|
|
477
340
|
}
|
|
478
341
|
function unChooseOld() {
|
|
479
|
-
return
|
|
342
|
+
return leftSideRef.current.querySelector(`td > .${CSS.chosen}`)
|
|
480
343
|
?.classList.remove(CSS.chosen)
|
|
481
344
|
}
|
|
482
345
|
}
|
|
@@ -504,30 +367,13 @@ function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
|
|
|
504
367
|
|
|
505
368
|
/** @param {ClientMockBroker} broker */
|
|
506
369
|
function MockSelector(broker) {
|
|
507
|
-
const STR_PROXIED = t`Proxied`
|
|
508
|
-
|
|
509
370
|
let selected = broker.currentMock.file
|
|
510
|
-
const
|
|
511
|
-
const files = broker.mocks.filter(item =>
|
|
512
|
-
status === 500 ||
|
|
513
|
-
!item.includes(AUTOGENERATED_500_COMMENT))
|
|
514
|
-
|
|
515
|
-
if (store.canProxy && broker.currentMock.proxied) {
|
|
516
|
-
selected = STR_PROXIED
|
|
517
|
-
files.push(selected)
|
|
518
|
-
}
|
|
371
|
+
const selectedStatus = parseFilename(selected).status
|
|
519
372
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const comments = extractComments(file)
|
|
525
|
-
const isAutogen500 = comments.includes(AUTOGENERATED_500_COMMENT)
|
|
526
|
-
return [
|
|
527
|
-
isAutogen500 ? '' : status,
|
|
528
|
-
ext === 'empty' || ext === 'unknown' ? '' : ext,
|
|
529
|
-
isAutogen500 ? t`Auto500` : comments.join(' ')
|
|
530
|
-
].filter(Boolean).join(' ')
|
|
373
|
+
const files = baseOptionsFor(broker.mocks, selectedStatus === 500)
|
|
374
|
+
if (store.canProxy && broker.currentMock.proxied) {
|
|
375
|
+
selected = t`Proxied`
|
|
376
|
+
files.push([selected, selected])
|
|
531
377
|
}
|
|
532
378
|
|
|
533
379
|
return (
|
|
@@ -538,35 +384,83 @@ function MockSelector(broker) {
|
|
|
538
384
|
disabled: files.length <= 1,
|
|
539
385
|
...className(
|
|
540
386
|
CSS.MockSelector,
|
|
541
|
-
selected !== files[0] && CSS.nonDefault,
|
|
542
|
-
|
|
543
|
-
}, files.map(file => (
|
|
387
|
+
selected !== files[0][0] && CSS.nonDefault,
|
|
388
|
+
selectedStatus >= 400 && selectedStatus < 500 && CSS.status4xx)
|
|
389
|
+
}, files.map(([file, name]) => (
|
|
544
390
|
r('option', {
|
|
545
391
|
value: file,
|
|
546
392
|
selected: file === selected
|
|
547
|
-
},
|
|
393
|
+
}, name)))))
|
|
548
394
|
}
|
|
395
|
+
function baseOptionsFor(mocks, selectedIs500) {
|
|
396
|
+
return mocks
|
|
397
|
+
.filter(f => selectedIs500 || !f.includes(AUTO_500_COMMENT))
|
|
398
|
+
.map(f => [f, nameFor(f)])
|
|
549
399
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const
|
|
554
|
-
|
|
400
|
+
function nameFor(file) {
|
|
401
|
+
const { status, ext } = parseFilename(file)
|
|
402
|
+
const comments = extractComments(file)
|
|
403
|
+
const isAutogen500 = comments.includes(AUTO_500_COMMENT)
|
|
404
|
+
return [
|
|
405
|
+
isAutogen500 ? '' : status,
|
|
406
|
+
ext === 'empty' || ext === 'unknown' ? '' : ext,
|
|
407
|
+
isAutogen500 ? t`Auto500` : comments.join(' ')
|
|
408
|
+
].filter(Boolean).join(' ')
|
|
555
409
|
}
|
|
410
|
+
}
|
|
411
|
+
baseOptionsFor.test = function () {
|
|
412
|
+
(function ignoresAutoGen500WhenUnselected() {
|
|
413
|
+
const files = baseOptionsFor([
|
|
414
|
+
`api/user${AUTO_500_COMMENT}.GET.500.empty`
|
|
415
|
+
], false)
|
|
416
|
+
console.assert(files.length === 0)
|
|
417
|
+
}());
|
|
418
|
+
|
|
419
|
+
(function keepsNonAutoGen500WhenUnselected() {
|
|
420
|
+
const files = baseOptionsFor([
|
|
421
|
+
`api/user.GET.500.txt`
|
|
422
|
+
], false)
|
|
423
|
+
console.assert(files.length === 1)
|
|
424
|
+
console.assert(files[0][1] === t`500 txt`)
|
|
425
|
+
}());
|
|
426
|
+
|
|
427
|
+
(function renamesAutoGenFileToAuto500() {
|
|
428
|
+
const files = baseOptionsFor([
|
|
429
|
+
`api/user${AUTO_500_COMMENT}.GET.500.empty`
|
|
430
|
+
], true)
|
|
431
|
+
console.assert(files.length === 1)
|
|
432
|
+
console.assert(files[0][1] === t`Auto500`)
|
|
433
|
+
}());
|
|
434
|
+
|
|
435
|
+
(function filenameHasExtensionExceptWhenEmptyOrUnknown() {
|
|
436
|
+
const files = baseOptionsFor([
|
|
437
|
+
`api/user0.GET.200.empty`,
|
|
438
|
+
`api/user1.GET.200.unknown`,
|
|
439
|
+
`api/user2.GET.200.json`,
|
|
440
|
+
`api/user3(another json).GET.200.json`,
|
|
441
|
+
], true)
|
|
442
|
+
console.assert(deepEqual(files.map(([, n]) => n), [
|
|
443
|
+
// Think about, in cases like this, the only option the user
|
|
444
|
+
// has for discerning empty and unknown is on the Previewer Title
|
|
445
|
+
'200',
|
|
446
|
+
'200',
|
|
447
|
+
'200 json',
|
|
448
|
+
'200 json (another json)',
|
|
449
|
+
]))
|
|
450
|
+
}())
|
|
451
|
+
}
|
|
452
|
+
deferred(baseOptionsFor.test)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
function DelayRouteToggler(method, urlMask, checked) {
|
|
556
456
|
return ClickDragToggler({
|
|
557
|
-
checked
|
|
558
|
-
commit,
|
|
457
|
+
checked,
|
|
458
|
+
commit(checked) { store.setDelayed(method, urlMask, checked) },
|
|
559
459
|
focusGroup: FocusGroup.DelayToggler
|
|
560
460
|
})
|
|
561
461
|
}
|
|
562
462
|
|
|
563
|
-
|
|
564
|
-
/** @param {ClientMockBroker} broker */
|
|
565
|
-
function InternalServerErrorToggler(broker) {
|
|
566
|
-
function onChange() {
|
|
567
|
-
const { method, urlMask } = parseFilename(broker.mocks[0])
|
|
568
|
-
store.toggle500(method, urlMask)
|
|
569
|
-
}
|
|
463
|
+
function InternalServerErrorToggler(method, urlMask, checked) {
|
|
570
464
|
return (
|
|
571
465
|
r('label', {
|
|
572
466
|
className: CSS.InternalServerErrorToggler,
|
|
@@ -574,19 +468,14 @@ function InternalServerErrorToggler(broker) {
|
|
|
574
468
|
},
|
|
575
469
|
r('input', {
|
|
576
470
|
type: 'checkbox',
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
471
|
+
checked,
|
|
472
|
+
onChange() { store.toggle500(method, urlMask) },
|
|
473
|
+
'data-focus-group': FocusGroup.StatusToggler
|
|
580
474
|
}),
|
|
581
475
|
r('span', null, t`500`)))
|
|
582
476
|
}
|
|
583
477
|
|
|
584
|
-
|
|
585
|
-
function ProxyToggler(broker) {
|
|
586
|
-
function onChange() {
|
|
587
|
-
const { urlMask, method } = parseFilename(broker.mocks[0])
|
|
588
|
-
store.toggleProxied(method, urlMask, this.checked)
|
|
589
|
-
}
|
|
478
|
+
function ProxyToggler(method, urlMask, checked) {
|
|
590
479
|
return (
|
|
591
480
|
r('label', {
|
|
592
481
|
className: CSS.ProxyToggler,
|
|
@@ -594,8 +483,8 @@ function ProxyToggler(broker) {
|
|
|
594
483
|
},
|
|
595
484
|
r('input', {
|
|
596
485
|
type: 'checkbox',
|
|
597
|
-
checked
|
|
598
|
-
onChange,
|
|
486
|
+
checked,
|
|
487
|
+
onChange() { store.setProxied(method, urlMask, this.checked) },
|
|
599
488
|
'data-focus-group': FocusGroup.ProxyToggler
|
|
600
489
|
}),
|
|
601
490
|
CloudIcon()))
|
|
@@ -617,34 +506,39 @@ function StaticFilesList() {
|
|
|
617
506
|
r('tr', null,
|
|
618
507
|
r('th', { colspan: (2 + Number(!groupByMethod)) + Number(canProxy) }),
|
|
619
508
|
r('th', null, t`Static GET`)),
|
|
620
|
-
Object.values(staticBrokers).map((
|
|
509
|
+
Object.values(staticBrokers).map(({ route, status, delayed }, i) =>
|
|
621
510
|
r('tr', null,
|
|
622
511
|
canProxy && r('td'),
|
|
623
|
-
r('td', null,
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
r('td', null,
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
'
|
|
631
|
-
|
|
512
|
+
r('td', null,
|
|
513
|
+
DelayStaticRouteToggler(route, delayed)),
|
|
514
|
+
|
|
515
|
+
r('td', null,
|
|
516
|
+
NotFoundToggler(route, status === 404)),
|
|
517
|
+
|
|
518
|
+
!groupByMethod && r('td', className(CSS.Method),
|
|
519
|
+
'GET'),
|
|
520
|
+
|
|
521
|
+
r('td', null,
|
|
522
|
+
r('a', {
|
|
523
|
+
href: route,
|
|
524
|
+
target: '_blank',
|
|
525
|
+
className: CSS.PreviewLink,
|
|
526
|
+
'data-focus-group': FocusGroup.PreviewLink
|
|
527
|
+
}, dp[i]))
|
|
632
528
|
))))
|
|
633
529
|
}
|
|
634
530
|
|
|
635
|
-
|
|
636
|
-
function DelayStaticRouteToggler(broker) {
|
|
531
|
+
function DelayStaticRouteToggler(route, checked) {
|
|
637
532
|
return ClickDragToggler({
|
|
638
|
-
checked
|
|
533
|
+
checked,
|
|
639
534
|
focusGroup: FocusGroup.DelayToggler,
|
|
640
535
|
commit(checked) {
|
|
641
|
-
store.setDelayedStatic(
|
|
536
|
+
store.setDelayedStatic(route, checked)
|
|
642
537
|
}
|
|
643
538
|
})
|
|
644
539
|
}
|
|
645
540
|
|
|
646
|
-
|
|
647
|
-
function NotFoundToggler(broker) {
|
|
541
|
+
function NotFoundToggler(route, checked) {
|
|
648
542
|
return (
|
|
649
543
|
r('label', {
|
|
650
544
|
className: CSS.NotFoundToggler,
|
|
@@ -652,10 +546,10 @@ function NotFoundToggler(broker) {
|
|
|
652
546
|
},
|
|
653
547
|
r('input', {
|
|
654
548
|
type: 'checkbox',
|
|
655
|
-
checked
|
|
549
|
+
checked,
|
|
656
550
|
'data-focus-group': FocusGroup.StatusToggler,
|
|
657
551
|
onChange() {
|
|
658
|
-
store.setStaticRouteStatus(
|
|
552
|
+
store.setStaticRouteStatus(route, this.checked ? 404 : 200)
|
|
659
553
|
}
|
|
660
554
|
}),
|
|
661
555
|
r('span', null, t`404`)))
|
|
@@ -770,7 +664,7 @@ function PayloadViewerTitleWhenProxied({ mime, status, statusText, gatewayIsBad
|
|
|
770
664
|
return (
|
|
771
665
|
r('span', null,
|
|
772
666
|
gatewayIsBad
|
|
773
|
-
? r('span',
|
|
667
|
+
? r('span', null, t`⛔ Fallback Backend Error` + ' ')
|
|
774
668
|
: r('span', null, t`Got` + ' '),
|
|
775
669
|
r('abbr', { title: statusText }, status),
|
|
776
670
|
' ' + mime))
|
|
@@ -799,11 +693,8 @@ async function previewMock(method, urlMask) {
|
|
|
799
693
|
})
|
|
800
694
|
clearTimeout(spinnerTimer)
|
|
801
695
|
const { proxied, file } = store.brokerFor(method, urlMask).currentMock
|
|
802
|
-
if (proxied)
|
|
803
|
-
await updatePayloadViewer(
|
|
804
|
-
else if (file)
|
|
805
|
-
await updatePayloadViewer(false, file, response)
|
|
806
|
-
else {/* e.g. selected was deleted */}
|
|
696
|
+
if (proxied || file)
|
|
697
|
+
await updatePayloadViewer(proxied, file, response)
|
|
807
698
|
}
|
|
808
699
|
catch (err) {
|
|
809
700
|
onError(err)
|
|
@@ -826,23 +717,23 @@ async function updatePayloadViewer(proxied, file, response) {
|
|
|
826
717
|
file,
|
|
827
718
|
statusText: response.statusText
|
|
828
719
|
}))
|
|
829
|
-
|
|
720
|
+
|
|
830
721
|
if (mime.startsWith('image/')) // Naively assumes GET.200
|
|
831
722
|
payloadViewerCodeRef.current.replaceChildren(
|
|
832
723
|
r('img', { src: URL.createObjectURL(await response.blob()) }))
|
|
833
724
|
else {
|
|
834
725
|
const body = await response.text() || t`/* Empty Response Body */`
|
|
835
726
|
if (mime === 'application/json')
|
|
836
|
-
payloadViewerCodeRef.current.replaceChildren(r('span', className(CSS.json),
|
|
837
|
-
else if (isXML(mime))
|
|
838
|
-
payloadViewerCodeRef.current.replaceChildren(
|
|
727
|
+
payloadViewerCodeRef.current.replaceChildren(r('span', className(CSS.json), SyntaxJSON(body)))
|
|
728
|
+
else if (isXML(mime))
|
|
729
|
+
payloadViewerCodeRef.current.replaceChildren(SyntaxXML(body))
|
|
839
730
|
else
|
|
840
731
|
payloadViewerCodeRef.current.textContent = body
|
|
841
732
|
}
|
|
842
733
|
}
|
|
843
734
|
|
|
844
735
|
function isXML(mime) {
|
|
845
|
-
return ['text/html', 'text/xml', 'application/xml'].includes(
|
|
736
|
+
return ['text/html', 'text/xml', 'application/xml'].some(m => mime.includes(m))
|
|
846
737
|
|| /application\/.*\+xml/.test(mime)
|
|
847
738
|
}
|
|
848
739
|
|
|
@@ -901,6 +792,7 @@ function SettingsIcon() {
|
|
|
901
792
|
s('path', { d: 'M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6' })))
|
|
902
793
|
}
|
|
903
794
|
|
|
795
|
+
|
|
904
796
|
/**
|
|
905
797
|
* # Long polls UI sync version
|
|
906
798
|
* The version increments when a mock file is added, removed, or renamed.
|
|
@@ -921,14 +813,14 @@ function initRealTimeUpdates() {
|
|
|
921
813
|
|
|
922
814
|
async function poll() {
|
|
923
815
|
try {
|
|
924
|
-
const response = await
|
|
816
|
+
const response = await store.getSyncVersion(oldSyncVersion, controller.signal)
|
|
925
817
|
if (response.ok) {
|
|
926
818
|
const syncVersion = await response.json()
|
|
927
819
|
const skipUpdate = oldSyncVersion === -1
|
|
928
820
|
if (oldSyncVersion !== syncVersion) { // because it could be < or >
|
|
929
821
|
oldSyncVersion = syncVersion
|
|
930
822
|
if (!skipUpdate)
|
|
931
|
-
|
|
823
|
+
store.fetchState().catch(onError)
|
|
932
824
|
}
|
|
933
825
|
poll()
|
|
934
826
|
}
|
|
@@ -942,6 +834,7 @@ function initRealTimeUpdates() {
|
|
|
942
834
|
}
|
|
943
835
|
}
|
|
944
836
|
|
|
837
|
+
|
|
945
838
|
function initKeyboardNavigation() {
|
|
946
839
|
addEventListener('keydown', onKeyDown)
|
|
947
840
|
|
|
@@ -977,8 +870,8 @@ function initKeyboardNavigation() {
|
|
|
977
870
|
}
|
|
978
871
|
|
|
979
872
|
function allInFocusGroup(focusGroup) {
|
|
980
|
-
return Array.from(
|
|
981
|
-
`
|
|
873
|
+
return Array.from(leftSideRef.current.querySelectorAll(
|
|
874
|
+
`tr > td [data-focus-group="${focusGroup}"]:is(input, a)`))
|
|
982
875
|
}
|
|
983
876
|
|
|
984
877
|
function circularAdjacent(step = 1, arr, pivot) {
|
|
@@ -987,172 +880,7 @@ function initKeyboardNavigation() {
|
|
|
987
880
|
}
|
|
988
881
|
|
|
989
882
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
function className(...args) {
|
|
993
|
-
return {
|
|
994
|
-
className: args.filter(Boolean).join(' ')
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
function createElement(tag, props, ...children) {
|
|
999
|
-
const node = document.createElement(tag)
|
|
1000
|
-
for (const [k, v] of Object.entries(props || {}))
|
|
1001
|
-
if (k === 'ref') v.current = node
|
|
1002
|
-
else if (k === 'style') Object.assign(node.style, v)
|
|
1003
|
-
else if (k.startsWith('on')) node.addEventListener(k.slice(2).toLowerCase(), ...[v].flat())
|
|
1004
|
-
else if (k in node) node[k] = v
|
|
1005
|
-
else node.setAttribute(k, v)
|
|
1006
|
-
node.append(...children.flat().filter(Boolean))
|
|
1007
|
-
return node
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
function createSvgElement(tagName, props, ...children) {
|
|
1011
|
-
const elem = document.createElementNS('http://www.w3.org/2000/svg', tagName)
|
|
1012
|
-
for (const [k, v] of Object.entries(props))
|
|
1013
|
-
elem.setAttribute(k, v)
|
|
1014
|
-
elem.append(...children.flat().filter(Boolean))
|
|
1015
|
-
return elem
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
function useRef() {
|
|
1019
|
-
return { current: null }
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
function Fragment(...args) {
|
|
1023
|
-
const frag = new DocumentFragment()
|
|
1024
|
-
for (const arg of args)
|
|
1025
|
-
if (Array.isArray(arg))
|
|
1026
|
-
frag.append(...arg)
|
|
1027
|
-
else
|
|
1028
|
-
frag.appendChild(arg)
|
|
1029
|
-
return frag
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
function Defer(cb) {
|
|
1033
|
-
const placeholder = document.createComment('')
|
|
1034
|
-
deferred(() => placeholder.replaceWith(cb()))
|
|
1035
|
-
return placeholder
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
function deferred(cb) {
|
|
1039
|
-
return window.requestIdleCallback
|
|
1040
|
-
? requestIdleCallback(cb)
|
|
1041
|
-
: setTimeout(cb, 100) // Safari
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
function selectorFor(elem) {
|
|
1045
|
-
if (!(elem instanceof Element))
|
|
1046
|
-
return
|
|
1047
|
-
|
|
1048
|
-
const path = []
|
|
1049
|
-
while (elem) {
|
|
1050
|
-
let qualifier = ''
|
|
1051
|
-
if (elem.hasAttribute('key'))
|
|
1052
|
-
qualifier = `[key="${elem.getAttribute('key')}"]`
|
|
1053
|
-
else {
|
|
1054
|
-
let i = 0
|
|
1055
|
-
let sib = elem
|
|
1056
|
-
while ((sib = sib.previousElementSibling))
|
|
1057
|
-
if (sib.tagName === elem.tagName)
|
|
1058
|
-
i++
|
|
1059
|
-
if (i)
|
|
1060
|
-
qualifier = `:nth-of-type(${i + 1})`
|
|
1061
|
-
}
|
|
1062
|
-
path.push(elem.tagName + qualifier)
|
|
1063
|
-
elem = elem.parentElement
|
|
1064
|
-
}
|
|
1065
|
-
return path.reverse().join('>')
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
// When false, the URL will be updated with param=false
|
|
1070
|
-
function initPreference(param) {
|
|
1071
|
-
const qs = new URLSearchParams(location.search)
|
|
1072
|
-
if (!qs.has(param)) {
|
|
1073
|
-
const group = localStorage.getItem(param) !== 'false'
|
|
1074
|
-
if (!group) {
|
|
1075
|
-
const url = new URL(location.href)
|
|
1076
|
-
url.searchParams.set(param, false)
|
|
1077
|
-
history.replaceState(null, '', url)
|
|
1078
|
-
}
|
|
1079
|
-
return group
|
|
1080
|
-
}
|
|
1081
|
-
return qs.get(param) !== 'false'
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
// When false, the URL and localStorage will have param=false
|
|
1085
|
-
function togglePreference(param, nextVal) {
|
|
1086
|
-
if (nextVal)
|
|
1087
|
-
localStorage.removeItem(param)
|
|
1088
|
-
else
|
|
1089
|
-
localStorage.setItem(param, nextVal)
|
|
1090
|
-
|
|
1091
|
-
const url = new URL(location.href)
|
|
1092
|
-
if (nextVal)
|
|
1093
|
-
url.searchParams.delete(param)
|
|
1094
|
-
else
|
|
1095
|
-
url.searchParams.set(param, false)
|
|
1096
|
-
history.replaceState(null, '', url)
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
/**
|
|
1101
|
-
* Think of this as a way of printing a directory tree in which
|
|
1102
|
-
* the repeated folder paths are kept but styled differently.
|
|
1103
|
-
* @param {string[]} paths - sorted
|
|
1104
|
-
*/
|
|
1105
|
-
function dittoSplitPaths(paths) {
|
|
1106
|
-
const result = [['', paths[0]]]
|
|
1107
|
-
const pathsInParts = paths.map(p => p.split('/').filter(Boolean))
|
|
1108
|
-
|
|
1109
|
-
for (let i = 1; i < paths.length; i++) {
|
|
1110
|
-
const prevParts = pathsInParts[i - 1]
|
|
1111
|
-
const currParts = pathsInParts[i]
|
|
1112
|
-
|
|
1113
|
-
let j = 0
|
|
1114
|
-
while (
|
|
1115
|
-
j < currParts.length &&
|
|
1116
|
-
j < prevParts.length &&
|
|
1117
|
-
currParts[j] === prevParts[j])
|
|
1118
|
-
j++
|
|
1119
|
-
|
|
1120
|
-
if (!j) // no common dirs
|
|
1121
|
-
result.push(['', paths[i]])
|
|
1122
|
-
else {
|
|
1123
|
-
const ditto = '/' + currParts.slice(0, j).join('/') + '/'
|
|
1124
|
-
result.push([ditto, paths[i].slice(ditto.length)])
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
return result
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
dittoSplitPaths.test = function () {
|
|
1131
|
-
const input = [
|
|
1132
|
-
'/api/user',
|
|
1133
|
-
'/api/user/avatar',
|
|
1134
|
-
'/api/user/friends',
|
|
1135
|
-
'/api/vid',
|
|
1136
|
-
'/api/video/id',
|
|
1137
|
-
'/api/video/stats',
|
|
1138
|
-
'/v2/foo',
|
|
1139
|
-
'/v2/foo/bar'
|
|
1140
|
-
]
|
|
1141
|
-
const expected = [
|
|
1142
|
-
['', '/api/user'],
|
|
1143
|
-
['/api/user/', 'avatar'],
|
|
1144
|
-
['/api/user/', 'friends'],
|
|
1145
|
-
['/api/', 'vid'],
|
|
1146
|
-
['/api/', 'video/id'],
|
|
1147
|
-
['/api/video/', 'stats'],
|
|
1148
|
-
['', '/v2/foo'],
|
|
1149
|
-
['/v2/foo/', 'bar']
|
|
1150
|
-
]
|
|
1151
|
-
console.assert(JSON.stringify(dittoSplitPaths(input)) === JSON.stringify(expected))
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
function syntaxJSON(json) {
|
|
883
|
+
function SyntaxJSON(json) {
|
|
1156
884
|
const MAX_NODES = 50_000
|
|
1157
885
|
let nNodes = 0
|
|
1158
886
|
const frag = new DocumentFragment()
|
|
@@ -1172,8 +900,8 @@ function syntaxJSON(json) {
|
|
|
1172
900
|
|
|
1173
901
|
let match
|
|
1174
902
|
let lastIndex = 0
|
|
1175
|
-
|
|
1176
|
-
while ((match =
|
|
903
|
+
SyntaxJSON.regex.lastIndex = 0 // resets regex
|
|
904
|
+
while ((match = SyntaxJSON.regex.exec(json)) !== null) {
|
|
1177
905
|
if (nNodes > MAX_NODES)
|
|
1178
906
|
break
|
|
1179
907
|
|
|
@@ -1195,11 +923,11 @@ function syntaxJSON(json) {
|
|
|
1195
923
|
text(json.slice(lastIndex))
|
|
1196
924
|
return frag
|
|
1197
925
|
}
|
|
1198
|
-
|
|
926
|
+
SyntaxJSON.regex = /("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|([{}\[\],:\s]+)|\S+/g
|
|
1199
927
|
// Capture group order: [string, optional colon, punc]
|
|
1200
928
|
|
|
1201
929
|
|
|
1202
|
-
function
|
|
930
|
+
function SyntaxXML(xml) {
|
|
1203
931
|
const MAX_NODES = 50_000
|
|
1204
932
|
let nNodes = 0
|
|
1205
933
|
const frag = new DocumentFragment()
|
|
@@ -1219,8 +947,8 @@ function syntaxXML(xml) {
|
|
|
1219
947
|
|
|
1220
948
|
let match
|
|
1221
949
|
let lastIndex = 0
|
|
1222
|
-
|
|
1223
|
-
while ((match =
|
|
950
|
+
SyntaxXML.regex.lastIndex = 0
|
|
951
|
+
while ((match = SyntaxXML.regex.exec(xml)) !== null) {
|
|
1224
952
|
if (nNodes > MAX_NODES)
|
|
1225
953
|
break
|
|
1226
954
|
|
|
@@ -1238,6 +966,11 @@ function syntaxXML(xml) {
|
|
|
1238
966
|
frag.normalize()
|
|
1239
967
|
return frag
|
|
1240
968
|
}
|
|
1241
|
-
|
|
969
|
+
SyntaxXML.regex = /(<\/?|\/?>|\?>)|(?<=<\??\/?)([A-Za-z_:][\w:.-]*)|([A-Za-z_:][\w:.-]*)(?==)|("(?:[^"\\]|\\.)*")/g
|
|
1242
970
|
// Capture groups order: [tagPunc, tagName, attrName, attrVal]
|
|
1243
971
|
|
|
972
|
+
|
|
973
|
+
function deepEqual(a, b) {
|
|
974
|
+
return JSON.stringify(a) === JSON.stringify(b)
|
|
975
|
+
}
|
|
976
|
+
|