mockaton 8.5.1 → 8.6.1
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 +45 -26
- package/TODO.md +1 -2
- package/package.json +4 -4
- package/src/Api.js +2 -0
- package/src/Commander.js +7 -4
- package/src/Dashboard.css +38 -21
- package/src/Dashboard.js +35 -24
package/README.md
CHANGED
|
@@ -5,50 +5,60 @@
|
|
|
5
5
|
|
|
6
6
|
## Mock your APIs, Enhance your Development Workflow
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
development and testing
|
|
8
|
+
Welcome to developer experience tooling! Mockaton is here to help
|
|
9
|
+
make your frontend development and testing easier—and a lot more fun.
|
|
10
10
|
|
|
11
|
-
With Mockaton you don’t need to write code for wiring your mocks. Instead,
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
With Mockaton you don’t need to write code for wiring your mocks. Instead, just
|
|
12
|
+
place your mocks in a directory and let Mockaton do the rest. It will automatically
|
|
13
|
+
scan the directory for filenames that follow a convention similar to the URL paths.
|
|
14
|
+
|
|
15
|
+
For example, for this route `/api/user/1234`, the mock filename would be:
|
|
14
16
|
```
|
|
15
17
|
my-mocks-dir/api/user/[user-id].GET.200.json
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
And hey, no need to mock everything. If you don’t have a mock for
|
|
21
|
+
a certain API, Mockaton can fallback to your real backend. Just
|
|
22
|
+
type your backend address in the **Fallback Backend** field.
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
And there’s one more cool thing—you can collect those responses
|
|
25
|
+
by clicking the **Save Mocks** checkbox. Those mocks will
|
|
26
|
+
be saved to your `config.mocksDir` following the filename convention.
|
|
22
27
|
|
|
23
|
-
## Scraping Mocks
|
|
24
|
-
You can save mocks following the filename convention
|
|
25
|
-
for the routes that reached your `proxyFallback` with:
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
## Multiple Mock Variants
|
|
28
30
|
|
|
31
|
+
You can have many mocks for any route. For example, you might
|
|
32
|
+
want different mocks with different response status codes
|
|
33
|
+
(like triggering errors). Here are a couple of ways to do it:
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- __Comment__ on the filename, which is anything within parentheses.
|
|
34
|
-
- e.g. `api/login(locked out user).POST.423.json`
|
|
35
|
+
### Adding comments in filenames
|
|
36
|
+
Want to mock a locked-out user or an invalid login attempt? You
|
|
37
|
+
can just add a comment to the filename in parentheses. For example:
|
|
35
38
|
|
|
39
|
+
`api/login(locked out user).POST.423.json`
|
|
40
|
+
|
|
41
|
+
### Different response status code
|
|
42
|
+
For instance, you can have mocks with a `4xx` or `5xx` status code for triggering
|
|
43
|
+
error responses. Or with a `204` (No Content) for testing empty collections.
|
|
36
44
|
|
|
37
|
-
## Dashboard
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
## Dashboard
|
|
47
|
+
In the dashboard you can select a mock variant for a particular route, among
|
|
48
|
+
other features such as delaying responses, or triggering an autogenerated
|
|
49
|
+
`500` (Internal Server Error). Nonetheless, there’s a programmatic API,
|
|
41
50
|
which is handy for setting up tests (see **Commander API** below).
|
|
42
51
|
|
|
43
52
|
<picture>
|
|
44
|
-
<source media="(prefers-color-scheme: light)" srcset="./
|
|
45
|
-
<source media="(prefers-color-scheme: dark)" srcset="./
|
|
46
|
-
<img alt="Mockaton Dashboard
|
|
53
|
+
<source media="(prefers-color-scheme: light)" srcset="./pixaton-tests/pic-for-readme.vp860x800.light.gold.png">
|
|
54
|
+
<source media="(prefers-color-scheme: dark)" srcset="./pixaton-tests/pic-for-readme.vp860x800.dark.gold.png">
|
|
55
|
+
<img alt="Mockaton Dashboard" src="./pixaton-tests/pic-for-readme.vp860x800.light.gold.png">
|
|
47
56
|
</picture>
|
|
48
57
|
|
|
49
58
|
|
|
59
|
+
|
|
50
60
|
## Basic Usage
|
|
51
|
-
`tsx` is only needed if you want to write mocks in TypeScript
|
|
61
|
+
`tsx` is only needed if you want to write mocks in TypeScript.
|
|
52
62
|
```sh
|
|
53
63
|
npm install mockaton tsx --save-dev
|
|
54
64
|
```
|
|
@@ -72,7 +82,12 @@ node --import=tsx my-mockaton.js
|
|
|
72
82
|
|
|
73
83
|
## Running the demo app (Vite)
|
|
74
84
|
|
|
75
|
-
This is a minimal React + Vite + Mockaton app.
|
|
85
|
+
This is a minimal React + Vite + Mockaton app. It’s mainly a list of
|
|
86
|
+
colors, which contains all of their possible states. For example,
|
|
87
|
+
permutations for out-of-stock, new-arrival, and discontinued.
|
|
88
|
+
|
|
89
|
+
Also, if you select the **Admin User** from the Mockaton dashboard,
|
|
90
|
+
the color cards will have a Delete button as well.
|
|
76
91
|
|
|
77
92
|
```sh
|
|
78
93
|
git clone https://github.com/ericfortis/mockaton.git
|
|
@@ -82,7 +97,11 @@ npm run mockaton
|
|
|
82
97
|
npm run start
|
|
83
98
|
```
|
|
84
99
|
|
|
85
|
-
By the way, that directory has
|
|
100
|
+
By the way, that directory has scripts for opening Mockaton and Vite in one command.
|
|
101
|
+
|
|
102
|
+
The app looks like this:
|
|
103
|
+
|
|
104
|
+
<img src="./demo-app-vite/README-screenshot.png" alt="Mockaton Demo App Screenshot" width="580" />
|
|
86
105
|
|
|
87
106
|
|
|
88
107
|
## Use Cases
|
package/TODO.md
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "mockaton",
|
|
3
3
|
"description": "A deterministic server-side for developing and testing frontend clients",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.
|
|
5
|
+
"version": "8.6.1",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
8
8
|
"license": "MIT",
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
"test": "node --test src/**.test.js",
|
|
12
12
|
"start": "node dev-mockaton.js",
|
|
13
13
|
"start:ts": "node --import=tsx dev-mockaton.js",
|
|
14
|
-
"
|
|
14
|
+
"pixaton": "node --test --import=./pixaton-tests/_setup.js --experimental-test-isolation=none \"pixaton-tests/**/*.test.js\"",
|
|
15
15
|
"outdated": "npm outdated --parseable | awk -F: '{ printf \"npm i %-30s ;# %s\\n\", $4, $2 }'"
|
|
16
16
|
},
|
|
17
17
|
"optionalDependencies": {
|
|
18
|
-
"pixaton": ">=1.0.
|
|
19
|
-
"puppeteer": ">=
|
|
18
|
+
"pixaton": ">=1.0.2",
|
|
19
|
+
"puppeteer": ">=24.1.1"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/src/Api.js
CHANGED
|
@@ -26,6 +26,7 @@ export const apiGetRequests = new Map([
|
|
|
26
26
|
[API.comments, listComments],
|
|
27
27
|
[API.fallback, getProxyFallback],
|
|
28
28
|
[API.cors, getIsCorsAllowed],
|
|
29
|
+
[API.collectProxied, getCollectProxied],
|
|
29
30
|
[API.static, listStaticFiles]
|
|
30
31
|
])
|
|
31
32
|
|
|
@@ -50,6 +51,7 @@ function listComments(_, response) { sendJSON(response, mockBrokersCollection.ex
|
|
|
50
51
|
function listMockBrokers(_, response) { sendJSON(response, mockBrokersCollection.getAll()) }
|
|
51
52
|
function getProxyFallback(_, response) { sendJSON(response, config.proxyFallback) }
|
|
52
53
|
function getIsCorsAllowed(_, response) { sendJSON(response, config.corsAllowed) }
|
|
54
|
+
function getCollectProxied(_, response) { sendJSON(response, config.collectProxied) }
|
|
53
55
|
|
|
54
56
|
function listStaticFiles(req, response) {
|
|
55
57
|
try {
|
package/src/Commander.js
CHANGED
|
@@ -54,14 +54,13 @@ export class Commander {
|
|
|
54
54
|
return this.#patch(API.fallback, proxyAddr)
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
getCollectProxied() {
|
|
58
|
+
return this.#get(API.collectProxied)
|
|
59
|
+
}
|
|
57
60
|
setCollectProxied(shouldCollect) {
|
|
58
61
|
return this.#patch(API.collectProxied, shouldCollect)
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
reset() {
|
|
62
|
-
return this.#patch(API.reset)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
64
|
getCorsAllowed() {
|
|
66
65
|
return this.#get(API.cors)
|
|
67
66
|
}
|
|
@@ -72,4 +71,8 @@ export class Commander {
|
|
|
72
71
|
listStaticFiles() {
|
|
73
72
|
return this.#get(API.static)
|
|
74
73
|
}
|
|
74
|
+
|
|
75
|
+
reset() {
|
|
76
|
+
return this.#patch(API.reset)
|
|
77
|
+
}
|
|
75
78
|
}
|
package/src/Dashboard.css
CHANGED
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
--colorAccentAlt: #009c71;
|
|
10
10
|
--colorBackground: #fff;
|
|
11
11
|
--colorHeaderBackground: #f7f7f7;
|
|
12
|
-
--colorComboBoxBackground: #
|
|
12
|
+
--colorComboBoxBackground: #f7f7f7;
|
|
13
|
+
--colorSecondaryButtonBackground: #f5f5f5;
|
|
13
14
|
--colorComboBoxHeaderBackground: #fff;
|
|
14
15
|
--colorDisabled: #444;
|
|
15
16
|
--colorHover: #dfefff;
|
|
16
17
|
--colorLabel: #444;
|
|
17
18
|
--colorLightRed: #ffe4ee;
|
|
18
19
|
--colorRed: #da0f00;
|
|
19
|
-
--colorSecondaryButtonBackground: #fafafa;
|
|
20
20
|
--colorText: #000;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
--colorBackground: #161616;
|
|
29
29
|
--colorHeaderBackground: #090909;
|
|
30
30
|
--colorComboBoxBackground: #252525;
|
|
31
|
+
--colorSecondaryButtonBackground: #444;
|
|
31
32
|
--colorComboBoxHeaderBackground: #222;
|
|
32
33
|
--colorDisabled: #bbb;
|
|
33
34
|
--colorHover: #023661;
|
|
34
35
|
--colorLabel: #aaa;
|
|
35
36
|
--colorLightRed: #ffe4ee;
|
|
36
37
|
--colorRed: #f41606;
|
|
37
|
-
--colorSecondaryButtonBackground: #444;
|
|
38
38
|
--colorText: #fff;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -49,12 +49,13 @@ body {
|
|
|
49
49
|
color: var(--colorText);
|
|
50
50
|
}
|
|
51
51
|
* {
|
|
52
|
+
box-sizing: border-box;
|
|
52
53
|
padding: 0;
|
|
53
54
|
border: 0;
|
|
54
55
|
margin: 0;
|
|
55
56
|
font-family: system-ui, sans-serif;
|
|
56
57
|
font-size: 100%;
|
|
57
|
-
outline: 0
|
|
58
|
+
outline: 0;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
select, a, input, button, summary {
|
|
@@ -91,7 +92,7 @@ menu {
|
|
|
91
92
|
display: flex;
|
|
92
93
|
width: 100%;
|
|
93
94
|
align-items: flex-end;
|
|
94
|
-
padding: 16px;
|
|
95
|
+
padding: 15px 16px;
|
|
95
96
|
border-bottom: 1px solid rgba(127, 127, 127, 0.1);
|
|
96
97
|
background: var(--colorHeaderBackground);
|
|
97
98
|
box-shadow: var(--boxShadow1);
|
|
@@ -103,7 +104,9 @@ menu {
|
|
|
103
104
|
margin-right: 18px;
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
|
|
107
|
+
.Field {
|
|
108
|
+
min-width: 150px;
|
|
109
|
+
|
|
107
110
|
span {
|
|
108
111
|
display: block;
|
|
109
112
|
color: var(--colorLabel);
|
|
@@ -112,27 +115,48 @@ menu {
|
|
|
112
115
|
|
|
113
116
|
input[type=url],
|
|
114
117
|
select {
|
|
118
|
+
width: 100%;
|
|
115
119
|
height: 28px;
|
|
116
|
-
width: 150px;
|
|
117
120
|
padding: 4px 2px;
|
|
118
121
|
border-right: 3px solid transparent;
|
|
119
|
-
margin-top:
|
|
122
|
+
margin-top: 4px;
|
|
120
123
|
font-size: 11px;
|
|
121
124
|
background: var(--colorComboBoxHeaderBackground);
|
|
122
125
|
border-radius: 6px;
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
&.FallbackBackend {
|
|
129
|
+
position: relative;
|
|
130
|
+
width: 194px;
|
|
131
|
+
|
|
132
|
+
input[type=url] {
|
|
133
|
+
padding: 0 6px;
|
|
134
|
+
box-shadow: var(--boxShadow1);
|
|
135
|
+
color: var(--colorText);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.SaveProxiedCheckbox {
|
|
139
|
+
position: absolute;
|
|
140
|
+
top: 0;
|
|
141
|
+
right: 0;
|
|
142
|
+
display: flex;
|
|
143
|
+
width: auto;
|
|
144
|
+
min-width: unset;
|
|
145
|
+
align-items: center;
|
|
146
|
+
font-size: 11px;
|
|
147
|
+
gap: 4px;
|
|
148
|
+
|
|
149
|
+
input:disabled + span {
|
|
150
|
+
opacity: 0.7;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
129
153
|
}
|
|
130
154
|
}
|
|
131
155
|
|
|
132
156
|
.ResetButton {
|
|
133
157
|
padding: 4px 12px;
|
|
134
|
-
margin-bottom: 4px;
|
|
135
158
|
border: 1px solid var(--colorRed);
|
|
159
|
+
margin-bottom: 4px;
|
|
136
160
|
background: transparent;
|
|
137
161
|
color: var(--colorRed);
|
|
138
162
|
border-radius: 50px;
|
|
@@ -144,13 +168,6 @@ menu {
|
|
|
144
168
|
box-shadow: var(--boxShadow1);
|
|
145
169
|
}
|
|
146
170
|
}
|
|
147
|
-
|
|
148
|
-
.CorsCheckbox {
|
|
149
|
-
display: flex;
|
|
150
|
-
align-items: center;
|
|
151
|
-
margin-bottom: 8px;
|
|
152
|
-
gap: 4px;
|
|
153
|
-
}
|
|
154
171
|
}
|
|
155
172
|
|
|
156
173
|
|
|
@@ -368,8 +385,8 @@ main {
|
|
|
368
385
|
|
|
369
386
|
a {
|
|
370
387
|
display: inline-block;
|
|
371
|
-
border-radius: 6px;
|
|
372
388
|
padding: 6px;
|
|
389
|
+
border-radius: 6px;
|
|
373
390
|
color: var(--colorAccentAlt);
|
|
374
391
|
text-decoration: none;
|
|
375
392
|
|
package/src/Dashboard.js
CHANGED
|
@@ -4,7 +4,6 @@ import { DEFAULT_500_COMMENT } from '/ApiConstants.js'
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
const Strings = {
|
|
7
|
-
allow_cors: 'Allow CORS',
|
|
8
7
|
bulk_select_by_comment: 'Bulk Select by Comment',
|
|
9
8
|
bulk_select_by_comment_disabled_title: 'No mock files have comments, which are anything within parentheses on the filename.',
|
|
10
9
|
click_link_to_preview: 'Click a link to preview it',
|
|
@@ -18,19 +17,22 @@ const Strings = {
|
|
|
18
17
|
mock: 'Mock',
|
|
19
18
|
no_mocks_found: 'No mocks found',
|
|
20
19
|
reset: 'Reset',
|
|
20
|
+
save_proxied: 'Save Mocks',
|
|
21
21
|
select_one: 'Select One',
|
|
22
22
|
static: 'Static'
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const CSS = {
|
|
26
|
-
ResetButton: 'ResetButton',
|
|
27
|
-
CorsCheckbox: 'CorsCheckbox',
|
|
28
26
|
DelayToggler: 'DelayToggler',
|
|
27
|
+
FallbackBackend: 'FallbackBackend',
|
|
28
|
+
Field: 'Field',
|
|
29
29
|
InternalServerErrorToggler: 'InternalServerErrorToggler',
|
|
30
30
|
MockSelector: 'MockSelector',
|
|
31
31
|
PayloadViewer: 'PayloadViewer',
|
|
32
32
|
PreviewLink: 'PreviewLink',
|
|
33
33
|
ProgressBar: 'ProgressBar',
|
|
34
|
+
ResetButton: 'ResetButton',
|
|
35
|
+
SaveProxiedCheckbox: 'SaveProxiedCheckbox',
|
|
34
36
|
StaticFilesList: 'StaticFilesList',
|
|
35
37
|
|
|
36
38
|
bold: 'bold',
|
|
@@ -49,7 +51,7 @@ function init() {
|
|
|
49
51
|
mockaton.listMocks(),
|
|
50
52
|
mockaton.listCookies(),
|
|
51
53
|
mockaton.listComments(),
|
|
52
|
-
mockaton.
|
|
54
|
+
mockaton.getCollectProxied(),
|
|
53
55
|
mockaton.getProxyFallback(),
|
|
54
56
|
mockaton.listStaticFiles()
|
|
55
57
|
].map(api => api.then(response => response.ok && response.json())))
|
|
@@ -63,7 +65,7 @@ function App(apiResponses) {
|
|
|
63
65
|
document.body.appendChild(DevPanel(apiResponses))
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
function DevPanel([brokersByMethod, cookies, comments,
|
|
68
|
+
function DevPanel([brokersByMethod, cookies, comments, collectProxied, fallbackAddress, staticFiles]) {
|
|
67
69
|
const isEmpty = Object.keys(brokersByMethod).length === 0
|
|
68
70
|
return (
|
|
69
71
|
r('div', null,
|
|
@@ -71,8 +73,7 @@ function DevPanel([brokersByMethod, cookies, comments, corsAllowed, fallbackAddr
|
|
|
71
73
|
r('img', { src: '/mockaton-logo.svg', width: 160, alt: Strings.title }),
|
|
72
74
|
r(CookieSelector, { list: cookies }),
|
|
73
75
|
r(BulkSelector, { comments }),
|
|
74
|
-
r(ProxyFallbackField, { fallbackAddress }),
|
|
75
|
-
r(CorsCheckbox, { corsAllowed }),
|
|
76
|
+
r(ProxyFallbackField, { fallbackAddress, collectProxied }),
|
|
76
77
|
r(ResetButton)),
|
|
77
78
|
isEmpty
|
|
78
79
|
? r('main', null, Strings.no_mocks_found)
|
|
@@ -94,7 +95,7 @@ function CookieSelector({ list }) {
|
|
|
94
95
|
}
|
|
95
96
|
const disabled = list.length <= 1
|
|
96
97
|
return (
|
|
97
|
-
r('label',
|
|
98
|
+
r('label', { className: CSS.Field },
|
|
98
99
|
r('span', null, Strings.cookie),
|
|
99
100
|
r('select', {
|
|
100
101
|
autocomplete: 'off',
|
|
@@ -118,7 +119,7 @@ function BulkSelector({ comments }) {
|
|
|
118
119
|
? []
|
|
119
120
|
: [Strings.select_one].concat(comments)
|
|
120
121
|
return (
|
|
121
|
-
r('label',
|
|
122
|
+
r('label', { className: CSS.Field },
|
|
122
123
|
r('span', null, Strings.bulk_select_by_comment),
|
|
123
124
|
r('select', {
|
|
124
125
|
'data-qaid': 'BulkSelector',
|
|
@@ -132,41 +133,51 @@ function BulkSelector({ comments }) {
|
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
|
|
135
|
-
function ProxyFallbackField({ fallbackAddress = '' }) {
|
|
136
|
+
function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
|
|
137
|
+
const refSaveProxiedCheckbox = useRef()
|
|
136
138
|
function onChange(event) {
|
|
137
139
|
const input = event.currentTarget
|
|
140
|
+
refSaveProxiedCheckbox.current.disabled = !input.validity.valid || !input.value.trim()
|
|
138
141
|
if (!input.validity.valid)
|
|
139
142
|
input.reportValidity()
|
|
140
143
|
else
|
|
141
|
-
mockaton.setProxyFallback(input.value)
|
|
144
|
+
mockaton.setProxyFallback(input.value.trim())
|
|
142
145
|
.catch(onError)
|
|
143
146
|
}
|
|
144
147
|
return (
|
|
145
|
-
r('
|
|
146
|
-
r('
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
r('div', { className: CSS.Field + ' ' + CSS.FallbackBackend },
|
|
149
|
+
r('label', null,
|
|
150
|
+
r('span', null, Strings.fallback_server),
|
|
151
|
+
r('input', {
|
|
152
|
+
type: 'url',
|
|
153
|
+
autocomplete: 'none',
|
|
154
|
+
placeholder: Strings.fallback_server_placeholder,
|
|
155
|
+
value: fallbackAddress,
|
|
156
|
+
onChange
|
|
157
|
+
})),
|
|
158
|
+
r(SaveProxiedCheckbox, {
|
|
159
|
+
collectProxied,
|
|
160
|
+
disabled: !fallbackAddress,
|
|
161
|
+
ref: refSaveProxiedCheckbox
|
|
153
162
|
})))
|
|
154
163
|
}
|
|
155
164
|
|
|
156
165
|
|
|
157
|
-
function
|
|
166
|
+
function SaveProxiedCheckbox({ ref, disabled, collectProxied }) {
|
|
158
167
|
function onChange(event) {
|
|
159
|
-
mockaton.
|
|
168
|
+
mockaton.setCollectProxied(event.currentTarget.checked)
|
|
160
169
|
.catch(onError)
|
|
161
170
|
}
|
|
162
171
|
return (
|
|
163
|
-
r('label', { className: CSS.
|
|
172
|
+
r('label', { className: CSS.SaveProxiedCheckbox },
|
|
164
173
|
r('input', {
|
|
174
|
+
ref,
|
|
165
175
|
type: 'checkbox',
|
|
166
|
-
|
|
176
|
+
disabled,
|
|
177
|
+
checked: collectProxied,
|
|
167
178
|
onChange
|
|
168
179
|
}),
|
|
169
|
-
Strings.
|
|
180
|
+
r('span', null, Strings.save_proxied)))
|
|
170
181
|
}
|
|
171
182
|
|
|
172
183
|
|