mockaton 13.3.3 → 13.3.5
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/client/app-header.js +3 -2
- package/src/client/app-payload-viewer.js +9 -9
- package/src/client/app-store.js +25 -52
- package/src/client/app-store.test.js +1 -25
- package/src/client/app.css +14 -11
- package/src/client/app.js +65 -83
- package/src/client/dir-tree.js +82 -0
- package/src/client/dir-tree.test.js +106 -0
- package/src/client/dom-utils-test.js +74 -0
- package/src/client/dom-utils.js +71 -4
- package/src/client/graphics.js +18 -28
- package/src/server/Mockaton.js +2 -0
- package/src/server/mockBrokersCollection.js +1 -1
- package/src/client/dirStructure.js +0 -83
- package/src/client/dirStructure.test.js +0 -86
- package/src/client/dom-utils.test.js +0 -76
package/package.json
CHANGED
package/src/client/app-header.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { createElement as r, t,
|
|
1
|
+
import { createElement as r, t, extractClassNames } from './dom-utils.js'
|
|
2
2
|
import { Logo, HelpIcon } from './graphics.js'
|
|
3
3
|
import { store } from './app-store.js'
|
|
4
4
|
|
|
5
5
|
import CSS from './app.css' with { type: 'css' }
|
|
6
|
-
defineClassNames(CSS)
|
|
7
6
|
|
|
8
7
|
|
|
8
|
+
Object.assign(CSS, extractClassNames(CSS))
|
|
9
|
+
|
|
9
10
|
export function Header() {
|
|
10
11
|
return (
|
|
11
12
|
r('header', null,
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { createElement as r, t,
|
|
1
|
+
import { createElement as r, t, extractClassNames } from './dom-utils.js'
|
|
2
2
|
import { HEADER_502 } from './ApiConstants.js'
|
|
3
3
|
import { parseFilename } from './Filename.js'
|
|
4
4
|
import { store } from './app-store.js'
|
|
5
5
|
|
|
6
6
|
import CSS from './app.css' with { type: 'css' }
|
|
7
|
-
defineClassNames(CSS)
|
|
8
7
|
|
|
9
8
|
|
|
9
|
+
Object.assign(CSS, extractClassNames(CSS))
|
|
10
|
+
|
|
10
11
|
const titleRef = {}
|
|
11
12
|
const codeRef = {}
|
|
12
13
|
|
|
@@ -93,8 +94,6 @@ export async function previewMock() {
|
|
|
93
94
|
|
|
94
95
|
|
|
95
96
|
async function updatePayloadViewer(proxied, file, response) {
|
|
96
|
-
const mime = response.headers.get('content-type') || ''
|
|
97
|
-
|
|
98
97
|
titleRef.elem.replaceChildren(proxied
|
|
99
98
|
? PayloadViewerTitleWhenProxied(response)
|
|
100
99
|
: PayloadViewerTitle(file, response.statusText))
|
|
@@ -104,6 +103,12 @@ async function updatePayloadViewer(proxied, file, response) {
|
|
|
104
103
|
return
|
|
105
104
|
}
|
|
106
105
|
|
|
106
|
+
async function bodyAsText() {
|
|
107
|
+
return (await response.text()) || t`/* Empty Response Body */`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const mime = response.headers.get('content-type') || ''
|
|
111
|
+
|
|
107
112
|
if (mime.startsWith('image/'))
|
|
108
113
|
codeRef.elem.replaceChildren(r('img', {
|
|
109
114
|
src: URL.createObjectURL(await response.blob())
|
|
@@ -140,11 +145,6 @@ async function updatePayloadViewer(proxied, file, response) {
|
|
|
140
145
|
href: URL.createObjectURL(await response.blob()),
|
|
141
146
|
download: store.chosenLink.urlMask
|
|
142
147
|
}, t`Download`))
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
async function bodyAsText() {
|
|
146
|
-
return (await response.text()) || t`/* Empty Response Body */`
|
|
147
|
-
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
|
package/src/client/app-store.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Commander } from './ApiCommander.js'
|
|
2
|
+
import { dittoSplitPaths, groupByFolder } from './dir-tree.js'
|
|
2
3
|
import { parseFilename, extractComments } from './Filename.js'
|
|
4
|
+
import { QueryParamBool, LocalStorageSet } from './dom-utils.js'
|
|
3
5
|
import { EXT_UNKNOWN_MIME, EXT_EMPTY } from './ApiConstants.js'
|
|
4
6
|
|
|
5
7
|
|
|
@@ -22,25 +24,21 @@ export const store = {
|
|
|
22
24
|
collectProxied: false,
|
|
23
25
|
proxyFallback: '',
|
|
24
26
|
showProxyField: null,
|
|
25
|
-
get canProxy() {
|
|
26
|
-
return Boolean(store.proxyFallback)
|
|
27
|
-
},
|
|
27
|
+
get canProxy() { return Boolean(store.proxyFallback) },
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
_groupByMethod: new QueryParamBool('groupByMethod'),
|
|
30
|
+
get groupByMethod() { return store._groupByMethod.value },
|
|
30
31
|
toggleGroupByMethod() {
|
|
31
|
-
store.
|
|
32
|
-
togglePreference('groupByMethod', store.groupByMethod)
|
|
32
|
+
store._groupByMethod.toggle()
|
|
33
33
|
store.render()
|
|
34
34
|
},
|
|
35
35
|
|
|
36
|
-
collapsedFolders: new
|
|
36
|
+
collapsedFolders: new LocalStorageSet('collapsedFolders'),
|
|
37
37
|
setFolderCollapsed(folder, collapsed) {
|
|
38
38
|
if (collapsed)
|
|
39
39
|
store.collapsedFolders.add(folder)
|
|
40
40
|
else
|
|
41
41
|
store.collapsedFolders.delete(folder)
|
|
42
|
-
|
|
43
|
-
globalThis.localStorage?.setItem('collapsedFolders', JSON.stringify([...store.collapsedFolders]))
|
|
44
42
|
},
|
|
45
43
|
|
|
46
44
|
chosenLink: { method: '', urlMask: '' },
|
|
@@ -123,10 +121,6 @@ export const store = {
|
|
|
123
121
|
|
|
124
122
|
_dittoCache: new Map(),
|
|
125
123
|
|
|
126
|
-
brokerFor(method, urlMask) {
|
|
127
|
-
return store.brokersByMethod[method]?.[urlMask]
|
|
128
|
-
},
|
|
129
|
-
|
|
130
124
|
brokerAsRow(method, urlMask) {
|
|
131
125
|
const b = store.brokerFor(method, urlMask)
|
|
132
126
|
const r = new BrokerRowModel(b, store.canProxy)
|
|
@@ -134,13 +128,15 @@ export const store = {
|
|
|
134
128
|
return r
|
|
135
129
|
},
|
|
136
130
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
131
|
+
brokerFor(method, urlMask) {
|
|
132
|
+
return store.brokersByMethod[method]?.[urlMask]
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
folderGroupsByMethod(method) {
|
|
136
|
+
return groupByFolder(store._brokersAsRowsByMethod(method))
|
|
141
137
|
},
|
|
142
138
|
|
|
143
|
-
|
|
139
|
+
_brokersAsRowsByMethod(method) {
|
|
144
140
|
const rows = store._brokersAsArray(method)
|
|
145
141
|
.map(b => new BrokerRowModel(b, store.canProxy))
|
|
146
142
|
.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
|
|
@@ -200,27 +196,31 @@ export const store = {
|
|
|
200
196
|
store._request(() => api.setRouteIsDelayed(method, urlMask, checked), async response => {
|
|
201
197
|
store._setBroker(await response.json())
|
|
202
198
|
})
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
_setBroker(broker) {
|
|
202
|
+
const { method, urlMask } = parseFilename(broker.file)
|
|
203
|
+
store.brokersByMethod[method] ??= {}
|
|
204
|
+
store.brokersByMethod[method][urlMask] = broker
|
|
203
205
|
}
|
|
204
206
|
}
|
|
205
207
|
|
|
206
|
-
|
|
207
|
-
|
|
208
208
|
// When false, the URL will be updated with param=false
|
|
209
209
|
function initPreference(param) {
|
|
210
210
|
const qs = new URLSearchParams(globalThis.location?.search)
|
|
211
211
|
if (!qs.has(param)) {
|
|
212
|
-
const group = globalThis.localStorage?.getItem(param) !== '
|
|
212
|
+
const group = globalThis.localStorage?.getItem(param) !== '0'
|
|
213
213
|
if (!group) {
|
|
214
214
|
const url = new URL(globalThis.location?.href)
|
|
215
|
-
url.searchParams.set(param,
|
|
215
|
+
url.searchParams.set(param, '0')
|
|
216
216
|
history.replaceState(null, '', url)
|
|
217
217
|
}
|
|
218
218
|
return group
|
|
219
219
|
}
|
|
220
|
-
return qs.get(param) !== '
|
|
220
|
+
return qs.get(param) !== '0'
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
// When false, the URL and localStorage will have param=
|
|
223
|
+
// When false, the URL and localStorage will have param='0'
|
|
224
224
|
function togglePreference(param, nextVal) {
|
|
225
225
|
if (nextVal)
|
|
226
226
|
globalThis.localStorage?.removeItem(param)
|
|
@@ -231,39 +231,12 @@ function togglePreference(param, nextVal) {
|
|
|
231
231
|
if (nextVal)
|
|
232
232
|
url.searchParams.delete(param)
|
|
233
233
|
else
|
|
234
|
-
url.searchParams.set(param,
|
|
234
|
+
url.searchParams.set(param, '0')
|
|
235
235
|
history.replaceState(null, '', url)
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
|
|
239
239
|
|
|
240
|
-
/**
|
|
241
|
-
* Think of this as a way of printing a directory tree in which
|
|
242
|
-
* the repeated folder paths are kept but styled differently.
|
|
243
|
-
* @param {string[]} paths - sorted
|
|
244
|
-
*/
|
|
245
|
-
export function dittoSplitPaths(paths) {
|
|
246
|
-
const pParts = paths.map(p => p.split('/').filter(Boolean))
|
|
247
|
-
return paths.map((p, i) => {
|
|
248
|
-
if (i === 0)
|
|
249
|
-
return ['', p]
|
|
250
|
-
|
|
251
|
-
const prev = pParts[i - 1]
|
|
252
|
-
const curr = pParts[i]
|
|
253
|
-
const min = Math.min(curr.length, prev.length)
|
|
254
|
-
let j = 0
|
|
255
|
-
while (j < min && curr[j] === prev[j])
|
|
256
|
-
j++
|
|
257
|
-
|
|
258
|
-
if (!j) // no common dirs
|
|
259
|
-
return ['', p]
|
|
260
|
-
|
|
261
|
-
const ditto = '/' + curr.slice(0, j).join('/') + '/'
|
|
262
|
-
return [ditto, p.slice(ditto.length)]
|
|
263
|
-
})
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
|
|
267
240
|
export class BrokerRowModel {
|
|
268
241
|
opts = /** @type {[key:string, label:string, selected:boolean][]} */ []
|
|
269
242
|
isNew = false
|
|
@@ -1,31 +1,7 @@
|
|
|
1
1
|
import { test } from 'node:test'
|
|
2
2
|
import { deepEqual } from 'node:assert/strict'
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
test('dittoSplitPaths', () => {
|
|
8
|
-
const input = [
|
|
9
|
-
'/api/user',
|
|
10
|
-
'/api/user/avatar',
|
|
11
|
-
'/api/user/friends',
|
|
12
|
-
'/api/vid',
|
|
13
|
-
'/api/video/id',
|
|
14
|
-
'/api/video/stats',
|
|
15
|
-
'/v2/foo',
|
|
16
|
-
'/v2/foo/bar'
|
|
17
|
-
]
|
|
18
|
-
deepEqual(dittoSplitPaths(input), [
|
|
19
|
-
['', '/api/user'],
|
|
20
|
-
['/api/user/', 'avatar'],
|
|
21
|
-
['/api/user/', 'friends'],
|
|
22
|
-
['/api/', 'vid'],
|
|
23
|
-
['/api/', 'video/id'],
|
|
24
|
-
['/api/video/', 'stats'],
|
|
25
|
-
['', '/v2/foo'],
|
|
26
|
-
['/v2/foo/', 'bar']
|
|
27
|
-
])
|
|
28
|
-
})
|
|
4
|
+
import { BrokerRowModel, t } from './app-store.js'
|
|
29
5
|
|
|
30
6
|
|
|
31
7
|
test('BrokerRowModel', () => {
|
package/src/client/app.css
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
--colorBgHeader: light-dark(#f2f2f3, #141414);
|
|
7
7
|
--colorBgHeaderField: light-dark(#fff, #222);
|
|
8
8
|
|
|
9
|
-
--colorBorder: light-dark(#e0e0e0, #
|
|
9
|
+
--colorBorder: light-dark(#e0e0e0, #2c2c2c);
|
|
10
10
|
--colorBorderActive: light-dark(#c8c8c8, #3c3c3c);
|
|
11
11
|
|
|
12
12
|
--colorLabel: light-dark(#555, #aaa);
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
--colorHover: light-dark(#dbedff, #062d59);
|
|
17
17
|
|
|
18
18
|
--colorRed: light-dark(#da0f00, #f41606);
|
|
19
|
-
--colorPink: light-dark(#ed206a, #
|
|
19
|
+
--colorPink: light-dark(#ed206a, #ff2e7a);
|
|
20
20
|
--colorPurple: light-dark(#9b71e8, #ae81ff);
|
|
21
21
|
--colorGreen: light-dark(#388e3c, #a6e22e);
|
|
22
22
|
--colorBg4xx: light-dark(#ffedd1, #68554a);
|
|
@@ -44,6 +44,8 @@ body {
|
|
|
44
44
|
padding: 0;
|
|
45
45
|
border: 0;
|
|
46
46
|
margin: 0;
|
|
47
|
+
letter-spacing: -0.374px;
|
|
48
|
+
line-height: 1.2;
|
|
47
49
|
font-family: inherit;
|
|
48
50
|
font-size: 100%;
|
|
49
51
|
scrollbar-width: thin;
|
|
@@ -119,13 +121,8 @@ header {
|
|
|
119
121
|
align-self: end;
|
|
120
122
|
margin-right: 22px;
|
|
121
123
|
margin-bottom: 3px;
|
|
122
|
-
opacity: 94%;
|
|
123
124
|
transition: opacity 240ms ease-in-out;
|
|
124
125
|
|
|
125
|
-
&:hover {
|
|
126
|
-
opacity: 1;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
126
|
svg {
|
|
130
127
|
width: 120px;
|
|
131
128
|
pointer-events: none;
|
|
@@ -188,16 +185,22 @@ header {
|
|
|
188
185
|
}
|
|
189
186
|
|
|
190
187
|
.HelpLink {
|
|
191
|
-
width:
|
|
192
|
-
height:
|
|
188
|
+
width: 22px;
|
|
189
|
+
height: 22px;
|
|
193
190
|
flex-shrink: 0;
|
|
194
191
|
align-self: end;
|
|
192
|
+
margin-bottom: 3px;
|
|
195
193
|
margin-left: auto;
|
|
196
194
|
border-radius: 50%;
|
|
197
|
-
fill: var(--
|
|
195
|
+
fill: var(--colorBgHeader);
|
|
196
|
+
background: var(--colorLabel);
|
|
198
197
|
|
|
199
198
|
&:hover {
|
|
200
|
-
|
|
199
|
+
background: var(--colorAccent);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
svg {
|
|
203
|
+
transform: scale(.7);
|
|
201
204
|
}
|
|
202
205
|
}
|
|
203
206
|
}
|
package/src/client/app.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { createElement as r, t,
|
|
1
|
+
import { createElement as r, t, restoreFocus, Fragment, classNames, extractClassNames } from './dom-utils.js'
|
|
2
2
|
|
|
3
3
|
import { store } from './app-store.js'
|
|
4
4
|
import { API } from './ApiConstants.js'
|
|
5
5
|
import { Header } from './app-header.js'
|
|
6
|
-
import { dirStructure } from './dirStructure.js'
|
|
7
6
|
import { PayloadViewer, previewMock } from './app-payload-viewer.js'
|
|
8
7
|
import { TimerIcon, CloudIcon, ChevronDownIcon } from './graphics.js'
|
|
9
8
|
|
|
10
9
|
import CSS from './app.css' with { type: 'css' }
|
|
11
10
|
document.adoptedStyleSheets.push(CSS)
|
|
12
|
-
defineClassNames(CSS)
|
|
13
11
|
|
|
12
|
+
Object.assign(CSS, extractClassNames(CSS))
|
|
14
13
|
|
|
15
14
|
store.onError = onError
|
|
16
15
|
store.render = render
|
|
@@ -22,36 +21,36 @@ initKeyboardNavigation()
|
|
|
22
21
|
let mounted = false
|
|
23
22
|
function render() {
|
|
24
23
|
restoreFocus(() => document.body.replaceChildren(App()))
|
|
25
|
-
if (store.hasChosenLink)
|
|
26
|
-
|
|
24
|
+
if (store.hasChosenLink) previewMock()
|
|
25
|
+
if (!mounted) LeftSide.$('a')?.focus()
|
|
27
26
|
mounted = true
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
const leftSideRef = {}
|
|
32
|
-
|
|
33
29
|
function App() {
|
|
34
|
-
return Fragment(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
function Main() {
|
|
39
|
-
return (
|
|
30
|
+
return Fragment(
|
|
31
|
+
Header(),
|
|
40
32
|
r('main', null,
|
|
41
|
-
|
|
42
|
-
ref: leftSideRef,
|
|
43
|
-
style: { width: leftSideRef.width },
|
|
44
|
-
className: CSS.leftSide
|
|
45
|
-
},
|
|
46
|
-
r('div', { className: CSS.SubToolbar },
|
|
47
|
-
GroupByMethod(),
|
|
48
|
-
BulkSelector()),
|
|
49
|
-
r('div', { className: CSS.Table }, MockList())),
|
|
33
|
+
LeftSide(),
|
|
50
34
|
r('div', { className: CSS.rightSide },
|
|
51
|
-
Resizer(
|
|
35
|
+
Resizer(LeftSide.ref),
|
|
52
36
|
PayloadViewer())))
|
|
53
37
|
}
|
|
54
38
|
|
|
39
|
+
function LeftSide() {
|
|
40
|
+
return r('div', {
|
|
41
|
+
ref: LeftSide.ref,
|
|
42
|
+
style: { width: LeftSide.ref.width },
|
|
43
|
+
className: CSS.leftSide
|
|
44
|
+
},
|
|
45
|
+
r('div', { className: CSS.SubToolbar },
|
|
46
|
+
GroupByMethod(),
|
|
47
|
+
BulkSelector()),
|
|
48
|
+
r('div', { className: CSS.Table }, MockList()))
|
|
49
|
+
}
|
|
50
|
+
LeftSide.ref = { width: undefined }
|
|
51
|
+
LeftSide.$ = selector => LeftSide.ref.elem.querySelector(selector)
|
|
52
|
+
LeftSide.$$ = selector => LeftSide.ref.elem.querySelectorAll(selector)
|
|
53
|
+
|
|
55
54
|
|
|
56
55
|
function GroupByMethod() {
|
|
57
56
|
return (
|
|
@@ -91,7 +90,6 @@ function BulkSelector() {
|
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
|
|
94
|
-
|
|
95
93
|
function MockList() {
|
|
96
94
|
if (!Object.keys(store.brokersByMethod).length)
|
|
97
95
|
return r('div', null, t`No mocks found`)
|
|
@@ -101,14 +99,14 @@ function MockList() {
|
|
|
101
99
|
r('div', {
|
|
102
100
|
className: classNames(CSS.TableHeading, store.canProxy && CSS.canProxy)
|
|
103
101
|
}, method),
|
|
104
|
-
FolderGroups(store.
|
|
102
|
+
FolderGroups(store.folderGroupsByMethod(method))))
|
|
105
103
|
|
|
106
|
-
return FolderGroups(store.
|
|
104
|
+
return FolderGroups(store.folderGroupsByMethod('*'))
|
|
107
105
|
}
|
|
108
106
|
|
|
109
|
-
function FolderGroups(
|
|
107
|
+
function FolderGroups(brokersTree) {
|
|
110
108
|
const res = []
|
|
111
|
-
for (const b of
|
|
109
|
+
for (const b of brokersTree) {
|
|
112
110
|
if (!b.children.length)
|
|
113
111
|
res.push(Row(b))
|
|
114
112
|
else
|
|
@@ -151,25 +149,46 @@ function Row(row) {
|
|
|
151
149
|
key: row.key,
|
|
152
150
|
className: classNames(CSS.TableRow, mounted && row.isNew && CSS.animIn)
|
|
153
151
|
},
|
|
154
|
-
store.canProxy && ProxyToggler(method, urlMask, row.proxied),
|
|
155
152
|
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
store.canProxy && ClickDragToggler({
|
|
154
|
+
className: CSS.ProxyToggler,
|
|
155
|
+
title: t`Proxy Toggler`,
|
|
156
|
+
body: CloudIcon(),
|
|
157
|
+
checked: row.proxied,
|
|
158
158
|
commit(checked) {
|
|
159
|
-
store.
|
|
160
|
-
}
|
|
159
|
+
store.setProxied(method, urlMask, checked)
|
|
160
|
+
}
|
|
161
161
|
}),
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
checked:
|
|
168
|
-
commit() {
|
|
169
|
-
store.
|
|
163
|
+
ClickDragToggler({
|
|
164
|
+
className: CSS.DelayToggler,
|
|
165
|
+
title: t`Delay`,
|
|
166
|
+
body: TimerIcon(),
|
|
167
|
+
checked: row.delayed,
|
|
168
|
+
commit(checked) {
|
|
169
|
+
store.setDelayed(method, urlMask, checked)
|
|
170
170
|
}
|
|
171
171
|
}),
|
|
172
172
|
|
|
173
|
+
ClickDragToggler(
|
|
174
|
+
row.isStatic
|
|
175
|
+
? {
|
|
176
|
+
className: CSS.StatusCodeToggler,
|
|
177
|
+
title: t`Not Found`,
|
|
178
|
+
body: t`404`,
|
|
179
|
+
disabled: row.opts.length === 1 && row.status === 404,
|
|
180
|
+
checked: !row.proxied && row.status === 404,
|
|
181
|
+
commit() { store.toggleStatus(method, urlMask, 404) }
|
|
182
|
+
}
|
|
183
|
+
: {
|
|
184
|
+
className: CSS.StatusCodeToggler,
|
|
185
|
+
title: t`Internal Server Error`,
|
|
186
|
+
body: t`500`,
|
|
187
|
+
disabled: row.opts.length === 1 && row.status === 500,
|
|
188
|
+
checked: !row.proxied && row.status === 500,
|
|
189
|
+
commit() { store.toggleStatus(method, urlMask, 500) }
|
|
190
|
+
}),
|
|
191
|
+
|
|
173
192
|
!store.groupByMethod && r('span', { className: CSS.Method }, method),
|
|
174
193
|
|
|
175
194
|
PreviewLink(method, urlMask, row.urlMaskDittoed),
|
|
@@ -177,15 +196,16 @@ function Row(row) {
|
|
|
177
196
|
MockSelector(row)))
|
|
178
197
|
}
|
|
179
198
|
|
|
199
|
+
|
|
180
200
|
function renderRow(method, urlMask) {
|
|
181
201
|
unChooseOld()
|
|
182
202
|
const row = store.brokerAsRow(method, urlMask)
|
|
183
|
-
const tr =
|
|
203
|
+
const tr = LeftSide.$(`.${CSS.TableRow}[key="${row.key}"]`)
|
|
184
204
|
mergeTableRow(tr, Row(row))
|
|
185
205
|
previewMock()
|
|
186
206
|
|
|
187
207
|
function unChooseOld() {
|
|
188
|
-
return
|
|
208
|
+
return LeftSide.$(`a.${CSS.chosen}`)
|
|
189
209
|
?.classList.remove(CSS.chosen)
|
|
190
210
|
}
|
|
191
211
|
|
|
@@ -216,7 +236,6 @@ function renderRow(method, urlMask) {
|
|
|
216
236
|
}
|
|
217
237
|
|
|
218
238
|
|
|
219
|
-
|
|
220
239
|
function PreviewLink(method, urlMask, urlMaskDittoed) {
|
|
221
240
|
function onClick(event) {
|
|
222
241
|
event.preventDefault()
|
|
@@ -259,42 +278,6 @@ function MockSelector(row) {
|
|
|
259
278
|
}
|
|
260
279
|
|
|
261
280
|
|
|
262
|
-
function ProxyToggler(method, urlMask, checked) {
|
|
263
|
-
return ClickDragToggler({
|
|
264
|
-
className: CSS.ProxyToggler,
|
|
265
|
-
title: t`Proxy Toggler`,
|
|
266
|
-
body: CloudIcon(),
|
|
267
|
-
checked,
|
|
268
|
-
commit(checked) {
|
|
269
|
-
store.setProxied(method, urlMask, checked)
|
|
270
|
-
}
|
|
271
|
-
})
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
function StatusCodeToggler({ title, body, commit, checked, disabled }) {
|
|
277
|
-
return ClickDragToggler({
|
|
278
|
-
title,
|
|
279
|
-
disabled,
|
|
280
|
-
className: CSS.StatusCodeToggler,
|
|
281
|
-
commit,
|
|
282
|
-
checked,
|
|
283
|
-
body
|
|
284
|
-
})
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function DelayToggler({ checked, commit, className }) {
|
|
288
|
-
return ClickDragToggler({
|
|
289
|
-
checked,
|
|
290
|
-
commit,
|
|
291
|
-
className: classNames(CSS.DelayToggler, className),
|
|
292
|
-
canClickDrag: true,
|
|
293
|
-
title: t`Delay`,
|
|
294
|
-
body: TimerIcon()
|
|
295
|
-
})
|
|
296
|
-
}
|
|
297
|
-
|
|
298
281
|
function ClickDragToggler({ checked, commit, className, title, body }) {
|
|
299
282
|
function onPointerEnter(event) {
|
|
300
283
|
if (event.buttons === 1)
|
|
@@ -317,7 +300,7 @@ function ClickDragToggler({ checked, commit, className, title, body }) {
|
|
|
317
300
|
return
|
|
318
301
|
|
|
319
302
|
// Uncheck all other in the column.
|
|
320
|
-
for (const elem of
|
|
303
|
+
for (const elem of LeftSide.$$(selector))
|
|
321
304
|
if (elem !== this && elem.checked && !elem.disabled) {
|
|
322
305
|
elem.checked = false
|
|
323
306
|
elem.dispatchEvent(new Event('change'))
|
|
@@ -500,7 +483,6 @@ function columnSelectors() {
|
|
|
500
483
|
]
|
|
501
484
|
}
|
|
502
485
|
|
|
503
|
-
|
|
504
486
|
function initKeyboardNavigation() {
|
|
505
487
|
const rowSelectors = [
|
|
506
488
|
...columnSelectors(),
|
|
@@ -515,7 +497,7 @@ function initKeyboardNavigation() {
|
|
|
515
497
|
const sel = selectorForColumnOf(pivot)
|
|
516
498
|
if (sel) {
|
|
517
499
|
const offset = key === 'ArrowDown' ? +1 : -1
|
|
518
|
-
const siblings =
|
|
500
|
+
const siblings = LeftSide.$$(sel)
|
|
519
501
|
circularAdjacent(offset, siblings, pivot).focus()
|
|
520
502
|
}
|
|
521
503
|
break
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Think of this as a way of printing a directory tree in which
|
|
3
|
+
* the repeated folder paths are kept but styled differently.
|
|
4
|
+
* @param {string[]} paths - sorted
|
|
5
|
+
*/
|
|
6
|
+
export function dittoSplitPaths(paths) {
|
|
7
|
+
const pParts = paths.map(p => p.split('/').filter(Boolean))
|
|
8
|
+
return paths.map((p, i) => {
|
|
9
|
+
if (i === 0)
|
|
10
|
+
return ['', p]
|
|
11
|
+
|
|
12
|
+
const prev = pParts[i - 1]
|
|
13
|
+
const curr = pParts[i]
|
|
14
|
+
const min = Math.min(curr.length, prev.length)
|
|
15
|
+
let j = 0
|
|
16
|
+
while (j < min && curr[j] === prev[j])
|
|
17
|
+
j++
|
|
18
|
+
|
|
19
|
+
if (!j) // no common dirs
|
|
20
|
+
return ['', p]
|
|
21
|
+
|
|
22
|
+
const ditto = '/' + curr.slice(0, j).join('/') + '/'
|
|
23
|
+
return [ditto, p.slice(ditto.length)]
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {Partial<BrokerRowModel>[]} brokers
|
|
31
|
+
* @returns {Partial<BrokerRowModel>[]}
|
|
32
|
+
*/
|
|
33
|
+
export function groupByFolder(brokers) {
|
|
34
|
+
return dfs(trie(brokers))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function trie(brokers) {
|
|
38
|
+
const root = new TrieNode()
|
|
39
|
+
for (const b of brokers) {
|
|
40
|
+
let node = root
|
|
41
|
+
for (const seg of b.urlMask.split('/')) { // TODO it should ignore query string
|
|
42
|
+
const segNode = node.getChild(seg) || new TrieNode()
|
|
43
|
+
node.addChild(seg, segNode)
|
|
44
|
+
node = segNode
|
|
45
|
+
}
|
|
46
|
+
node.brokers.push(b)
|
|
47
|
+
}
|
|
48
|
+
return root
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class TrieNode {
|
|
52
|
+
#children
|
|
53
|
+
constructor() {
|
|
54
|
+
this.brokers = []
|
|
55
|
+
this.#children = new Map()
|
|
56
|
+
}
|
|
57
|
+
addChild(k, v) { this.#children.set(k, v) }
|
|
58
|
+
getChild(k) { return this.#children.get(k) }
|
|
59
|
+
getChildren() { return this.#children.values() }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @param {TrieNode} node */
|
|
63
|
+
function dfs(node) {
|
|
64
|
+
const childBrokers = []
|
|
65
|
+
for (const tnc of node.getChildren())
|
|
66
|
+
childBrokers.push(...dfs(tnc))
|
|
67
|
+
|
|
68
|
+
const brokers = node.brokers.length
|
|
69
|
+
? [node.brokers[0], ...childBrokers, ...node.brokers.slice(1)]
|
|
70
|
+
: childBrokers
|
|
71
|
+
|
|
72
|
+
if (!brokers.length)
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
const [b0, ...rest] = brokers
|
|
76
|
+
if (node.brokers.length || !b0.children.length) {
|
|
77
|
+
b0.children.push(...rest)
|
|
78
|
+
return [b0]
|
|
79
|
+
}
|
|
80
|
+
return brokers
|
|
81
|
+
}
|
|
82
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import { deepEqual } from 'node:assert/strict'
|
|
3
|
+
import { groupByFolder, dittoSplitPaths } from './dir-tree.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
test('dittoSplitPaths', () => {
|
|
7
|
+
const input = [
|
|
8
|
+
'/api/user',
|
|
9
|
+
'/api/user/avatar',
|
|
10
|
+
'/api/user/friends',
|
|
11
|
+
'/api/vid',
|
|
12
|
+
'/api/video/id',
|
|
13
|
+
'/api/video/stats',
|
|
14
|
+
'/v2/foo',
|
|
15
|
+
'/v2/foo/bar'
|
|
16
|
+
]
|
|
17
|
+
deepEqual(dittoSplitPaths(input), [
|
|
18
|
+
['', '/api/user'],
|
|
19
|
+
['/api/user/', 'avatar'],
|
|
20
|
+
['/api/user/', 'friends'],
|
|
21
|
+
['/api/', 'vid'],
|
|
22
|
+
['/api/', 'video/id'],
|
|
23
|
+
['/api/video/', 'stats'],
|
|
24
|
+
['', '/v2/foo'],
|
|
25
|
+
['/v2/foo/', 'bar']
|
|
26
|
+
])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
test('dirStructure', () => {
|
|
31
|
+
const input = [
|
|
32
|
+
{ children: [], method: 'GET', urlMask: '/api/user' },
|
|
33
|
+
{ children: [], method: 'GET', urlMask: '/api/user/avatar' },
|
|
34
|
+
{ children: [], method: 'GET', urlMask: '/api/video/[id]' },
|
|
35
|
+
{ children: [], method: 'GET', urlMask: '/index.html' },
|
|
36
|
+
{ children: [], method: 'GET', urlMask: '/media/file-a.txt' },
|
|
37
|
+
{ children: [], method: 'GET', urlMask: '/media/file-b.txt' },
|
|
38
|
+
{ children: [], method: 'GET', urlMask: '/media/sub/file-aa.txt' },
|
|
39
|
+
{ children: [], method: 'GET', urlMask: '/media/sub/file-bb.txt' },
|
|
40
|
+
{ children: [], method: 'POST', urlMask: '/api/user' },
|
|
41
|
+
{ children: [], method: 'POST', urlMask: '/api/user/avatar/foo' },
|
|
42
|
+
{ children: [], method: 'PATCH', urlMask: '/api/user' }
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
const expected = [
|
|
47
|
+
{
|
|
48
|
+
urlMask: '/api/user',
|
|
49
|
+
method: 'GET',
|
|
50
|
+
children: [
|
|
51
|
+
{
|
|
52
|
+
urlMask: '/api/user/avatar',
|
|
53
|
+
method: 'GET',
|
|
54
|
+
children: [
|
|
55
|
+
{
|
|
56
|
+
urlMask: '/api/user/avatar/foo',
|
|
57
|
+
method: 'POST',
|
|
58
|
+
children: []
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}, {
|
|
62
|
+
urlMask: '/api/user',
|
|
63
|
+
method: 'POST',
|
|
64
|
+
children: []
|
|
65
|
+
}, {
|
|
66
|
+
urlMask: '/api/user',
|
|
67
|
+
method: 'PATCH',
|
|
68
|
+
children: []
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
urlMask: '/api/video/[id]',
|
|
74
|
+
method: 'GET',
|
|
75
|
+
children: []
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
urlMask: '/index.html',
|
|
79
|
+
method: 'GET',
|
|
80
|
+
children: []
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
urlMask: '/media/file-a.txt',
|
|
84
|
+
method: 'GET',
|
|
85
|
+
children: [
|
|
86
|
+
{
|
|
87
|
+
urlMask: '/media/file-b.txt',
|
|
88
|
+
method: 'GET',
|
|
89
|
+
children: []
|
|
90
|
+
}, {
|
|
91
|
+
urlMask: '/media/sub/file-aa.txt',
|
|
92
|
+
method: 'GET',
|
|
93
|
+
children: [
|
|
94
|
+
{
|
|
95
|
+
urlMask: '/media/sub/file-bb.txt',
|
|
96
|
+
method: 'GET',
|
|
97
|
+
children: []
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
deepEqual(groupByFolder(input), expected)
|
|
106
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import { deepEqual, equal } from 'node:assert/strict'
|
|
3
|
+
import { classNames, extractClassNames } from './dom-utils.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
test('classNames', () => equal(classNames('a', false && 'b'), 'a'))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
test('extractClassNames', () => {
|
|
10
|
+
const cssRules = [
|
|
11
|
+
{ cssText: '.TopLevelPascal { color: red; }' },
|
|
12
|
+
{ cssText: '.topLevelCamel { color: blue; }' },
|
|
13
|
+
{ cssText: '.top_level_snake { color: green; }' },
|
|
14
|
+
{ cssText: '.top-level-kebab { color: yellow; }' },
|
|
15
|
+
{ cssText: '.Level2Parent {\n & .level2ChildCamel { color: purple; }\n}' },
|
|
16
|
+
{ cssText: '.level2Base {\n &.level2ModifierCamel { font-weight: bold; }\n}' },
|
|
17
|
+
{ cssText: '.Level3Parent {\n & .level3ChildCamel {\n & .level3_grand_child_snake { color: orange; }\n}\n}' },
|
|
18
|
+
{ cssText: '.pseudoParent {\n &:hover { background: red; }\n & .pseudoNestedChild { color: pink; }\n}' },
|
|
19
|
+
{ cssText: '.multiClass1, .multi_class_2 { padding: 10px; }' },
|
|
20
|
+
{ cssText: '.combParent {\n & > .combChildDirect { margin: 5px; }\n}' },
|
|
21
|
+
{ cssText: '.siblingBase {\n & + .siblingAdjacent { border: 1px solid; }\n}' },
|
|
22
|
+
{ cssText: '@media (max-width: 768px) {\n .mediaQueryClass {\n & .mqNestedChild { display: none; }\n}\n}' },
|
|
23
|
+
{ cssText: '.class_with_123_numbers { color: cyan; }' },
|
|
24
|
+
{ cssText: '._privateStyleClass { opacity: 0.5; }' },
|
|
25
|
+
{ cssText: '.stringTest { content: ".shouldNotBeExtracted"; background: url(".alsoIgnored"); }' },
|
|
26
|
+
{ cssText: '.ComplexRoot {\n & .level2-kebab {\n &.level2ModCamel { color: red; }\n & .level3_snake {\n & .level4PascalChild { color: blue; }\n}\n}\n}' }
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
const expected = {
|
|
30
|
+
TopLevelPascal: null,
|
|
31
|
+
topLevelCamel: null,
|
|
32
|
+
top_level_snake: null,
|
|
33
|
+
'top-level-kebab': null,
|
|
34
|
+
|
|
35
|
+
Level2Parent: null,
|
|
36
|
+
level2ChildCamel: null,
|
|
37
|
+
level2Base: null,
|
|
38
|
+
level2ModifierCamel: null,
|
|
39
|
+
|
|
40
|
+
Level3Parent: null,
|
|
41
|
+
level3ChildCamel: null,
|
|
42
|
+
level3_grand_child_snake: null,
|
|
43
|
+
|
|
44
|
+
pseudoParent: null,
|
|
45
|
+
pseudoNestedChild: null,
|
|
46
|
+
|
|
47
|
+
multiClass1: null,
|
|
48
|
+
multi_class_2: null,
|
|
49
|
+
|
|
50
|
+
combParent: null,
|
|
51
|
+
combChildDirect: null,
|
|
52
|
+
|
|
53
|
+
siblingBase: null,
|
|
54
|
+
siblingAdjacent: null,
|
|
55
|
+
|
|
56
|
+
mediaQueryClass: null,
|
|
57
|
+
mqNestedChild: null,
|
|
58
|
+
|
|
59
|
+
class_with_123_numbers: null,
|
|
60
|
+
_privateStyleClass: null,
|
|
61
|
+
|
|
62
|
+
stringTest: null,
|
|
63
|
+
|
|
64
|
+
ComplexRoot: null,
|
|
65
|
+
'level2-kebab': null,
|
|
66
|
+
level2ModCamel: null,
|
|
67
|
+
level3_snake: null,
|
|
68
|
+
level4PascalChild: null
|
|
69
|
+
}
|
|
70
|
+
for (const k of Object.keys(expected))
|
|
71
|
+
expected[k] = k
|
|
72
|
+
|
|
73
|
+
deepEqual(extractClassNames({ cssRules }), expected)
|
|
74
|
+
})
|
package/src/client/dom-utils.js
CHANGED
|
@@ -70,12 +70,9 @@ function selectorFor(elem) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
export function defineClassNames(sheet) {
|
|
74
|
-
Object.assign(sheet, extractClassNames(sheet))
|
|
75
|
-
}
|
|
76
|
-
|
|
77
73
|
export function extractClassNames({ cssRules }) {
|
|
78
74
|
// Class names must begin with _ or a letter, then it can have numbers and hyphens
|
|
75
|
+
// TODO think about tag.className selectors
|
|
79
76
|
const reClassName = /(?:^|[\s,{>])&?\s*\.([a-zA-Z_][\w-]*)/g
|
|
80
77
|
const cNames = {}
|
|
81
78
|
let match
|
|
@@ -85,3 +82,73 @@ export function extractClassNames({ cssRules }) {
|
|
|
85
82
|
return cNames
|
|
86
83
|
}
|
|
87
84
|
|
|
85
|
+
|
|
86
|
+
export class QueryParamBool {
|
|
87
|
+
constructor(param) {
|
|
88
|
+
this.param = param
|
|
89
|
+
this.value = this.#init()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#init() {
|
|
93
|
+
const qs = new URLSearchParams(globalThis.location?.search)
|
|
94
|
+
if (qs.has(this.param))
|
|
95
|
+
return qs.get(this.param) !== '0'
|
|
96
|
+
const stored = globalThis.localStorage?.getItem(this.param) !== '0'
|
|
97
|
+
if (!stored)
|
|
98
|
+
this.#applyToUrl(false)
|
|
99
|
+
return stored
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
toggle() {
|
|
103
|
+
this.value = !this.value
|
|
104
|
+
if (this.value)
|
|
105
|
+
globalThis.localStorage?.removeItem(this.param)
|
|
106
|
+
else
|
|
107
|
+
globalThis.localStorage?.setItem(this.param, '0')
|
|
108
|
+
this.#applyToUrl(this.value)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#applyToUrl(nextVal) {
|
|
112
|
+
const url = new URL(globalThis.location?.href)
|
|
113
|
+
if (nextVal)
|
|
114
|
+
url.searchParams.delete(this.param)
|
|
115
|
+
else
|
|
116
|
+
url.searchParams.set(this.param, '0')
|
|
117
|
+
history.replaceState(null, '', url)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
export class LocalStorageSet {
|
|
123
|
+
constructor(key) {
|
|
124
|
+
this.key = key
|
|
125
|
+
this.value = this.#parse()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
add(item) {
|
|
129
|
+
this.value.add(item)
|
|
130
|
+
this.#persist()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
delete(item) {
|
|
134
|
+
this.value.delete(item)
|
|
135
|
+
this.#persist()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
has(item) {
|
|
139
|
+
return this.value.has(item)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#parse() {
|
|
143
|
+
try {
|
|
144
|
+
return new Set(JSON.parse(globalThis.localStorage?.getItem(this.key) || '[]'))
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return new Set()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#persist() {
|
|
152
|
+
globalThis.localStorage?.setItem(this.key, JSON.stringify([...this.value]))
|
|
153
|
+
}
|
|
154
|
+
}
|
package/src/client/graphics.js
CHANGED
|
@@ -1,35 +1,25 @@
|
|
|
1
1
|
import { createSvgElement as s } from './dom-utils.js'
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
s('
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
s('path', { d: 'm392.75 1.373c-2.216 0-4 1.784-4 4v18.043h-5.3086c-2.216 0-4 1.784-4 4v4.793c0 2.216 1.784 4 4 4h5.3086v51.398c0 6.1465 3.7064 10.823 9.232 10.823h16.531c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-12.97v-49.428h9.8711c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-9.8711v-18.043c0-2.216-1.784-4-4-4zm122.96 23.896c-10.699 0-19.312 8.6137-19.312 19.312v49.812c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-45.648c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v45.684c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312zm-69.999 0c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312v-34.535c0-10.699-8.6117-19.312-19.311-19.312zm1.9356 11h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v26.209c0 6.9115-1.5631 12.475-8.4746 12.475h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477z' })))
|
|
10
|
-
}
|
|
4
|
+
export const Logo = () =>
|
|
5
|
+
s('svg', { viewBox: '0 0 556 100' },
|
|
6
|
+
s('path', { d: 'm13.75 1.8789c-5.9487 0.19352-10.865 4.5652-11.082 11.686v81.445c-1e-7 2.216 1.784 4 4 4h4.793c2.216 0 4-1.784 4-4v-64.982c0.02794-3.4488 3.0988-3.5551 4.2031-1.1562l16.615 59.059c1.4393 5.3711 5.1083 7.9633 8.7656 7.9473 3.6573 0.01603 7.3263-2.5762 8.7656-7.9473l16.615-59.059c1.1043-2.3989 4.1752-2.2925 4.2031 1.1562v64.982c0 2.216 1.784 4 4 4h4.793c2.216 0 4-1.784 4-4v-81.445c-0.17732-7.0807-5.1334-11.492-11.082-11.686-5.9487-0.19352-12.652 3.8309-15.609 13.619l-15.686 57.334-15.686-57.334c-2.9569-9.7882-9.6607-13.813-15.609-13.619zm239.19 0.074219c-2.216 0-4 1.784-4 4v89.057c0 2.216 1.784 4 4 4h4.793c2.216 0 3.9868-1.784 4-4l0.10644-17.94c0.0734-0.07237 12.175-13.75 12.175-13.75 5.6772 11.091 11.404 22.158 17.113 33.232 1.0168 1.9689 3.4217 2.7356 5.3906 1.7188l4.2578-2.1992c1.9689-1.0168 2.7356-3.4217 1.7188-5.3906-6.4691-12.585-12.958-25.16-19.442-37.738l17.223-19.771c1.4555-1.671 1.2803-4.189-0.39062-5.6445l-3.6133-3.1465c-0.73105-0.63679-1.6224-0.96212-2.5176-0.98633-1.151-0.03113-2.3063 0.43508-3.125 1.375l-28.896 33.174v-51.99c0-2.216-1.784-4-4-4zm-58.255 23.316c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312l-0.125-7.8457c0-2.216-1.784-4-4-4h-4.6524c-2.216 0-4 1.784-4 4l3e-3 6.7888c3e-3 3.8063-1.5601 9.3694-8.4716 9.3694h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.6937 0 8.3697 5.2207 8.4687 11.828v2.2207c0 2.216 1.784 4 4 4h4.6524c2.216 0 4-1.784 4-4l0.125-5.7363c0-10.699-8.6117-19.312-19.311-19.312zm-72.182 0c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312v-34.535c0-10.699-8.6117-19.312-19.311-19.312zm1.9356 11h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v26.209c0 6.9115-1.5631 12.475-8.4746 12.475h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477z' }),
|
|
7
|
+
s('path', { opacity: 0.85, fill: 'currentColor', d: 'm331.9 25.27c-10.699 0-19.312 8.6137-19.312 19.312v4.3682c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-0.20414c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v7.0148h-28.059c-10.699 0-19.312 8.6117-19.312 19.311v4.0477c0 10.699 8.6137 19.313 19.312 19.312h17.812c2.216-1e-6 4-1.784 4-4v-4.7715c0-2.216-1.784-4-4-4h-13.648c-6.9115-2e-5 -12.477-1.5651-12.477-8.5649 0-6.9998 5.5651-8.5629 12.477-8.5629h23.895v25.897c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312z' }),
|
|
8
|
+
s('path', { d: 'm392.75 1.373c-2.216 0-4 1.784-4 4v18.043h-5.3086c-2.216 0-4 1.784-4 4v4.793c0 2.216 1.784 4 4 4h5.3086v51.398c0 6.1465 3.7064 10.823 9.232 10.823h16.531c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-12.97v-49.428h9.8711c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-9.8711v-18.043c0-2.216-1.784-4-4-4zm122.96 23.896c-10.699 0-19.312 8.6137-19.312 19.312v49.812c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-45.648c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v45.684c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312zm-69.999 0c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312v-34.535c0-10.699-8.6117-19.312-19.311-19.312zm1.9356 11h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v26.209c0 6.9115-1.5631 12.475-8.4746 12.475h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477z' }))
|
|
11
9
|
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
s('
|
|
15
|
-
s('path', { d: 'm11 5.6 0.14 7.2 6 3.7' })))
|
|
16
|
-
}
|
|
10
|
+
export const TimerIcon = () =>
|
|
11
|
+
s('svg', { viewBox: '0 0 24 24' },
|
|
12
|
+
s('path', { d: 'm11 5.6 0.14 7.2 6 3.7' }))
|
|
17
13
|
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
s('
|
|
21
|
-
|
|
22
|
-
s('path', { d: 'm6.1 9.1c2.8 0 5 2.3 5 5' })))
|
|
23
|
-
}
|
|
14
|
+
export const CloudIcon = () =>
|
|
15
|
+
s('svg', { viewBox: '0 0 24 24' },
|
|
16
|
+
s('path', { d: 'm6.1 8.9c0.98-2.3 3.3-3.9 6-3.9 3.3-2e-7 6 2.5 6.4 5.7 0.018 0.15 0.024 0.18 0.026 0.23 0.0016 0.037 8.2e-4 0.084 0.098 0.14 0.097 0.054 0.29 0.05 0.48 0.05 2.2 0 4 1.8 4 4s-1.8 4-4 4c-4-0.038-9-0.038-13-0.018-2.8 0-5-2.2-5-5-2.2e-7 -2.8 2.2-5 5-5 2.8 2e-7 5 2.2 5 5' }),
|
|
17
|
+
s('path', { d: 'm6.1 9.1c2.8 0 5 2.3 5 5' }))
|
|
24
18
|
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
s('
|
|
28
|
-
s('path', { d: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m1 17h-2v-2h2zm2.07-7.75-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25' })))
|
|
29
|
-
}
|
|
19
|
+
export const HelpIcon = () =>
|
|
20
|
+
s('svg', { viewBox: '0 0 24 24' },
|
|
21
|
+
s('path', { d: 'M11.07 12.85c.77-1.39 2.25-2.21 3.11-3.44.91-1.29.4-3.7-2.18-3.7-1.69 0-2.52 1.28-2.87 2.34L6.54 6.96C7.25 4.83 9.18 3 11.99 3c2.35 0 3.96 1.07 4.78 2.41.7 1.15 1.11 3.3.03 4.9-1.2 1.77-2.35 2.31-2.97 3.45-.25.46-.35.76-.35 2.24h-2.89c-.01-.78-.13-2.05.48-3.15M14 20c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2' }))
|
|
30
22
|
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
s('
|
|
34
|
-
s('path', { d: 'M6 9l6 6 6-6' })))
|
|
35
|
-
}
|
|
23
|
+
export const ChevronDownIcon = () =>
|
|
24
|
+
s('svg', { viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', 'stroke-width': '2' },
|
|
25
|
+
s('path', { d: 'M6 9l6 6 6-6' }))
|
package/src/server/Mockaton.js
CHANGED
|
@@ -10,6 +10,7 @@ import { IncomingMessage, BodyReaderError, hasControlChars } from './utils/HttpI
|
|
|
10
10
|
|
|
11
11
|
import { API } from '../client/ApiConstants.js'
|
|
12
12
|
|
|
13
|
+
import { cookie } from './cookie.js'
|
|
13
14
|
import { config, setup } from './config.js'
|
|
14
15
|
import { apiPatchReqs, apiGetReqs } from './Api.js'
|
|
15
16
|
|
|
@@ -24,6 +25,7 @@ import { watchMocksDir } from './Watcher.js'
|
|
|
24
25
|
export function Mockaton(options) {
|
|
25
26
|
return new Promise((resolve, reject) => {
|
|
26
27
|
setup(options)
|
|
28
|
+
cookie.init(config.cookies)
|
|
27
29
|
mockBrokerCollection.init()
|
|
28
30
|
|
|
29
31
|
if (config.watcherEnabled) {
|
|
@@ -35,7 +35,7 @@ export function init() {
|
|
|
35
35
|
|
|
36
36
|
/** @returns {boolean} registered */
|
|
37
37
|
export function registerMock(file, isFromWatcher = false) {
|
|
38
|
-
if (brokerByFilename(file)?.hasMock(file) ||
|
|
38
|
+
if (brokerByFilename(file)?.hasMock(file) ||
|
|
39
39
|
!isFileAllowed(basename(file)))
|
|
40
40
|
return false
|
|
41
41
|
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
class TrieNode {
|
|
2
|
-
constructor() {
|
|
3
|
-
this.items = []
|
|
4
|
-
this.kids = new Map()
|
|
5
|
-
}
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
// TODO it should ignore query string
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @typedef {{method: string, urlMask:string, children: BrokerLite[]}} BrokerLite
|
|
12
|
-
* @param {BrokerLite[]} brokers
|
|
13
|
-
* @returns {BrokerLite[]}
|
|
14
|
-
*/
|
|
15
|
-
export function dirStructure(brokers) {
|
|
16
|
-
const root = new TrieNode()
|
|
17
|
-
|
|
18
|
-
for (let b of brokers) {
|
|
19
|
-
let curr = root
|
|
20
|
-
for (const seg of b.urlMask.split('/').filter(Boolean)) {
|
|
21
|
-
if (!curr.kids.has(seg))
|
|
22
|
-
curr.kids.set(seg, new TrieNode())
|
|
23
|
-
curr = curr.kids.get(seg)
|
|
24
|
-
}
|
|
25
|
-
curr.items.push(b)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const result = []
|
|
29
|
-
for (const child of root.kids.values())
|
|
30
|
-
result.push(...convertNode(child))
|
|
31
|
-
|
|
32
|
-
if (root.items.length) {
|
|
33
|
-
const elems = [root.items[0], ...result]
|
|
34
|
-
for (let i = 1; i < root.items.length; i++)
|
|
35
|
-
elems.push(root.items[i])
|
|
36
|
-
|
|
37
|
-
const parentNode = elems[0]
|
|
38
|
-
for (let i = 1; i < elems.length; i++)
|
|
39
|
-
parentNode.children.push(elems[i])
|
|
40
|
-
|
|
41
|
-
return [parentNode]
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return result
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Recursively converts a TrieNode into a flattened, nested array of objects.
|
|
49
|
-
* Flattens the tree by having the first available route at a given directory level
|
|
50
|
-
* act as the parent wrapper for the remaining items and subdirectories.
|
|
51
|
-
*
|
|
52
|
-
* @param {TrieNode} node
|
|
53
|
-
* @returns {BrokerLite[]}
|
|
54
|
-
*/
|
|
55
|
-
function convertNode(node) {
|
|
56
|
-
const childNodes = []
|
|
57
|
-
for (const child of node.kids.values())
|
|
58
|
-
childNodes.push(...convertNode(child))
|
|
59
|
-
|
|
60
|
-
const elems = []
|
|
61
|
-
if (node.items.length) {
|
|
62
|
-
elems.push(node.items[0])
|
|
63
|
-
elems.push(...childNodes)
|
|
64
|
-
for (let i = 1; i < node.items.length; i++)
|
|
65
|
-
elems.push(node.items[i])
|
|
66
|
-
}
|
|
67
|
-
else
|
|
68
|
-
elems.push(...childNodes)
|
|
69
|
-
|
|
70
|
-
if (!elems.length)
|
|
71
|
-
return []
|
|
72
|
-
|
|
73
|
-
const parentNode = elems[0]
|
|
74
|
-
|
|
75
|
-
if (node.items.length || !parentNode.children.length) {
|
|
76
|
-
for (let i = 1; i < elems.length; i++)
|
|
77
|
-
parentNode.children.push(elems[i])
|
|
78
|
-
return [parentNode]
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return elems
|
|
82
|
-
}
|
|
83
|
-
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import { deepEqual } from 'node:assert/strict'
|
|
3
|
-
import { dirStructure } from './dirStructure.js'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const input = [
|
|
7
|
-
{ children: [], method: 'GET', urlMask: '/api/user' },
|
|
8
|
-
{ children: [], method: 'GET', urlMask: '/api/user/avatar' },
|
|
9
|
-
{ children: [], method: 'GET', urlMask: '/api/video/[id]' },
|
|
10
|
-
{ children: [], method: 'GET', urlMask: '/index.html' },
|
|
11
|
-
{ children: [], method: 'GET', urlMask: '/media/file-a.txt' },
|
|
12
|
-
{ children: [], method: 'GET', urlMask: '/media/file-b.txt' },
|
|
13
|
-
{ children: [], method: 'GET', urlMask: '/media/sub/file-aa.txt' },
|
|
14
|
-
{ children: [], method: 'GET', urlMask: '/media/sub/file-bb.txt' },
|
|
15
|
-
{ children: [], method: 'POST', urlMask: '/api/user' },
|
|
16
|
-
{ children: [], method: 'POST', urlMask: '/api/user/avatar/foo' },
|
|
17
|
-
{ children: [], method: 'PATCH', urlMask: '/api/user' }
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const expected = [
|
|
22
|
-
{
|
|
23
|
-
urlMask: '/api/user',
|
|
24
|
-
method: 'GET',
|
|
25
|
-
children: [
|
|
26
|
-
{
|
|
27
|
-
urlMask: '/api/user/avatar',
|
|
28
|
-
method: 'GET',
|
|
29
|
-
children: [
|
|
30
|
-
{
|
|
31
|
-
urlMask: '/api/user/avatar/foo',
|
|
32
|
-
method: 'POST',
|
|
33
|
-
children: []
|
|
34
|
-
}
|
|
35
|
-
]
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
urlMask: '/api/user',
|
|
39
|
-
method: 'POST',
|
|
40
|
-
children: []
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
urlMask: '/api/user',
|
|
44
|
-
method: 'PATCH',
|
|
45
|
-
children: []
|
|
46
|
-
}
|
|
47
|
-
]
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
urlMask: '/api/video/[id]',
|
|
51
|
-
method: 'GET',
|
|
52
|
-
children: []
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
urlMask: '/index.html',
|
|
56
|
-
method: 'GET',
|
|
57
|
-
children: []
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
urlMask: '/media/file-a.txt',
|
|
61
|
-
method: 'GET',
|
|
62
|
-
children: [
|
|
63
|
-
{
|
|
64
|
-
urlMask: '/media/file-b.txt',
|
|
65
|
-
method: 'GET',
|
|
66
|
-
children: []
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
urlMask: '/media/sub/file-aa.txt',
|
|
70
|
-
method: 'GET',
|
|
71
|
-
children: [
|
|
72
|
-
{
|
|
73
|
-
urlMask: '/media/sub/file-bb.txt',
|
|
74
|
-
method: 'GET',
|
|
75
|
-
children: []
|
|
76
|
-
}
|
|
77
|
-
]
|
|
78
|
-
}
|
|
79
|
-
]
|
|
80
|
-
}
|
|
81
|
-
]
|
|
82
|
-
|
|
83
|
-
test('acceptance', () => {
|
|
84
|
-
deepEqual(dirStructure(input), expected)
|
|
85
|
-
})
|
|
86
|
-
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import { deepEqual, equal } from 'node:assert/strict'
|
|
3
|
-
import { extractClassNames, classNames } from './dom-utils.js'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
test('classNames', () => {
|
|
7
|
-
equal(classNames('a', false && 'b'), 'a')
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const cssRules = [
|
|
12
|
-
{ cssText: '.TopLevelPascal { color: red; }' },
|
|
13
|
-
{ cssText: '.topLevelCamel { color: blue; }' },
|
|
14
|
-
{ cssText: '.top_level_snake { color: green; }' },
|
|
15
|
-
{ cssText: '.top-level-kebab { color: yellow; }' },
|
|
16
|
-
{ cssText: '.Level2Parent {\n & .level2ChildCamel { color: purple; }\n}' },
|
|
17
|
-
{ cssText: '.level2Base {\n &.level2ModifierCamel { font-weight: bold; }\n}' },
|
|
18
|
-
{ cssText: '.Level3Parent {\n & .level3ChildCamel {\n & .level3_grand_child_snake { color: orange; }\n}\n}' },
|
|
19
|
-
{ cssText: '.pseudoParent {\n &:hover { background: red; }\n & .pseudoNestedChild { color: pink; }\n}' },
|
|
20
|
-
{ cssText: '.multiClass1, .multi_class_2 { padding: 10px; }' },
|
|
21
|
-
{ cssText: '.combParent {\n & > .combChildDirect { margin: 5px; }\n}' },
|
|
22
|
-
{ cssText: '.siblingBase {\n & + .siblingAdjacent { border: 1px solid; }\n}' },
|
|
23
|
-
{ cssText: '@media (max-width: 768px) {\n .mediaQueryClass {\n & .mqNestedChild { display: none; }\n}\n}' },
|
|
24
|
-
{ cssText: '.class_with_123_numbers { color: cyan; }' },
|
|
25
|
-
{ cssText: '._privateStyleClass { opacity: 0.5; }' },
|
|
26
|
-
{ cssText: '.stringTest { content: ".shouldNotBeExtracted"; background: url(".alsoIgnored"); }' },
|
|
27
|
-
{ cssText: '.ComplexRoot {\n & .level2-kebab {\n &.level2ModCamel { color: red; }\n & .level3_snake {\n & .level4PascalChild { color: blue; }\n}\n}\n}' }
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
const expected = {
|
|
31
|
-
TopLevelPascal: null,
|
|
32
|
-
topLevelCamel: null,
|
|
33
|
-
top_level_snake: null,
|
|
34
|
-
'top-level-kebab': null,
|
|
35
|
-
|
|
36
|
-
Level2Parent: null,
|
|
37
|
-
level2ChildCamel: null,
|
|
38
|
-
level2Base: null,
|
|
39
|
-
level2ModifierCamel: null,
|
|
40
|
-
|
|
41
|
-
Level3Parent: null,
|
|
42
|
-
level3ChildCamel: null,
|
|
43
|
-
level3_grand_child_snake: null,
|
|
44
|
-
|
|
45
|
-
pseudoParent: null,
|
|
46
|
-
pseudoNestedChild: null,
|
|
47
|
-
|
|
48
|
-
multiClass1: null,
|
|
49
|
-
multi_class_2: null,
|
|
50
|
-
|
|
51
|
-
combParent: null,
|
|
52
|
-
combChildDirect: null,
|
|
53
|
-
|
|
54
|
-
siblingBase: null,
|
|
55
|
-
siblingAdjacent: null,
|
|
56
|
-
|
|
57
|
-
mediaQueryClass: null,
|
|
58
|
-
mqNestedChild: null,
|
|
59
|
-
|
|
60
|
-
class_with_123_numbers: null,
|
|
61
|
-
_privateStyleClass: null,
|
|
62
|
-
|
|
63
|
-
stringTest: null,
|
|
64
|
-
|
|
65
|
-
ComplexRoot: null,
|
|
66
|
-
'level2-kebab': null,
|
|
67
|
-
level2ModCamel: null,
|
|
68
|
-
level3_snake: null,
|
|
69
|
-
level4PascalChild: null
|
|
70
|
-
}
|
|
71
|
-
for (const k of Object.keys(expected))
|
|
72
|
-
expected[k] = k
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
test('extracts', () => deepEqual(extractClassNames({ cssRules }), expected))
|
|
76
|
-
|