dsh-foreman-node 0.1.1 → 0.2.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/CHANGELOG.md +69 -0
- package/LICENSE +21 -21
- package/README.md +29 -0
- package/cordis.patch.yml +0 -0
- package/lib/client.js +611 -0
- package/lib/index.d.ts +246 -32
- package/lib/index.js +662 -23
- package/package.json +27 -3
- package/src/client/index.tsx +211 -0
- package/src/client/locales.ts +95 -0
- package/src/client/page-state.ts +99 -0
- package/src/client.ts +0 -0
- package/src/config.ts +100 -0
- package/src/index.ts +112 -34
- package/src/keys.ts +49 -0
- package/src/protocol.ts +0 -0
- package/src/report-tool.ts +0 -0
- package/src/routes.ts +157 -0
- package/src/session.ts +57 -1
- package/src/tunnel.ts +267 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-foreman-node",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
// src/config.ts
|
|
10
|
+
/** The SSH defaults, spread over whatever the host hands in, so a partial block is still complete. */
|
|
11
|
+
const SSH_DEFAULTS = {
|
|
12
|
+
enabled: false,
|
|
13
|
+
host: "",
|
|
14
|
+
port: 22,
|
|
15
|
+
user: "",
|
|
16
|
+
keyPath: "",
|
|
17
|
+
remotePort: 8790,
|
|
18
|
+
localPort: 8790
|
|
19
|
+
};
|
|
20
|
+
/** A volatile field arrives wrapped; this reads it either way, so callers do not have to know which. */
|
|
21
|
+
const valueOf = (field, fallback) => {
|
|
22
|
+
if (field === void 0) return fallback;
|
|
23
|
+
if (typeof field === "object" && field !== null && "get" in field && typeof field.get === "function") return field.get();
|
|
24
|
+
return field;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Unwrap a whole configuration: every field may be a volatile wrapper, a plain value, or absent.
|
|
28
|
+
*
|
|
29
|
+
* `Config` already applies defaults, so `apply` sees a complete object. This is for the paths that do not go through
|
|
30
|
+
* the schema: the configuration page's read, and a `cordis.patch.yml` written before a field existed.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Unwrap the `ssh` block.
|
|
34
|
+
*
|
|
35
|
+
* Its fields are volatile for the same reason the top level is, so each may arrive wrapped. Reading them all through
|
|
36
|
+
* `valueOf` keeps one unwrapping rule instead of two, and it means a plain partial block (an older patch file) works
|
|
37
|
+
* just as well as a wrapped one.
|
|
38
|
+
*/
|
|
39
|
+
function unwrapSsh(block) {
|
|
40
|
+
if (block === null || typeof block !== "object") return {};
|
|
41
|
+
const source = block;
|
|
42
|
+
return {
|
|
43
|
+
enabled: valueOf(source["enabled"], SSH_DEFAULTS.enabled),
|
|
44
|
+
host: valueOf(source["host"], SSH_DEFAULTS.host),
|
|
45
|
+
port: valueOf(source["port"], SSH_DEFAULTS.port),
|
|
46
|
+
user: valueOf(source["user"], SSH_DEFAULTS.user),
|
|
47
|
+
keyPath: valueOf(source["keyPath"], SSH_DEFAULTS.keyPath),
|
|
48
|
+
remotePort: valueOf(source["remotePort"], SSH_DEFAULTS.remotePort),
|
|
49
|
+
localPort: valueOf(source["localPort"], SSH_DEFAULTS.localPort)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function configured(config) {
|
|
53
|
+
const source = config ?? {};
|
|
54
|
+
return {
|
|
55
|
+
url: valueOf(source["url"], ""),
|
|
56
|
+
deviceId: valueOf(source["deviceId"], ""),
|
|
57
|
+
token: valueOf(source["token"], ""),
|
|
58
|
+
roots: valueOf(source["roots"], []),
|
|
59
|
+
heartbeatMs: valueOf(source["heartbeatMs"], 15e3),
|
|
60
|
+
reconnectMs: valueOf(source["reconnectMs"], 1e3),
|
|
61
|
+
ssh: {
|
|
62
|
+
...SSH_DEFAULTS,
|
|
63
|
+
...unwrapSsh(source["ssh"])
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// src/client/page-state.ts
|
|
68
|
+
/** A key as one line of the picker: the filename and, when it has one, the comment that tells it apart. */
|
|
69
|
+
function keyLabel(key) {
|
|
70
|
+
return key.comment === "" ? key.name : `${key.name} · ${key.comment}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The address the device will dial, spelled out for the page.
|
|
74
|
+
*
|
|
75
|
+
* With the tunnel on this is the local end of it and `url` is not used — which is the point a person most easily
|
|
76
|
+
* gets wrong, so the page says the address rather than leaving them to infer it.
|
|
77
|
+
*/
|
|
78
|
+
function addressPreview(config) {
|
|
79
|
+
return config.ssh.enabled ? {
|
|
80
|
+
address: `ws://127.0.0.1:${config.ssh.localPort}`,
|
|
81
|
+
derived: true
|
|
82
|
+
} : {
|
|
83
|
+
address: config.url,
|
|
84
|
+
derived: false
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The names of the fields that still have to be filled in, in the order the page shows them.
|
|
89
|
+
*
|
|
90
|
+
* This mirrors `missingConfiguration` for the page's benefit: the host reports the same thing in one sentence, and a
|
|
91
|
+
* form that highlights the empty field is the difference between reading a sentence and fixing it.
|
|
92
|
+
*/
|
|
93
|
+
function emptyFields(config, tokenPresent = true) {
|
|
94
|
+
const empty = [];
|
|
95
|
+
if (config.ssh.enabled) {
|
|
96
|
+
if (config.ssh.host.trim() === "") empty.push("ssh.host");
|
|
97
|
+
if (config.ssh.port === 0) empty.push("ssh.port");
|
|
98
|
+
if (config.ssh.user.trim() === "") empty.push("ssh.user");
|
|
99
|
+
if (config.ssh.keyPath.trim() === "") empty.push("ssh.keyPath");
|
|
100
|
+
} else if (config.url.trim() === "") empty.push("url");
|
|
101
|
+
if (config.deviceId.trim() === "") empty.push("deviceId");
|
|
102
|
+
if (!tokenPresent) empty.push("token");
|
|
103
|
+
return empty;
|
|
104
|
+
}
|
|
105
|
+
function testMessage(result) {
|
|
106
|
+
if (result.ok) return "ok";
|
|
107
|
+
if (/Permission denied \(publickey/.test(result.output)) return "passphrase";
|
|
108
|
+
if (/Could not resolve hostname/.test(result.output)) return "unresolved";
|
|
109
|
+
if (/timed out/i.test(result.output)) return "timeout";
|
|
110
|
+
return result.output.trim() === "" ? "refused" : result.output;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The edits the page sends, one `set` per field it actually holds.
|
|
114
|
+
*
|
|
115
|
+
* This is deliberately not one `set` at the root. A root write replaces the whole section, and the page never
|
|
116
|
+
* receives the token — the host strips it as a secret — so a root write erased the stored token on every save and
|
|
117
|
+
* left the device unable to authenticate. It did exactly that on a real machine: one save took the device offline.
|
|
118
|
+
*
|
|
119
|
+
* Writing only the fields this page holds leaves every other key untouched, the token included. An empty token box
|
|
120
|
+
* means "keep whatever is stored", and that is one value the page cannot read at all.
|
|
121
|
+
*/
|
|
122
|
+
function editsOf(draft) {
|
|
123
|
+
const edits = [
|
|
124
|
+
{
|
|
125
|
+
op: "set",
|
|
126
|
+
path: ["url"],
|
|
127
|
+
value: draft.url.trim()
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
op: "set",
|
|
131
|
+
path: ["deviceId"],
|
|
132
|
+
value: draft.deviceId.trim()
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
op: "set",
|
|
136
|
+
path: ["roots"],
|
|
137
|
+
value: draft.roots.map((root) => ({
|
|
138
|
+
project: root.project.trim(),
|
|
139
|
+
path: root.path.trim()
|
|
140
|
+
}))
|
|
141
|
+
}
|
|
142
|
+
];
|
|
143
|
+
for (const key of [
|
|
144
|
+
"enabled",
|
|
145
|
+
"host",
|
|
146
|
+
"port",
|
|
147
|
+
"user",
|
|
148
|
+
"keyPath",
|
|
149
|
+
"remotePort",
|
|
150
|
+
"localPort"
|
|
151
|
+
]) {
|
|
152
|
+
const value = draft.ssh[key];
|
|
153
|
+
edits.push({
|
|
154
|
+
op: "set",
|
|
155
|
+
path: ["ssh", key],
|
|
156
|
+
value: typeof value === "string" ? value.trim() : value
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const token = typeof draft.token === "string" ? draft.token.trim() : "";
|
|
160
|
+
if (token !== "") edits.push({
|
|
161
|
+
op: "set",
|
|
162
|
+
path: ["token"],
|
|
163
|
+
value: token
|
|
164
|
+
});
|
|
165
|
+
return edits;
|
|
166
|
+
}
|
|
167
|
+
/** The SSH block a fresh page starts from, matching the schema's defaults so an untouched field is not a change. */
|
|
168
|
+
const DEFAULT_SSH = {
|
|
169
|
+
enabled: false,
|
|
170
|
+
host: "",
|
|
171
|
+
port: 22,
|
|
172
|
+
user: "",
|
|
173
|
+
keyPath: "",
|
|
174
|
+
remotePort: 8790,
|
|
175
|
+
localPort: 8790
|
|
176
|
+
};
|
|
177
|
+
// src/client/locales.ts
|
|
178
|
+
/** The device page's own words; the host renders them through its locale service. */
|
|
179
|
+
const zh = {
|
|
180
|
+
title: "设备连接",
|
|
181
|
+
intro: "这台机器用自己的密钥连上枢纽,隧道也由它自己开。填完保存后重启一次 DSH 生效。",
|
|
182
|
+
keysSection: "密钥",
|
|
183
|
+
keysHint: "从这台机器 ~/.ssh 里的密钥中挑一把。枢纽只认公钥,私钥不离开这里。",
|
|
184
|
+
keysEmpty: "~/.ssh 里没有可用的密钥(需要私钥和同名的 .pub 一起在)。",
|
|
185
|
+
keysUnavailable: "读不到密钥列表:配置页需要 DSH 的 Web 界面。",
|
|
186
|
+
keyPath: "私钥路径",
|
|
187
|
+
sshSection: "SSH",
|
|
188
|
+
sshEnabled: "走 SSH 隧道",
|
|
189
|
+
sshEnabledHint: "枢纽只监听 127.0.0.1,所以别的机器要连它就得走隧道。关掉表示枢纽地址本身可达。",
|
|
190
|
+
host: "枢纽机器",
|
|
191
|
+
port: "SSH 端口",
|
|
192
|
+
user: "SSH 用户",
|
|
193
|
+
remotePort: "枢纽端口",
|
|
194
|
+
localPort: "本机端口",
|
|
195
|
+
test: "测试连接",
|
|
196
|
+
testing: "测试中…",
|
|
197
|
+
testPassphrase: "这台机器拒绝了这把钥匙。如果它有口令,请改用 ssh-agent,或换一把没有口令的。",
|
|
198
|
+
testUnresolved: "找不到这台枢纽机器,检查主机名。",
|
|
199
|
+
testTimeout: "超时了,网络不通或被防火墙挡住。",
|
|
200
|
+
testRefused: "连不上。",
|
|
201
|
+
testOk: "连上了。",
|
|
202
|
+
hubSection: "枢纽",
|
|
203
|
+
address: "设备将连接的地址",
|
|
204
|
+
addressDerived: "(由隧道推导,上面的地址不用于连接)",
|
|
205
|
+
deviceId: "设备 id",
|
|
206
|
+
token: "令牌",
|
|
207
|
+
tokenHint: "在枢纽的「设置 › 设备」里登记设备时得到,只显示那一次。",
|
|
208
|
+
tokenStored: "留空即保留已存的令牌",
|
|
209
|
+
tokenKeep: "已存的令牌不会显示在这里。留空表示不动它;只有要换一个时才需要填。",
|
|
210
|
+
rootsSection: "项目路径",
|
|
211
|
+
rootsHint: "枢纽把工单派到项目名;这里的路径是它在您这台机器上的位置。",
|
|
212
|
+
addRoot: "加一个项目",
|
|
213
|
+
project: "项目名",
|
|
214
|
+
path: "本机路径",
|
|
215
|
+
save: "保存",
|
|
216
|
+
saved: "已保存,重启 DSH 后生效。",
|
|
217
|
+
saveFailed: "保存失败。",
|
|
218
|
+
readOnly: "这个宿主不提供配置写入,请手工编辑 profile 的 cordis.patch.yml。",
|
|
219
|
+
loading: "读取配置…",
|
|
220
|
+
missing: "还需要填:"
|
|
221
|
+
};
|
|
222
|
+
const en = {
|
|
223
|
+
title: "Device connection",
|
|
224
|
+
intro: "This machine reaches the hub with its own key and opens its own tunnel. Save, then restart DSH once.",
|
|
225
|
+
keysSection: "Key",
|
|
226
|
+
keysHint: "Pick one of the keys in this machine's ~/.ssh. The hub only ever sees the public half.",
|
|
227
|
+
keysEmpty: "No usable key in ~/.ssh: a private key needs its .pub beside it.",
|
|
228
|
+
keysUnavailable: "The key list needs the DSH web interface.",
|
|
229
|
+
keyPath: "Private key",
|
|
230
|
+
sshSection: "SSH",
|
|
231
|
+
sshEnabled: "Reach the hub through an SSH tunnel",
|
|
232
|
+
sshEnabledHint: "The hub listens on 127.0.0.1 only, so another machine reaches it through a tunnel. Off means the hub address is already reachable.",
|
|
233
|
+
host: "Hub machine",
|
|
234
|
+
port: "SSH port",
|
|
235
|
+
user: "SSH user",
|
|
236
|
+
remotePort: "Hub port",
|
|
237
|
+
localPort: "Local port",
|
|
238
|
+
test: "Test connection",
|
|
239
|
+
testing: "Testing…",
|
|
240
|
+
testPassphrase: "This machine refused the key. If it has a passphrase, use ssh-agent or pick one without.",
|
|
241
|
+
testUnresolved: "The hub machine does not resolve; check the host name.",
|
|
242
|
+
testTimeout: "Timed out: the network is blocked or unreachable.",
|
|
243
|
+
testRefused: "Could not connect.",
|
|
244
|
+
testOk: "Connected.",
|
|
245
|
+
hubSection: "Hub",
|
|
246
|
+
address: "The address this device will dial",
|
|
247
|
+
addressDerived: " (derived from the tunnel; the address above is not used)",
|
|
248
|
+
deviceId: "Device id",
|
|
249
|
+
token: "Token",
|
|
250
|
+
tokenHint: "Shown once when the device was registered, on the hub's Settings › Devices.",
|
|
251
|
+
tokenStored: "leave empty to keep the stored token",
|
|
252
|
+
tokenKeep: "The stored token is never shown here. Leave this empty to keep it; type a new one only to replace it.",
|
|
253
|
+
rootsSection: "Project paths",
|
|
254
|
+
rootsHint: "The hub assigns work by project name; this is where that project lives on this machine.",
|
|
255
|
+
addRoot: "Add a project",
|
|
256
|
+
path: "Local path",
|
|
257
|
+
project: "Project",
|
|
258
|
+
save: "Save",
|
|
259
|
+
saved: "Saved. Restart DSH for it to take effect.",
|
|
260
|
+
saveFailed: "Could not save.",
|
|
261
|
+
readOnly: "This host does not accept configuration writes; edit the profile's cordis.patch.yml by hand.",
|
|
262
|
+
loading: "Reading configuration…",
|
|
263
|
+
missing: "Still needed: "
|
|
264
|
+
};
|
|
265
|
+
// src/client/index.tsx
|
|
266
|
+
/**
|
|
267
|
+
* The device's configuration page, mounted in the DSH Plugin Manager.
|
|
268
|
+
*
|
|
269
|
+
* It does two things the page alone cannot: it lists the keys this machine has (through the device's own route, because
|
|
270
|
+
* a browser cannot read `~/.ssh`) and it tests one authentication (also through the device, because a browser cannot
|
|
271
|
+
* start ssh). Everything it *writes* goes through DSH's own configuration form, so the saved values land in the
|
|
272
|
+
* profile's patch exactly as if they had been typed there by hand.
|
|
273
|
+
*/
|
|
274
|
+
/** The prefix the device's own routes answer under. */
|
|
275
|
+
const API = "/foreman-node/api";
|
|
276
|
+
/** Fetches a key list or runs one test; both need the marker header the device's same-origin check looks for. */
|
|
277
|
+
async function ask(path, init) {
|
|
278
|
+
const response = await fetch(new URL(`${API}/${path}`, window.location.href), {
|
|
279
|
+
...init,
|
|
280
|
+
headers: {
|
|
281
|
+
"x-orchestrator-request": "1",
|
|
282
|
+
...init?.body === void 0 ? {} : { "content-type": "application/json" },
|
|
283
|
+
...init?.headers
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
if (!response.ok) throw new Error(`the device answered ${response.status}`);
|
|
287
|
+
return await response.json();
|
|
288
|
+
}
|
|
289
|
+
const inject = ["locale", "slots"];
|
|
290
|
+
const name = "dsh-foreman-node";
|
|
291
|
+
/** The locale namespace this page registers its dictionaries under. */
|
|
292
|
+
const NS = "dshNode";
|
|
293
|
+
/**
|
|
294
|
+
* The page itself. `t` comes from the host's locale service, like every other plugin's page.
|
|
295
|
+
*
|
|
296
|
+
* `initialKeys` and `keysFailed` exist so a test can render a page without a network or a web server. Production never
|
|
297
|
+
* passes them, so the page fetches as it otherwise would — they change nothing about how it behaves.
|
|
298
|
+
*/
|
|
299
|
+
function DevicePage({ form, t, initialKeys, keysFailed: prefailed }) {
|
|
300
|
+
const saved = form?.state.value === void 0 ? void 0 : configured(form.state.value);
|
|
301
|
+
const editable = form?.state.status === "ready" && form.state.writable;
|
|
302
|
+
const [draft, setDraft] = (0, react.useState)(() => saved === void 0 ? void 0 : {
|
|
303
|
+
...saved,
|
|
304
|
+
token: "",
|
|
305
|
+
ssh: {
|
|
306
|
+
...DEFAULT_SSH,
|
|
307
|
+
...saved.ssh
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
const keepsToken = saved !== void 0;
|
|
311
|
+
const [keys, setKeys] = (0, react.useState)(initialKeys === void 0 ? void 0 : [...initialKeys]);
|
|
312
|
+
const [keysFailed, setKeysFailed] = (0, react.useState)(prefailed === true);
|
|
313
|
+
const [tested, setTested] = (0, react.useState)();
|
|
314
|
+
const [testing, setTesting] = (0, react.useState)(false);
|
|
315
|
+
const [notice, setNotice] = (0, react.useState)();
|
|
316
|
+
(0, react.useEffect)(() => {
|
|
317
|
+
if (saved !== void 0) setDraft((current) => current ?? {
|
|
318
|
+
...saved,
|
|
319
|
+
ssh: {
|
|
320
|
+
...DEFAULT_SSH,
|
|
321
|
+
...saved.ssh
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}, [saved]);
|
|
325
|
+
(0, react.useEffect)(() => {
|
|
326
|
+
if (initialKeys !== void 0 || prefailed === true) return;
|
|
327
|
+
ask("keys").then((answer) => setKeys(answer.keys), () => setKeysFailed(true));
|
|
328
|
+
}, [initialKeys, prefailed]);
|
|
329
|
+
const patch = (change) => setDraft((current) => current === void 0 ? current : {
|
|
330
|
+
...current,
|
|
331
|
+
...change
|
|
332
|
+
});
|
|
333
|
+
const patchSsh = (change) => setDraft((current) => current === void 0 ? current : {
|
|
334
|
+
...current,
|
|
335
|
+
ssh: {
|
|
336
|
+
...current.ssh,
|
|
337
|
+
...change
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
const test = async () => {
|
|
341
|
+
if (draft === void 0) return;
|
|
342
|
+
setTesting(true);
|
|
343
|
+
setTested(void 0);
|
|
344
|
+
try {
|
|
345
|
+
setTested(await ask("test", {
|
|
346
|
+
method: "POST",
|
|
347
|
+
body: JSON.stringify({
|
|
348
|
+
host: draft.ssh.host.trim(),
|
|
349
|
+
port: draft.ssh.port,
|
|
350
|
+
user: draft.ssh.user.trim(),
|
|
351
|
+
keyPath: draft.ssh.keyPath.trim()
|
|
352
|
+
})
|
|
353
|
+
}));
|
|
354
|
+
} catch (error) {
|
|
355
|
+
setTested({
|
|
356
|
+
ok: false,
|
|
357
|
+
output: String(error)
|
|
358
|
+
});
|
|
359
|
+
} finally {
|
|
360
|
+
setTesting(false);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const save = async () => {
|
|
364
|
+
if (draft === void 0 || form === void 0) return;
|
|
365
|
+
setNotice(void 0);
|
|
366
|
+
try {
|
|
367
|
+
const ok = await form.mutate(editsOf(draft), form.state.revision);
|
|
368
|
+
setNotice(ok ? t("saved") : t("saveFailed"));
|
|
369
|
+
} catch {
|
|
370
|
+
setNotice(t("saveFailed"));
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
if (draft === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
374
|
+
"data-foreman-node-page": "loading",
|
|
375
|
+
children: t("loading")
|
|
376
|
+
});
|
|
377
|
+
const address = addressPreview(draft);
|
|
378
|
+
const missing = emptyFields(draft, keepsToken);
|
|
379
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
380
|
+
"data-foreman-node-page": "ready",
|
|
381
|
+
children: [
|
|
382
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("title") }),
|
|
383
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
384
|
+
className: "dt-hint",
|
|
385
|
+
children: t("intro")
|
|
386
|
+
}),
|
|
387
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
388
|
+
"data-section": "keys",
|
|
389
|
+
children: [
|
|
390
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("keysSection") }),
|
|
391
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
392
|
+
className: "dt-hint",
|
|
393
|
+
children: t("keysHint")
|
|
394
|
+
}),
|
|
395
|
+
keysFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
396
|
+
"data-keys": "unavailable",
|
|
397
|
+
children: t("keysUnavailable")
|
|
398
|
+
}) : keys === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("loading") }) : keys.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
399
|
+
"data-keys": "empty",
|
|
400
|
+
children: t("keysEmpty")
|
|
401
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
402
|
+
"aria-label": t("keysSection"),
|
|
403
|
+
"data-node-key": true,
|
|
404
|
+
value: draft.ssh.keyPath,
|
|
405
|
+
onChange: (event) => patchSsh({ keyPath: event.target.value }),
|
|
406
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
407
|
+
value: "",
|
|
408
|
+
children: "—"
|
|
409
|
+
}), keys.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
410
|
+
value: key.path,
|
|
411
|
+
children: keyLabel(key)
|
|
412
|
+
}, key.path))]
|
|
413
|
+
}),
|
|
414
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("keyPath"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
415
|
+
"data-node-key-path": true,
|
|
416
|
+
value: draft.ssh.keyPath,
|
|
417
|
+
disabled: !editable,
|
|
418
|
+
onChange: (event) => patchSsh({ keyPath: event.target.value })
|
|
419
|
+
})] })
|
|
420
|
+
]
|
|
421
|
+
}),
|
|
422
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
423
|
+
"data-section": "ssh",
|
|
424
|
+
children: [
|
|
425
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("sshSection") }),
|
|
426
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
427
|
+
type: "checkbox",
|
|
428
|
+
"data-node-ssh-enabled": true,
|
|
429
|
+
checked: draft.ssh.enabled,
|
|
430
|
+
disabled: !editable,
|
|
431
|
+
onChange: (event) => patchSsh({ enabled: event.target.checked })
|
|
432
|
+
}), t("sshEnabled")] }),
|
|
433
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
434
|
+
className: "dt-hint",
|
|
435
|
+
children: t("sshEnabledHint")
|
|
436
|
+
}),
|
|
437
|
+
draft.ssh.enabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
438
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("host"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
439
|
+
"data-node-host": true,
|
|
440
|
+
value: draft.ssh.host,
|
|
441
|
+
disabled: !editable,
|
|
442
|
+
onChange: (event) => patchSsh({ host: event.target.value })
|
|
443
|
+
})] }),
|
|
444
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("port"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
445
|
+
type: "number",
|
|
446
|
+
"data-node-port": true,
|
|
447
|
+
value: draft.ssh.port,
|
|
448
|
+
disabled: !editable,
|
|
449
|
+
onChange: (event) => patchSsh({ port: Number(event.target.value) || DEFAULT_SSH.port })
|
|
450
|
+
})] }),
|
|
451
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("user"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
452
|
+
"data-node-user": true,
|
|
453
|
+
value: draft.ssh.user,
|
|
454
|
+
disabled: !editable,
|
|
455
|
+
onChange: (event) => patchSsh({ user: event.target.value })
|
|
456
|
+
})] }),
|
|
457
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("remotePort"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
458
|
+
type: "number",
|
|
459
|
+
"data-node-remote-port": true,
|
|
460
|
+
value: draft.ssh.remotePort,
|
|
461
|
+
disabled: !editable,
|
|
462
|
+
onChange: (event) => patchSsh({ remotePort: Number(event.target.value) || DEFAULT_SSH.remotePort })
|
|
463
|
+
})] }),
|
|
464
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("localPort"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
465
|
+
type: "number",
|
|
466
|
+
"data-node-local-port": true,
|
|
467
|
+
value: draft.ssh.localPort,
|
|
468
|
+
disabled: !editable,
|
|
469
|
+
onChange: (event) => patchSsh({ localPort: Number(event.target.value) || DEFAULT_SSH.localPort })
|
|
470
|
+
})] }),
|
|
471
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
472
|
+
type: "button",
|
|
473
|
+
"data-node-test": true,
|
|
474
|
+
"data-testing": testing ? "true" : void 0,
|
|
475
|
+
disabled: !draft.ssh.host.trim() || !draft.ssh.user.trim() || !draft.ssh.keyPath.trim(),
|
|
476
|
+
onClick: () => {
|
|
477
|
+
test();
|
|
478
|
+
},
|
|
479
|
+
children: testing ? t("testing") : t("test")
|
|
480
|
+
}),
|
|
481
|
+
tested !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
482
|
+
"data-test-result": testMessage(tested),
|
|
483
|
+
children: tested.ok ? t("testOk") : testMessage(tested) === "passphrase" ? t("testPassphrase") : testMessage(tested) === "unresolved" ? t("testUnresolved") : testMessage(tested) === "timeout" ? t("testTimeout") : `${t("testRefused")} ${tested.output}`
|
|
484
|
+
})
|
|
485
|
+
] })
|
|
486
|
+
]
|
|
487
|
+
}),
|
|
488
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
489
|
+
"data-section": "hub",
|
|
490
|
+
children: [
|
|
491
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("hubSection") }),
|
|
492
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("address"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
493
|
+
"data-node-url": true,
|
|
494
|
+
value: address.address,
|
|
495
|
+
readOnly: address.derived,
|
|
496
|
+
disabled: !editable || address.derived,
|
|
497
|
+
onChange: (event) => patch({ url: event.target.value })
|
|
498
|
+
})] }),
|
|
499
|
+
address.derived && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
500
|
+
className: "dt-hint",
|
|
501
|
+
children: t("addressDerived")
|
|
502
|
+
}),
|
|
503
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("deviceId"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
504
|
+
"data-node-device": true,
|
|
505
|
+
value: draft.deviceId,
|
|
506
|
+
disabled: !editable,
|
|
507
|
+
onChange: (event) => patch({ deviceId: event.target.value })
|
|
508
|
+
})] }),
|
|
509
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("token"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
510
|
+
type: "password",
|
|
511
|
+
"data-node-token": true,
|
|
512
|
+
value: draft.token,
|
|
513
|
+
disabled: !editable,
|
|
514
|
+
autoComplete: "new-password",
|
|
515
|
+
placeholder: keepsToken ? t("tokenStored") : "",
|
|
516
|
+
onChange: (event) => patch({ token: event.target.value })
|
|
517
|
+
})] }),
|
|
518
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
519
|
+
className: "dt-hint",
|
|
520
|
+
children: keepsToken ? t("tokenKeep") : t("tokenHint")
|
|
521
|
+
})
|
|
522
|
+
]
|
|
523
|
+
}),
|
|
524
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
525
|
+
"data-section": "roots",
|
|
526
|
+
children: [
|
|
527
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("rootsSection") }),
|
|
528
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
529
|
+
className: "dt-hint",
|
|
530
|
+
children: t("rootsHint")
|
|
531
|
+
}),
|
|
532
|
+
draft.roots.map((root, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
533
|
+
"data-root": index,
|
|
534
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("project"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
535
|
+
value: root.project,
|
|
536
|
+
disabled: !editable,
|
|
537
|
+
onChange: (event) => patch({ roots: draft.roots.map((item, at) => at === index ? {
|
|
538
|
+
...item,
|
|
539
|
+
project: event.target.value
|
|
540
|
+
} : item) })
|
|
541
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("path"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
542
|
+
value: root.path,
|
|
543
|
+
disabled: !editable,
|
|
544
|
+
onChange: (event) => patch({ roots: draft.roots.map((item, at) => at === index ? {
|
|
545
|
+
...item,
|
|
546
|
+
path: event.target.value
|
|
547
|
+
} : item) })
|
|
548
|
+
})] })]
|
|
549
|
+
}, index)),
|
|
550
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
551
|
+
type: "button",
|
|
552
|
+
disabled: !editable,
|
|
553
|
+
onClick: () => patch({ roots: [...draft.roots, {
|
|
554
|
+
project: "",
|
|
555
|
+
path: ""
|
|
556
|
+
}] }),
|
|
557
|
+
children: t("addRoot")
|
|
558
|
+
})
|
|
559
|
+
]
|
|
560
|
+
}),
|
|
561
|
+
missing.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
562
|
+
"data-missing": "true",
|
|
563
|
+
children: [t("missing"), missing.join(", ")]
|
|
564
|
+
}),
|
|
565
|
+
editable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
566
|
+
type: "button",
|
|
567
|
+
"data-node-save": true,
|
|
568
|
+
onClick: () => {
|
|
569
|
+
save();
|
|
570
|
+
},
|
|
571
|
+
children: t("save")
|
|
572
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
573
|
+
"data-read-only": "true",
|
|
574
|
+
children: t("readOnly")
|
|
575
|
+
}),
|
|
576
|
+
notice !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
577
|
+
"data-notice": true,
|
|
578
|
+
children: notice
|
|
579
|
+
})
|
|
580
|
+
]
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
function Mounted({ t, view, form = void 0 }) {
|
|
584
|
+
return view === "summary" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DevicePage, {
|
|
585
|
+
t,
|
|
586
|
+
form
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
/** Registers the page for the `dsh-foreman-node` row. */
|
|
590
|
+
function apply(ctx) {
|
|
591
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
592
|
+
zh,
|
|
593
|
+
en
|
|
594
|
+
}), "dsh-foreman-node: dictionaries");
|
|
595
|
+
const t = ctx.locale.bind(NS);
|
|
596
|
+
ctx.slots.inject("plugins.row.config", () => ctx.slots.register({
|
|
597
|
+
name: "plugins.row.config",
|
|
598
|
+
key: "dsh-foreman-node#dsh-foreman-node",
|
|
599
|
+
locale: NS
|
|
600
|
+
}, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Mounted, {
|
|
601
|
+
...props,
|
|
602
|
+
t: props.t ?? t
|
|
603
|
+
})));
|
|
604
|
+
}
|
|
605
|
+
exports.DevicePage = DevicePage;
|
|
606
|
+
exports.apply = apply;
|
|
607
|
+
exports.inject = inject;
|
|
608
|
+
exports.name = name;
|
|
609
|
+
return module.exports;
|
|
610
|
+
}
|
|
611
|
+
});
|