@kwirthmagnify/kwirth-provider-http-pull-push 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/back.js +546 -0
- package/front.js +661 -0
- package/package.json +12 -0
package/back.js
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/back/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
HttpPullPushProvider: () => HttpPullPushProvider,
|
|
34
|
+
default: () => index_default
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
var import_express = __toESM(require("express"), 1);
|
|
38
|
+
|
|
39
|
+
// src/common/HttpPullPush.ts
|
|
40
|
+
var TEST_PREVIEW_CHARS = 1500;
|
|
41
|
+
|
|
42
|
+
// src/common/Validation.ts
|
|
43
|
+
var validateForTest = (config) => {
|
|
44
|
+
const errors = [];
|
|
45
|
+
if (!config) return ["No connection to test"];
|
|
46
|
+
if (!config.url || !/^https?:\/\//i.test(config.url)) errors.push("url must start with http:// or https://");
|
|
47
|
+
if (!(config.timeoutMs > 0)) errors.push("timeout must be greater than zero");
|
|
48
|
+
switch (config.auth?.type) {
|
|
49
|
+
case "basic" /* BASIC */:
|
|
50
|
+
if (!config.auth.username) errors.push("basic auth needs a username");
|
|
51
|
+
break;
|
|
52
|
+
case "bearer" /* BEARER */:
|
|
53
|
+
if (!config.auth.token) errors.push("bearer auth needs a token");
|
|
54
|
+
break;
|
|
55
|
+
case "header" /* HEADER */:
|
|
56
|
+
if (!config.auth.headerName) errors.push("header auth needs a header name");
|
|
57
|
+
break;
|
|
58
|
+
default:
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
return errors;
|
|
62
|
+
};
|
|
63
|
+
var validateConfigs = (configs) => {
|
|
64
|
+
const errors = [];
|
|
65
|
+
const seen = /* @__PURE__ */ new Set();
|
|
66
|
+
for (const config of configs) {
|
|
67
|
+
const name = (config.name ?? "").trim();
|
|
68
|
+
if (!name) {
|
|
69
|
+
errors.push("A connection has no name");
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (seen.has(name)) errors.push(`Duplicated connection name: '${name}'`);
|
|
73
|
+
seen.add(name);
|
|
74
|
+
if (!config.url || !/^https?:\/\//i.test(config.url)) errors.push(`'${name}': url must start with http:// or https://`);
|
|
75
|
+
if (!(config.intervalSeconds > 0)) errors.push(`'${name}': interval must be greater than zero`);
|
|
76
|
+
if (!(config.timeoutMs > 0)) errors.push(`'${name}': timeout must be greater than zero`);
|
|
77
|
+
if (config.retries < 0) errors.push(`'${name}': retries cannot be negative`);
|
|
78
|
+
if (config.timeoutMs > config.intervalSeconds * 1e3) errors.push(`'${name}': timeout is longer than the polling interval`);
|
|
79
|
+
switch (config.auth?.type) {
|
|
80
|
+
case "basic" /* BASIC */:
|
|
81
|
+
if (!config.auth.username) errors.push(`'${name}': basic auth needs a username`);
|
|
82
|
+
break;
|
|
83
|
+
case "bearer" /* BEARER */:
|
|
84
|
+
if (!config.auth.token) errors.push(`'${name}': bearer auth needs a token`);
|
|
85
|
+
break;
|
|
86
|
+
case "header" /* HEADER */:
|
|
87
|
+
if (!config.auth.headerName) errors.push(`'${name}': header auth needs a header name`);
|
|
88
|
+
break;
|
|
89
|
+
case "none" /* NONE */:
|
|
90
|
+
case void 0:
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
errors.push(`'${name}': unknown auth type`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return errors;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// src/back/ConfigStore.ts
|
|
100
|
+
var STORAGE_CONFIGS = "http-pull-push-configs";
|
|
101
|
+
var STORAGE_CREDS = "http-pull-push-creds";
|
|
102
|
+
var splitAuth = (auth) => {
|
|
103
|
+
const { password, token, headerValue, ...rest } = auth ?? { type: "none" /* NONE */ };
|
|
104
|
+
const secretPart = {};
|
|
105
|
+
if (password) secretPart.password = password;
|
|
106
|
+
if (token) secretPart.token = token;
|
|
107
|
+
if (headerValue) secretPart.headerValue = headerValue;
|
|
108
|
+
return { publicPart: rest, secretPart };
|
|
109
|
+
};
|
|
110
|
+
var ConfigStore = class {
|
|
111
|
+
constructor(storage) {
|
|
112
|
+
this.load = async () => {
|
|
113
|
+
if (!this.storage) return [];
|
|
114
|
+
const configs = await this.storage.readStorage(STORAGE_CONFIGS, false) ?? [];
|
|
115
|
+
const creds = await this.storage.readStorage(STORAGE_CREDS, true) ?? {};
|
|
116
|
+
return configs.map((config) => {
|
|
117
|
+
const credential = creds[config.name];
|
|
118
|
+
if (!credential) return config;
|
|
119
|
+
return {
|
|
120
|
+
...config,
|
|
121
|
+
auth: {
|
|
122
|
+
...config.auth,
|
|
123
|
+
...credential.password ? { password: credential.password } : {},
|
|
124
|
+
...credential.token ? { token: credential.token } : {},
|
|
125
|
+
...credential.headerValue ? { headerValue: credential.headerValue } : {}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
this.save = async (configs) => {
|
|
131
|
+
if (!this.storage) throw new Error("no storage available: this provider needs a Kwirth core that injects provider storage");
|
|
132
|
+
const publicConfigs = [];
|
|
133
|
+
const creds = {};
|
|
134
|
+
for (const config of configs) {
|
|
135
|
+
const { publicPart, secretPart } = splitAuth(config.auth);
|
|
136
|
+
publicConfigs.push({ ...config, auth: publicPart });
|
|
137
|
+
if (Object.keys(secretPart).length > 0) creds[config.name] = secretPart;
|
|
138
|
+
}
|
|
139
|
+
await this.storage.writeStorage(STORAGE_CONFIGS, false, publicConfigs);
|
|
140
|
+
await this.storage.writeStorage(STORAGE_CREDS, true, creds);
|
|
141
|
+
};
|
|
142
|
+
this.storage = storage;
|
|
143
|
+
}
|
|
144
|
+
get available() {
|
|
145
|
+
return this.storage !== void 0;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// src/back/HttpFetcher.ts
|
|
150
|
+
var import_http = __toESM(require("http"), 1);
|
|
151
|
+
var import_https = __toESM(require("https"), 1);
|
|
152
|
+
var METHODS_WITH_BODY = ["POST" /* POST */, "PUT" /* PUT */, "PATCH" /* PATCH */];
|
|
153
|
+
var buildHeaders = (config) => {
|
|
154
|
+
const headers = { ...config.headers ?? {} };
|
|
155
|
+
const auth = config.auth;
|
|
156
|
+
if (!auth) return headers;
|
|
157
|
+
switch (auth.type) {
|
|
158
|
+
case "basic" /* BASIC */:
|
|
159
|
+
headers["Authorization"] = "Basic " + Buffer.from(`${auth.username ?? ""}:${auth.password ?? ""}`, "utf8").toString("base64");
|
|
160
|
+
break;
|
|
161
|
+
case "bearer" /* BEARER */:
|
|
162
|
+
headers["Authorization"] = `Bearer ${auth.token ?? ""}`;
|
|
163
|
+
break;
|
|
164
|
+
case "header" /* HEADER */:
|
|
165
|
+
if (auth.headerName) headers[auth.headerName] = auth.headerValue ?? "";
|
|
166
|
+
break;
|
|
167
|
+
case "none" /* NONE */:
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
return headers;
|
|
171
|
+
};
|
|
172
|
+
var httpFetcher = (config) => {
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
let url;
|
|
175
|
+
try {
|
|
176
|
+
url = new URL(config.url);
|
|
177
|
+
} catch {
|
|
178
|
+
reject(new Error(`invalid url '${config.url}'`));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const isHttps = url.protocol === "https:";
|
|
182
|
+
const transport = isHttps ? import_https.default : import_http.default;
|
|
183
|
+
const headers = buildHeaders(config);
|
|
184
|
+
const hasBody = METHODS_WITH_BODY.includes(config.method) && config.body !== void 0;
|
|
185
|
+
if (hasBody && !headers["Content-Type"]) headers["Content-Type"] = "application/json";
|
|
186
|
+
const request = transport.request({
|
|
187
|
+
protocol: url.protocol,
|
|
188
|
+
hostname: url.hostname,
|
|
189
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
190
|
+
path: url.pathname + url.search,
|
|
191
|
+
method: config.method,
|
|
192
|
+
headers,
|
|
193
|
+
...isHttps && config.allowInsecureTls ? { rejectUnauthorized: false } : {}
|
|
194
|
+
}, (response) => {
|
|
195
|
+
const chunks = [];
|
|
196
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
197
|
+
response.on("end", () => {
|
|
198
|
+
resolve({
|
|
199
|
+
status: response.statusCode ?? 0,
|
|
200
|
+
body: Buffer.concat(chunks).toString("utf8")
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
request.setTimeout(config.timeoutMs, () => {
|
|
205
|
+
request.destroy(new Error(`timeout after ${config.timeoutMs}ms`));
|
|
206
|
+
});
|
|
207
|
+
request.on("error", (err) => reject(err));
|
|
208
|
+
if (hasBody) request.write(config.body);
|
|
209
|
+
request.end();
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// src/back/Poller.ts
|
|
214
|
+
var fingerprint = (config) => {
|
|
215
|
+
const headers = Object.keys(config.headers ?? {}).sort().map((k) => `${k}=${config.headers[k]}`).join("&");
|
|
216
|
+
const auth = config.auth ?? {};
|
|
217
|
+
return [
|
|
218
|
+
config.url,
|
|
219
|
+
config.method,
|
|
220
|
+
headers,
|
|
221
|
+
config.body ?? "",
|
|
222
|
+
config.intervalSeconds,
|
|
223
|
+
config.timeoutMs,
|
|
224
|
+
auth.type,
|
|
225
|
+
auth.username ?? "",
|
|
226
|
+
auth.password ?? "",
|
|
227
|
+
auth.token ?? "",
|
|
228
|
+
auth.headerName ?? "",
|
|
229
|
+
auth.headerValue ?? "",
|
|
230
|
+
config.responseType,
|
|
231
|
+
config.emitMode,
|
|
232
|
+
config.retries,
|
|
233
|
+
config.allowInsecureTls
|
|
234
|
+
].join("|");
|
|
235
|
+
};
|
|
236
|
+
var Poller = class {
|
|
237
|
+
constructor(config, fetcher, onEvent) {
|
|
238
|
+
this.running = false;
|
|
239
|
+
this.start = () => {
|
|
240
|
+
if (this.timer) return;
|
|
241
|
+
void this.tick();
|
|
242
|
+
this.timer = setInterval(() => {
|
|
243
|
+
void this.tick();
|
|
244
|
+
}, this.config.intervalSeconds * 1e3);
|
|
245
|
+
};
|
|
246
|
+
this.stop = () => {
|
|
247
|
+
if (this.timer) clearInterval(this.timer);
|
|
248
|
+
this.timer = void 0;
|
|
249
|
+
};
|
|
250
|
+
// ¿Sigue sirviendo este poller para la configuracion dada, o hay que recrearlo?
|
|
251
|
+
this.matches = (config) => fingerprint(this.config) === fingerprint(config);
|
|
252
|
+
// Un ciclo. Si el anterior sigue en vuelo se salta este, para no encadenar peticiones sobre un
|
|
253
|
+
// endpoint lento (el intervalo manda, no la latencia).
|
|
254
|
+
this.tick = async () => {
|
|
255
|
+
if (this.running) return;
|
|
256
|
+
this.running = true;
|
|
257
|
+
try {
|
|
258
|
+
const result = await this.fetchWithRetries();
|
|
259
|
+
const data = this.parse(result.body);
|
|
260
|
+
if (!this.shouldEmit(result.status, result.body)) return;
|
|
261
|
+
this.onEvent({
|
|
262
|
+
config: this.config.name,
|
|
263
|
+
timestamp: Date.now(),
|
|
264
|
+
status: result.status,
|
|
265
|
+
data
|
|
266
|
+
});
|
|
267
|
+
} catch (err) {
|
|
268
|
+
this.lastPayload = void 0;
|
|
269
|
+
this.onEvent({
|
|
270
|
+
config: this.config.name,
|
|
271
|
+
timestamp: Date.now(),
|
|
272
|
+
error: err instanceof Error ? err.message : String(err)
|
|
273
|
+
});
|
|
274
|
+
} finally {
|
|
275
|
+
this.running = false;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
this.fetchWithRetries = async () => {
|
|
279
|
+
let lastError;
|
|
280
|
+
for (let attempt = 0; attempt <= this.config.retries; attempt++) {
|
|
281
|
+
try {
|
|
282
|
+
return await this.fetcher(this.config);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
lastError = err;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
throw lastError;
|
|
288
|
+
};
|
|
289
|
+
this.parse = (body) => {
|
|
290
|
+
if (this.config.responseType === "text" /* TEXT */) return body;
|
|
291
|
+
try {
|
|
292
|
+
return JSON.parse(body);
|
|
293
|
+
} catch {
|
|
294
|
+
return body;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
// En modo onChange se compara el cuerpo crudo con el del ciclo anterior: es exacto y no depende de
|
|
298
|
+
// como serialice el objeto parseado.
|
|
299
|
+
this.shouldEmit = (status, body) => {
|
|
300
|
+
if (this.config.emitMode === "always" /* ALWAYS */) return true;
|
|
301
|
+
const payload = `${status}:${body}`;
|
|
302
|
+
if (this.lastPayload === payload) return false;
|
|
303
|
+
this.lastPayload = payload;
|
|
304
|
+
return true;
|
|
305
|
+
};
|
|
306
|
+
this.config = config;
|
|
307
|
+
this.fetcher = fetcher;
|
|
308
|
+
this.onEvent = onEvent;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// src/back/index.ts
|
|
313
|
+
var HttpPullPushProvider = class {
|
|
314
|
+
constructor(_clusterInfo, _kwirthData, storage, fetcher = httpFetcher) {
|
|
315
|
+
this.id = "http-pull-push";
|
|
316
|
+
this.providesRouter = false;
|
|
317
|
+
this.router = void 0;
|
|
318
|
+
this.routerAlias = void 0;
|
|
319
|
+
this.requiresApiKeyApi = false;
|
|
320
|
+
this.apiKeyApi = void 0;
|
|
321
|
+
this.configRouter = import_express.default.Router();
|
|
322
|
+
this.configs = /* @__PURE__ */ new Map();
|
|
323
|
+
this.subscribers = /* @__PURE__ */ new Map();
|
|
324
|
+
this.pollers = /* @__PURE__ */ new Map();
|
|
325
|
+
this.started = false;
|
|
326
|
+
// ── IProvider ───────────────────────────────────────────────────────────────
|
|
327
|
+
this.startProvider = async () => {
|
|
328
|
+
try {
|
|
329
|
+
const configs = await this.store.load();
|
|
330
|
+
this.configs = new Map(configs.map((c) => [c.name, c]));
|
|
331
|
+
console.log(`[http-pull-push] ${this.configs.size} connection(s) loaded`);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
console.error(`[http-pull-push] Could not load connections: ${err}`);
|
|
334
|
+
}
|
|
335
|
+
this.started = true;
|
|
336
|
+
this.reconcile();
|
|
337
|
+
};
|
|
338
|
+
this.stopProvider = async () => {
|
|
339
|
+
for (const poller of this.pollers.values()) poller.stop();
|
|
340
|
+
this.pollers.clear();
|
|
341
|
+
this.subscribers.clear();
|
|
342
|
+
this.started = false;
|
|
343
|
+
};
|
|
344
|
+
this.addSubscriber = async (c, data) => {
|
|
345
|
+
this.subscribers.set(c, { configs: this.parseSelection(data) });
|
|
346
|
+
this.warnUnknown(data);
|
|
347
|
+
this.reconcile();
|
|
348
|
+
};
|
|
349
|
+
this.removeSubscriber = async (c) => {
|
|
350
|
+
this.subscribers.delete(c);
|
|
351
|
+
this.reconcile();
|
|
352
|
+
};
|
|
353
|
+
// Permite a un canal cambiar su seleccion sin desuscribirse y volver a suscribirse.
|
|
354
|
+
this.updateSubscription = async (c, data) => {
|
|
355
|
+
if (!this.subscribers.has(c)) return;
|
|
356
|
+
this.subscribers.set(c, { configs: this.parseSelection(data) });
|
|
357
|
+
this.warnUnknown(data);
|
|
358
|
+
this.reconcile();
|
|
359
|
+
};
|
|
360
|
+
/*
|
|
361
|
+
Ayuda para quien se suscribe. Merece la pena declararla porque la semantica de 'configs' no se
|
|
362
|
+
adivina: un array VACIO no significa "todo", significa "nada"; y las conexiones las crea un
|
|
363
|
+
administrador en el dialogo del provider, asi que hay que decir sus nombres.
|
|
364
|
+
*/
|
|
365
|
+
this.getSubscriptionHelp = () => ({
|
|
366
|
+
usage: `Subscribe by CONNECTION NAME. The connections are created by an administrator in this provider's dialog (gear in Manage extensions > Providers), so their names are the ones listed there.
|
|
367
|
+
|
|
368
|
+
Selection semantics:
|
|
369
|
+
- configs: ["a","b"] -> only those connections
|
|
370
|
+
- configs: [] -> NOTHING is delivered (an empty array is not "everything")
|
|
371
|
+
- configs absent -> every enabled connection, including ones created later
|
|
372
|
+
|
|
373
|
+
Each event arrives wrapped, so a subscriber to several connections can tell them apart:
|
|
374
|
+
success: { config, timestamp, status, data } data = parsed body (json) or raw text
|
|
375
|
+
failure: { config, timestamp, error } no data, no status
|
|
376
|
+
|
|
377
|
+
Gotchas:
|
|
378
|
+
- Polling is LAZY: a connection is only polled while at least one subscriber wants it, so nothing happens until you subscribe (and the first pull is immediate).
|
|
379
|
+
- A DISABLED connection delivers nothing even if you name it explicitly.
|
|
380
|
+
- Naming a connection that does not exist is ignored and logged, never an error.
|
|
381
|
+
- With emitMode=onChange the connection stays quiet while the answer is identical.`,
|
|
382
|
+
example: { configs: ["stocks", "rss"] },
|
|
383
|
+
fields: [
|
|
384
|
+
{
|
|
385
|
+
name: "configs",
|
|
386
|
+
type: "string[]",
|
|
387
|
+
description: "Connection names to receive. Empty array = nothing; omit the field = all enabled connections."
|
|
388
|
+
}
|
|
389
|
+
]
|
|
390
|
+
});
|
|
391
|
+
// ── Configuracion (capa 1) ──────────────────────────────────────────────────
|
|
392
|
+
this.addConfigRoutes = () => {
|
|
393
|
+
this.configRouter.route("/configs").get(async (_req, res) => {
|
|
394
|
+
res.status(200).json([...this.configs.values()]);
|
|
395
|
+
}).put(async (req, res) => {
|
|
396
|
+
try {
|
|
397
|
+
const incoming = req.body;
|
|
398
|
+
if (!Array.isArray(incoming)) {
|
|
399
|
+
res.status(400).json({ errors: ["Body must be an array of connections"] });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const errors = validateConfigs(incoming);
|
|
403
|
+
if (errors.length > 0) {
|
|
404
|
+
res.status(400).json({ errors });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
await this.applyConfigs(incoming);
|
|
408
|
+
res.status(200).json({ ok: true });
|
|
409
|
+
} catch (err) {
|
|
410
|
+
console.error(`[http-pull-push] Error saving connections: ${err}`);
|
|
411
|
+
res.status(500).json({ errors: [String(err)] });
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
this.configRouter.route("/test").post(async (req, res) => {
|
|
415
|
+
try {
|
|
416
|
+
const result = await this.testConnection(req.body);
|
|
417
|
+
res.status(200).json(result);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
console.error(`[http-pull-push] Error testing connection: ${err}`);
|
|
420
|
+
res.status(500).json({ ok: false, durationMs: 0, error: String(err) });
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
};
|
|
424
|
+
/*
|
|
425
|
+
Prueba una conexion HACIENDO LA PETICION DE VERDAD, una sola vez y sin persistir nada.
|
|
426
|
+
|
|
427
|
+
La ejecuta el back a proposito: es el back quien tiene la red del cluster, los certificados y la
|
|
428
|
+
identidad con los que se hara el pull real, asi que probar desde el navegador no demostraria nada
|
|
429
|
+
(otra red, otro almacen de CAs, otras reglas de salida).
|
|
430
|
+
|
|
431
|
+
Se ignoran los reintentos: en una prueba interesa el primer resultado, no la insistencia.
|
|
432
|
+
*/
|
|
433
|
+
this.testConnection = async (config) => {
|
|
434
|
+
const errors = validateForTest(config);
|
|
435
|
+
if (errors.length > 0) return { ok: false, durationMs: 0, error: errors.join("; ") };
|
|
436
|
+
const started = Date.now();
|
|
437
|
+
try {
|
|
438
|
+
const result = await this.fetcher({ ...config, retries: 0 });
|
|
439
|
+
const body = result.body ?? "";
|
|
440
|
+
let jsonParsed = false;
|
|
441
|
+
try {
|
|
442
|
+
JSON.parse(body);
|
|
443
|
+
jsonParsed = true;
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
ok: true,
|
|
448
|
+
status: result.status,
|
|
449
|
+
durationMs: Date.now() - started,
|
|
450
|
+
bytes: Buffer.byteLength(body, "utf8"),
|
|
451
|
+
preview: body.slice(0, TEST_PREVIEW_CHARS),
|
|
452
|
+
jsonParsed
|
|
453
|
+
};
|
|
454
|
+
} catch (err) {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
durationMs: Date.now() - started,
|
|
458
|
+
error: err instanceof Error ? err.message : String(err)
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
/*
|
|
463
|
+
Guarda y aplica en caliente: no hay que reiniciar Kwirth para que una conexion nueva empiece a
|
|
464
|
+
consultarse, ni para que una que se deshabilita deje de hacerlo.
|
|
465
|
+
*/
|
|
466
|
+
this.applyConfigs = async (configs) => {
|
|
467
|
+
await this.store.save(configs);
|
|
468
|
+
this.configs = new Map(configs.map((c) => [c.name, c]));
|
|
469
|
+
this.reconcile();
|
|
470
|
+
};
|
|
471
|
+
this.getConfigs = () => [...this.configs.values()];
|
|
472
|
+
// Solo los nombres: alimenta el contador de la tarjeta en el gestor de extensiones.
|
|
473
|
+
this.getConfigNames = () => [...this.configs.keys()];
|
|
474
|
+
// ── Reconciliacion de pollers ───────────────────────────────────────────────
|
|
475
|
+
/*
|
|
476
|
+
Deja los pollers en marcha exactamente iguales a lo que dicen la configuracion y las suscripciones:
|
|
477
|
+
arranca los que faltan, para los que sobran y recrea los que han cambiado de parametros.
|
|
478
|
+
*/
|
|
479
|
+
this.reconcile = () => {
|
|
480
|
+
if (!this.started) return;
|
|
481
|
+
for (const [name, poller] of this.pollers) {
|
|
482
|
+
const config = this.configs.get(name);
|
|
483
|
+
if (!config || !this.shouldRun(config)) {
|
|
484
|
+
poller.stop();
|
|
485
|
+
this.pollers.delete(name);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
for (const config of this.configs.values()) {
|
|
489
|
+
if (!this.shouldRun(config)) continue;
|
|
490
|
+
const existing = this.pollers.get(config.name);
|
|
491
|
+
if (existing) {
|
|
492
|
+
if (!existing.matches(config)) {
|
|
493
|
+
existing.stop();
|
|
494
|
+
this.pollers.delete(config.name);
|
|
495
|
+
} else {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const poller = new Poller(config, this.fetcher, (event) => this.dispatch(event));
|
|
500
|
+
this.pollers.set(config.name, poller);
|
|
501
|
+
poller.start();
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
this.shouldRun = (config) => {
|
|
505
|
+
if (!config.enabled) return false;
|
|
506
|
+
return this.countListeners(config.name) > 0;
|
|
507
|
+
};
|
|
508
|
+
this.countListeners = (name) => {
|
|
509
|
+
let count = 0;
|
|
510
|
+
for (const entry of this.subscribers.values()) {
|
|
511
|
+
if (entry.configs === void 0 || entry.configs.has(name)) count++;
|
|
512
|
+
}
|
|
513
|
+
return count;
|
|
514
|
+
};
|
|
515
|
+
// ── Entrega (capa 2) ────────────────────────────────────────────────────────
|
|
516
|
+
this.dispatch = (event) => {
|
|
517
|
+
for (const [subscriber, entry] of this.subscribers) {
|
|
518
|
+
if (entry.configs !== void 0 && !entry.configs.has(event.config)) continue;
|
|
519
|
+
try {
|
|
520
|
+
subscriber.processProviderEvent(this.id, event);
|
|
521
|
+
} catch (err) {
|
|
522
|
+
console.error(`[http-pull-push] Subscriber failed processing '${event.config}': ${err}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
this.parseSelection = (data) => {
|
|
527
|
+
if (!data || data.configs === void 0) return void 0;
|
|
528
|
+
return new Set(data.configs);
|
|
529
|
+
};
|
|
530
|
+
// Suscribirse a algo que no existe (o que se borro despues) no es un error: se ignora y se deja traza.
|
|
531
|
+
this.warnUnknown = (data) => {
|
|
532
|
+
if (!data?.configs) return;
|
|
533
|
+
for (const name of data.configs) {
|
|
534
|
+
if (!this.configs.has(name)) console.log(`[http-pull-push] Subscription to unknown connection '${name}' \u2014 ignored`);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
this.store = new ConfigStore(storage);
|
|
538
|
+
this.fetcher = fetcher;
|
|
539
|
+
this.addConfigRoutes();
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
var index_default = HttpPullPushProvider;
|
|
543
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
544
|
+
0 && (module.exports = {
|
|
545
|
+
HttpPullPushProvider
|
|
546
|
+
});
|
package/front.js
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
10
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
|
|
29
|
+
// kwirth-globals:react
|
|
30
|
+
var require_react = __commonJS({
|
|
31
|
+
"kwirth-globals:react"(exports, module) {
|
|
32
|
+
module.exports = window.__kwirth__.React;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// kwirth-globals:@mui/material
|
|
37
|
+
var require_material = __commonJS({
|
|
38
|
+
"kwirth-globals:@mui/material"(exports, module) {
|
|
39
|
+
module.exports = window.__kwirth__.MUI.material;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// kwirth-globals:@mui/icons-material
|
|
44
|
+
var require_icons_material = __commonJS({
|
|
45
|
+
"kwirth-globals:@mui/icons-material"(exports, module) {
|
|
46
|
+
module.exports = window.__kwirth__.MUI.icons;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// src/front/HttpPullPushConfigDialog.tsx
|
|
51
|
+
var import_react3 = __toESM(require_react(), 1);
|
|
52
|
+
var import_material3 = __toESM(require_material(), 1);
|
|
53
|
+
var import_icons_material3 = __toESM(require_icons_material(), 1);
|
|
54
|
+
|
|
55
|
+
// src/common/HttpPullPush.ts
|
|
56
|
+
var EHttpMethod = /* @__PURE__ */ ((EHttpMethod2) => {
|
|
57
|
+
EHttpMethod2["GET"] = "GET";
|
|
58
|
+
EHttpMethod2["POST"] = "POST";
|
|
59
|
+
EHttpMethod2["PUT"] = "PUT";
|
|
60
|
+
EHttpMethod2["PATCH"] = "PATCH";
|
|
61
|
+
EHttpMethod2["DELETE"] = "DELETE";
|
|
62
|
+
return EHttpMethod2;
|
|
63
|
+
})(EHttpMethod || {});
|
|
64
|
+
var DEFAULT_INTERVAL_SECONDS = 60;
|
|
65
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
66
|
+
var newHttpPullConfig = (name) => ({
|
|
67
|
+
name,
|
|
68
|
+
enabled: true,
|
|
69
|
+
url: "",
|
|
70
|
+
method: "GET" /* GET */,
|
|
71
|
+
headers: {},
|
|
72
|
+
intervalSeconds: DEFAULT_INTERVAL_SECONDS,
|
|
73
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
74
|
+
auth: { type: "none" /* NONE */ },
|
|
75
|
+
responseType: "json" /* JSON */,
|
|
76
|
+
emitMode: "always" /* ALWAYS */,
|
|
77
|
+
retries: 0,
|
|
78
|
+
allowInsecureTls: false
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// src/common/Validation.ts
|
|
82
|
+
var validateConfigs = (configs) => {
|
|
83
|
+
const errors = [];
|
|
84
|
+
const seen = /* @__PURE__ */ new Set();
|
|
85
|
+
for (const config of configs) {
|
|
86
|
+
const name = (config.name ?? "").trim();
|
|
87
|
+
if (!name) {
|
|
88
|
+
errors.push("A connection has no name");
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (seen.has(name)) errors.push(`Duplicated connection name: '${name}'`);
|
|
92
|
+
seen.add(name);
|
|
93
|
+
if (!config.url || !/^https?:\/\//i.test(config.url)) errors.push(`'${name}': url must start with http:// or https://`);
|
|
94
|
+
if (!(config.intervalSeconds > 0)) errors.push(`'${name}': interval must be greater than zero`);
|
|
95
|
+
if (!(config.timeoutMs > 0)) errors.push(`'${name}': timeout must be greater than zero`);
|
|
96
|
+
if (config.retries < 0) errors.push(`'${name}': retries cannot be negative`);
|
|
97
|
+
if (config.timeoutMs > config.intervalSeconds * 1e3) errors.push(`'${name}': timeout is longer than the polling interval`);
|
|
98
|
+
switch (config.auth?.type) {
|
|
99
|
+
case "basic" /* BASIC */:
|
|
100
|
+
if (!config.auth.username) errors.push(`'${name}': basic auth needs a username`);
|
|
101
|
+
break;
|
|
102
|
+
case "bearer" /* BEARER */:
|
|
103
|
+
if (!config.auth.token) errors.push(`'${name}': bearer auth needs a token`);
|
|
104
|
+
break;
|
|
105
|
+
case "header" /* HEADER */:
|
|
106
|
+
if (!config.auth.headerName) errors.push(`'${name}': header auth needs a header name`);
|
|
107
|
+
break;
|
|
108
|
+
case "none" /* NONE */:
|
|
109
|
+
case void 0:
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
errors.push(`'${name}': unknown auth type`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return errors;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// src/front/HeaderEditor.tsx
|
|
119
|
+
var import_react = __toESM(require_react(), 1);
|
|
120
|
+
var import_material = __toESM(require_material(), 1);
|
|
121
|
+
var import_icons_material = __toESM(require_icons_material(), 1);
|
|
122
|
+
var HeaderEditor = ({ headers, onChange }) => {
|
|
123
|
+
const [name, setName] = (0, import_react.useState)("");
|
|
124
|
+
const [value, setValue] = (0, import_react.useState)("");
|
|
125
|
+
const entries = Object.entries(headers ?? {});
|
|
126
|
+
const add = () => {
|
|
127
|
+
const key = name.trim();
|
|
128
|
+
if (!key) return;
|
|
129
|
+
onChange({ ...headers, [key]: value });
|
|
130
|
+
setName("");
|
|
131
|
+
setValue("");
|
|
132
|
+
};
|
|
133
|
+
const remove = (key) => {
|
|
134
|
+
const next = { ...headers };
|
|
135
|
+
delete next[key];
|
|
136
|
+
onChange(next);
|
|
137
|
+
};
|
|
138
|
+
return /* @__PURE__ */ import_react.default.createElement(import_material.Box, null, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "subtitle2", sx: { mb: 1 } }, "Headers"), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { spacing: 0.5, sx: { mb: 1 } }, entries.length === 0 && /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2", color: "text.secondary" }, "No headers."), entries.map(([key, val]) => /* @__PURE__ */ import_react.default.createElement(
|
|
139
|
+
import_material.Stack,
|
|
140
|
+
{
|
|
141
|
+
key,
|
|
142
|
+
direction: "row",
|
|
143
|
+
alignItems: "center",
|
|
144
|
+
spacing: 1,
|
|
145
|
+
sx: { px: 1, py: 0.5, border: 1, borderColor: "divider", borderRadius: 1 }
|
|
146
|
+
},
|
|
147
|
+
/* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2", sx: { fontWeight: 500, minWidth: 140 } }, key),
|
|
148
|
+
/* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2", sx: { flex: 1, wordBreak: "break-all" } }, val),
|
|
149
|
+
/* @__PURE__ */ import_react.default.createElement(import_material.Tooltip, { title: "Remove header" }, /* @__PURE__ */ import_react.default.createElement(import_material.IconButton, { size: "small", color: "error", onClick: () => remove(key) }, /* @__PURE__ */ import_react.default.createElement(import_icons_material.Delete, { fontSize: "small" })))
|
|
150
|
+
))), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 1, alignItems: "center" }, /* @__PURE__ */ import_react.default.createElement(
|
|
151
|
+
import_material.TextField,
|
|
152
|
+
{
|
|
153
|
+
size: "small",
|
|
154
|
+
label: "Name",
|
|
155
|
+
value: name,
|
|
156
|
+
sx: { width: 180 },
|
|
157
|
+
onChange: (e) => setName(e.target.value)
|
|
158
|
+
}
|
|
159
|
+
), /* @__PURE__ */ import_react.default.createElement(
|
|
160
|
+
import_material.TextField,
|
|
161
|
+
{
|
|
162
|
+
size: "small",
|
|
163
|
+
label: "Value",
|
|
164
|
+
value,
|
|
165
|
+
sx: { flex: 1 },
|
|
166
|
+
onChange: (e) => setValue(e.target.value)
|
|
167
|
+
}
|
|
168
|
+
), /* @__PURE__ */ import_react.default.createElement(import_material.Tooltip, { title: "Add header" }, /* @__PURE__ */ import_react.default.createElement("span", null, /* @__PURE__ */ import_react.default.createElement(import_material.IconButton, { size: "small", onClick: add, disabled: !name.trim() }, /* @__PURE__ */ import_react.default.createElement(import_icons_material.Add, { fontSize: "small" }))))));
|
|
169
|
+
};
|
|
170
|
+
var HeaderEditor_default = HeaderEditor;
|
|
171
|
+
|
|
172
|
+
// src/front/SecretField.tsx
|
|
173
|
+
var import_react2 = __toESM(require_react(), 1);
|
|
174
|
+
var import_material2 = __toESM(require_material(), 1);
|
|
175
|
+
var import_icons_material2 = __toESM(require_icons_material(), 1);
|
|
176
|
+
var SecretField = ({ label, value, onChange, width }) => {
|
|
177
|
+
const [visible, setVisible] = (0, import_react2.useState)(false);
|
|
178
|
+
return /* @__PURE__ */ import_react2.default.createElement(
|
|
179
|
+
import_material2.TextField,
|
|
180
|
+
{
|
|
181
|
+
size: "small",
|
|
182
|
+
label,
|
|
183
|
+
value: value ?? "",
|
|
184
|
+
sx: { width: width ?? 260 },
|
|
185
|
+
type: visible ? "text" : "password",
|
|
186
|
+
onChange: (e) => onChange(e.target.value),
|
|
187
|
+
slotProps: {
|
|
188
|
+
input: {
|
|
189
|
+
endAdornment: /* @__PURE__ */ import_react2.default.createElement(import_material2.InputAdornment, { position: "end" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Tooltip, { title: visible ? "Hide" : "Show" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { size: "small", edge: "end", onClick: () => setVisible(!visible) }, visible ? /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.VisibilityOff, { fontSize: "small" }) : /* @__PURE__ */ import_react2.default.createElement(import_icons_material2.Visibility, { fontSize: "small" }))))
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
var SecretField_default = SecretField;
|
|
196
|
+
|
|
197
|
+
// src/front/HttpPullPushConfigDialog.tsx
|
|
198
|
+
var CONFIG_URL = (backendUrl) => `${backendUrl}/core/providerconfig/http-pull-push/configs`;
|
|
199
|
+
var TEST_URL = (backendUrl) => `${backendUrl}/core/providerconfig/http-pull-push/test`;
|
|
200
|
+
var EXPORT_VERSION = 1;
|
|
201
|
+
var METHODS_WITH_BODY = ["POST" /* POST */, "PUT" /* PUT */, "PATCH" /* PATCH */];
|
|
202
|
+
var authHeaders = (accessString) => ({
|
|
203
|
+
Authorization: accessString ? `Bearer ${accessString}` : "",
|
|
204
|
+
"Content-Type": "application/json",
|
|
205
|
+
"X-Kwirth-App": "true"
|
|
206
|
+
});
|
|
207
|
+
var stripCredentials = (config) => ({
|
|
208
|
+
...config,
|
|
209
|
+
auth: {
|
|
210
|
+
...config.auth,
|
|
211
|
+
password: void 0,
|
|
212
|
+
token: void 0,
|
|
213
|
+
headerValue: void 0
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
var HttpPullPushConfigDialog = ({ onClose, backendUrl, accessString }) => {
|
|
217
|
+
const [configs, setConfigs] = (0, import_react3.useState)([]);
|
|
218
|
+
const [loading, setLoading] = (0, import_react3.useState)(true);
|
|
219
|
+
const [saving, setSaving] = (0, import_react3.useState)(false);
|
|
220
|
+
const [deletingName, setDeletingName] = (0, import_react3.useState)();
|
|
221
|
+
const [error, setError] = (0, import_react3.useState)();
|
|
222
|
+
const [errors, setErrors] = (0, import_react3.useState)([]);
|
|
223
|
+
const [showForm, setShowForm] = (0, import_react3.useState)(false);
|
|
224
|
+
const [editingName, setEditingName] = (0, import_react3.useState)();
|
|
225
|
+
const [form, setForm] = (0, import_react3.useState)(newHttpPullConfig(""));
|
|
226
|
+
const [testing, setTesting] = (0, import_react3.useState)(false);
|
|
227
|
+
const [testResult, setTestResult] = (0, import_react3.useState)();
|
|
228
|
+
const [exportOpen, setExportOpen] = (0, import_react3.useState)(false);
|
|
229
|
+
const [exportSelected, setExportSelected] = (0, import_react3.useState)(/* @__PURE__ */ new Set());
|
|
230
|
+
const [exportWithCredentials, setExportWithCredentials] = (0, import_react3.useState)(false);
|
|
231
|
+
const [importData, setImportData] = (0, import_react3.useState)();
|
|
232
|
+
const [importSelected, setImportSelected] = (0, import_react3.useState)(/* @__PURE__ */ new Set());
|
|
233
|
+
const importFileRef = (0, import_react3.useRef)(null);
|
|
234
|
+
(0, import_react3.useEffect)(() => {
|
|
235
|
+
fetch(CONFIG_URL(backendUrl), { headers: { Authorization: accessString ? `Bearer ${accessString}` : "", "X-Kwirth-App": "true" } }).then((r) => r.ok ? r.json() : Promise.reject(`HTTP ${r.status}`)).then((data) => setConfigs(Array.isArray(data) ? data : [])).catch((err) => setError(`Failed to load connections: ${err}`)).finally(() => setLoading(false));
|
|
236
|
+
}, []);
|
|
237
|
+
const persist = async (next) => {
|
|
238
|
+
const found = validateConfigs(next);
|
|
239
|
+
setErrors(found);
|
|
240
|
+
if (found.length > 0) return false;
|
|
241
|
+
setSaving(true);
|
|
242
|
+
setError(void 0);
|
|
243
|
+
try {
|
|
244
|
+
const res = await fetch(CONFIG_URL(backendUrl), {
|
|
245
|
+
method: "PUT",
|
|
246
|
+
headers: authHeaders(accessString),
|
|
247
|
+
body: JSON.stringify(next)
|
|
248
|
+
});
|
|
249
|
+
if (!res.ok) {
|
|
250
|
+
const payload = await res.json().catch(() => void 0);
|
|
251
|
+
if (payload?.errors) {
|
|
252
|
+
setErrors(payload.errors);
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
throw new Error(`HTTP ${res.status}`);
|
|
256
|
+
}
|
|
257
|
+
setConfigs(next);
|
|
258
|
+
return true;
|
|
259
|
+
} catch (err) {
|
|
260
|
+
setError(`Failed to save: ${err}`);
|
|
261
|
+
return false;
|
|
262
|
+
} finally {
|
|
263
|
+
setSaving(false);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
const freeName = (base) => {
|
|
267
|
+
const used = new Set(configs.map((c) => c.name));
|
|
268
|
+
if (!used.has(base)) return base;
|
|
269
|
+
let index = 2;
|
|
270
|
+
while (used.has(`${base}-${index}`)) index++;
|
|
271
|
+
return `${base}-${index}`;
|
|
272
|
+
};
|
|
273
|
+
const startNew = () => {
|
|
274
|
+
setForm(newHttpPullConfig(freeName("connection")));
|
|
275
|
+
setEditingName(void 0);
|
|
276
|
+
setErrors([]);
|
|
277
|
+
setShowForm(true);
|
|
278
|
+
};
|
|
279
|
+
const startEdit = (config) => {
|
|
280
|
+
setForm({ ...config });
|
|
281
|
+
setEditingName(config.name);
|
|
282
|
+
setErrors([]);
|
|
283
|
+
setShowForm(true);
|
|
284
|
+
};
|
|
285
|
+
const startClone = () => {
|
|
286
|
+
setForm((prev) => ({ ...prev, name: freeName(`${prev.name}-copy`) }));
|
|
287
|
+
setEditingName(void 0);
|
|
288
|
+
setErrors([]);
|
|
289
|
+
setShowForm(true);
|
|
290
|
+
};
|
|
291
|
+
const submitForm = async () => {
|
|
292
|
+
const next = editingName ? configs.map((c) => c.name === editingName ? form : c) : [...configs, form];
|
|
293
|
+
if (await persist(next)) {
|
|
294
|
+
setShowForm(false);
|
|
295
|
+
setEditingName(void 0);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
const removeConnection = async (name) => {
|
|
299
|
+
setDeletingName(name);
|
|
300
|
+
const ok = await persist(configs.filter((c) => c.name !== name));
|
|
301
|
+
setDeletingName(void 0);
|
|
302
|
+
if (ok && editingName === name) {
|
|
303
|
+
setShowForm(false);
|
|
304
|
+
setEditingName(void 0);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
const patch = (changes) => {
|
|
308
|
+
setForm((prev) => ({ ...prev, ...changes }));
|
|
309
|
+
setTestResult(void 0);
|
|
310
|
+
};
|
|
311
|
+
const patchAuth = (changes) => {
|
|
312
|
+
setForm((prev) => ({ ...prev, auth: { ...prev.auth, ...changes } }));
|
|
313
|
+
setTestResult(void 0);
|
|
314
|
+
};
|
|
315
|
+
const testConnection = async () => {
|
|
316
|
+
setTesting(true);
|
|
317
|
+
setTestResult(void 0);
|
|
318
|
+
setError(void 0);
|
|
319
|
+
try {
|
|
320
|
+
const res = await fetch(TEST_URL(backendUrl), {
|
|
321
|
+
method: "POST",
|
|
322
|
+
headers: authHeaders(accessString),
|
|
323
|
+
body: JSON.stringify(form)
|
|
324
|
+
});
|
|
325
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
326
|
+
setTestResult(await res.json());
|
|
327
|
+
} catch (err) {
|
|
328
|
+
setError(`Could not run the test: ${err}`);
|
|
329
|
+
} finally {
|
|
330
|
+
setTesting(false);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const doExport = () => {
|
|
334
|
+
const chosen = configs.filter((c) => exportSelected.has(c.name));
|
|
335
|
+
const payload = {
|
|
336
|
+
provider: "http-pull-push",
|
|
337
|
+
version: EXPORT_VERSION,
|
|
338
|
+
credentialsIncluded: exportWithCredentials,
|
|
339
|
+
configs: exportWithCredentials ? chosen : chosen.map(stripCredentials)
|
|
340
|
+
};
|
|
341
|
+
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
|
|
342
|
+
const url = URL.createObjectURL(blob);
|
|
343
|
+
const link = document.createElement("a");
|
|
344
|
+
link.href = url;
|
|
345
|
+
link.download = "http-pull-push-connections.json";
|
|
346
|
+
link.click();
|
|
347
|
+
URL.revokeObjectURL(url);
|
|
348
|
+
setExportOpen(false);
|
|
349
|
+
};
|
|
350
|
+
const openImport = (file) => {
|
|
351
|
+
const reader = new FileReader();
|
|
352
|
+
reader.onload = () => {
|
|
353
|
+
try {
|
|
354
|
+
const parsed = JSON.parse(String(reader.result));
|
|
355
|
+
if (!Array.isArray(parsed?.configs)) throw new Error("no connections in the file");
|
|
356
|
+
if (parsed.provider && parsed.provider !== "http-pull-push") throw new Error(`the file belongs to provider '${parsed.provider}'`);
|
|
357
|
+
setImportData(parsed);
|
|
358
|
+
setImportSelected(new Set(parsed.configs.map((c) => c.name)));
|
|
359
|
+
} catch (err) {
|
|
360
|
+
setError(`Invalid import file: ${err}`);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
reader.readAsText(file);
|
|
364
|
+
if (importFileRef.current) importFileRef.current.value = "";
|
|
365
|
+
};
|
|
366
|
+
const doImport = async () => {
|
|
367
|
+
if (!importData) return;
|
|
368
|
+
const chosen = importData.configs.filter((c) => importSelected.has(c.name));
|
|
369
|
+
const byName = new Map(configs.map((c) => [c.name, c]));
|
|
370
|
+
for (const config of chosen) byName.set(config.name, { ...newHttpPullConfig(config.name), ...config });
|
|
371
|
+
if (await persist([...byName.values()])) setImportData(void 0);
|
|
372
|
+
};
|
|
373
|
+
const showBody = METHODS_WITH_BODY.includes(form.method);
|
|
374
|
+
const isHttps = (form.url ?? "").toLowerCase().startsWith("https://");
|
|
375
|
+
const importReplacing = importData ? importData.configs.filter((c) => importSelected.has(c.name) && configs.some((existing) => existing.name === c.name)).length : 0;
|
|
376
|
+
return /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.Dialog, { open: true, maxWidth: false, sx: { "& .MuiDialog-paper": { width: "1000px", height: "700px" } } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.DialogTitle, null, "HTTP Pull-Push Provider \u2014 Connections"), /* @__PURE__ */ import_react3.default.createElement(import_material3.DialogContent, { sx: { pt: "16px !important", display: "flex", gap: 2, overflow: "hidden" } }, loading ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { sx: { display: "flex", justifyContent: "center", width: "100%", mt: 4 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.CircularProgress, null)) : /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { sx: { width: 260, flexShrink: 0, display: "flex", flexDirection: "column", gap: 1 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary", fontWeight: "bold" }, "Connections"), /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { sx: { flex: 1, border: 1, borderColor: "divider", borderRadius: 1, overflowY: "auto" } }, configs.length === 0 ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.disabled", sx: { p: 1, display: "block" } }, "No connections yet.") : configs.map((config) => /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { key: config.name, sx: {
|
|
377
|
+
display: "flex",
|
|
378
|
+
alignItems: "center",
|
|
379
|
+
px: 1,
|
|
380
|
+
py: 0.5,
|
|
381
|
+
borderBottom: 1,
|
|
382
|
+
borderColor: "divider",
|
|
383
|
+
borderLeft: editingName === config.name ? 3 : 0,
|
|
384
|
+
borderLeftColor: "primary.main",
|
|
385
|
+
bgcolor: editingName === config.name ? "action.selected" : "transparent",
|
|
386
|
+
opacity: config.enabled ? 1 : 0.55
|
|
387
|
+
} }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { sx: { flex: 1, minWidth: 0, overflow: "hidden", cursor: "pointer" }, onClick: () => startEdit(config) }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "body2", fontWeight: "bold", noWrap: true }, config.name), /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary", noWrap: true, display: "block" }, config.url || "\u2014")), /* @__PURE__ */ import_react3.default.createElement(
|
|
388
|
+
import_material3.Chip,
|
|
389
|
+
{
|
|
390
|
+
size: "small",
|
|
391
|
+
label: config.enabled ? "on" : "off",
|
|
392
|
+
color: config.enabled ? "success" : "default",
|
|
393
|
+
sx: { mr: 0.5 }
|
|
394
|
+
}
|
|
395
|
+
), /* @__PURE__ */ import_react3.default.createElement(import_material3.Tooltip, { title: "Delete" }, /* @__PURE__ */ import_react3.default.createElement("span", null, /* @__PURE__ */ import_react3.default.createElement(
|
|
396
|
+
import_material3.IconButton,
|
|
397
|
+
{
|
|
398
|
+
size: "small",
|
|
399
|
+
color: "error",
|
|
400
|
+
"aria-label": `Delete ${config.name}`,
|
|
401
|
+
disabled: deletingName === config.name || saving,
|
|
402
|
+
onClick: () => removeConnection(config.name)
|
|
403
|
+
},
|
|
404
|
+
deletingName === config.name ? /* @__PURE__ */ import_react3.default.createElement(import_material3.CircularProgress, { size: 12 }) : /* @__PURE__ */ import_react3.default.createElement(import_icons_material3.Delete, { sx: { fontSize: 14 } })
|
|
405
|
+
)))))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Stack, { direction: "row", spacing: 0.5 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Button, { size: "small", startIcon: /* @__PURE__ */ import_react3.default.createElement(import_icons_material3.Add, null), onClick: startNew, sx: { flex: 1 } }, "New"), /* @__PURE__ */ import_react3.default.createElement(import_material3.Button, { size: "small", startIcon: /* @__PURE__ */ import_react3.default.createElement(import_icons_material3.ContentCopy, null), disabled: !showForm, onClick: startClone, sx: { flex: 1 } }, "Clone"))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Divider, { orientation: "vertical", flexItem: true }), /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { sx: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 1.5, overflow: "hidden" } }, !showForm ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { sx: { m: "auto", color: "text.disabled" } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "body2" }, "Select a connection to edit or click New.")) : /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary", fontWeight: "bold" }, editingName ? `Editing: ${editingName}` : "New connection"), /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { sx: { flex: 1, overflowY: "auto", pr: 1, pt: 1 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Stack, { spacing: 2 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Stack, { direction: "row", spacing: 2, alignItems: "center" }, /* @__PURE__ */ import_react3.default.createElement(
|
|
406
|
+
import_material3.TextField,
|
|
407
|
+
{
|
|
408
|
+
size: "small",
|
|
409
|
+
label: "Connection name",
|
|
410
|
+
value: form.name,
|
|
411
|
+
sx: { width: 220 },
|
|
412
|
+
onChange: (e) => patch({ name: e.target.value })
|
|
413
|
+
}
|
|
414
|
+
), /* @__PURE__ */ import_react3.default.createElement(import_material3.FormControlLabel, { label: "Enabled", control: /* @__PURE__ */ import_react3.default.createElement(import_material3.Switch, { checked: form.enabled, onChange: (_e, checked) => patch({ enabled: checked }) }) })), !form.enabled && /* @__PURE__ */ import_react3.default.createElement(import_material3.Alert, { severity: "info" }, "This connection is stored but not operative: it is not polled and delivers nothing."), /* @__PURE__ */ import_react3.default.createElement(import_material3.Stack, { direction: "row", spacing: 2 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.FormControl, { size: "small", sx: { width: 120 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.InputLabel, null, "Method"), /* @__PURE__ */ import_react3.default.createElement(
|
|
415
|
+
import_material3.Select,
|
|
416
|
+
{
|
|
417
|
+
label: "Method",
|
|
418
|
+
value: form.method,
|
|
419
|
+
onChange: (e) => patch({ method: e.target.value })
|
|
420
|
+
},
|
|
421
|
+
Object.values(EHttpMethod).map((m) => /* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { key: m, value: m }, m))
|
|
422
|
+
)), /* @__PURE__ */ import_react3.default.createElement(
|
|
423
|
+
import_material3.TextField,
|
|
424
|
+
{
|
|
425
|
+
size: "small",
|
|
426
|
+
label: "URL",
|
|
427
|
+
value: form.url,
|
|
428
|
+
sx: { flex: 1 },
|
|
429
|
+
placeholder: "https://api.example.com/quotes",
|
|
430
|
+
onChange: (e) => patch({ url: e.target.value })
|
|
431
|
+
}
|
|
432
|
+
)), /* @__PURE__ */ import_react3.default.createElement(import_material3.Stack, { direction: "row", spacing: 2 }, /* @__PURE__ */ import_react3.default.createElement(
|
|
433
|
+
import_material3.TextField,
|
|
434
|
+
{
|
|
435
|
+
size: "small",
|
|
436
|
+
label: "Interval (s)",
|
|
437
|
+
type: "number",
|
|
438
|
+
sx: { width: 130 },
|
|
439
|
+
value: form.intervalSeconds,
|
|
440
|
+
onChange: (e) => patch({ intervalSeconds: parseInt(e.target.value, 10) || 0 }),
|
|
441
|
+
slotProps: { htmlInput: { min: 1 } }
|
|
442
|
+
}
|
|
443
|
+
), /* @__PURE__ */ import_react3.default.createElement(
|
|
444
|
+
import_material3.TextField,
|
|
445
|
+
{
|
|
446
|
+
size: "small",
|
|
447
|
+
label: "Timeout (ms)",
|
|
448
|
+
type: "number",
|
|
449
|
+
sx: { width: 140 },
|
|
450
|
+
value: form.timeoutMs,
|
|
451
|
+
onChange: (e) => patch({ timeoutMs: parseInt(e.target.value, 10) || 0 }),
|
|
452
|
+
slotProps: { htmlInput: { min: 1 } }
|
|
453
|
+
}
|
|
454
|
+
), /* @__PURE__ */ import_react3.default.createElement(
|
|
455
|
+
import_material3.TextField,
|
|
456
|
+
{
|
|
457
|
+
size: "small",
|
|
458
|
+
label: "Retries",
|
|
459
|
+
type: "number",
|
|
460
|
+
sx: { width: 110 },
|
|
461
|
+
value: form.retries,
|
|
462
|
+
onChange: (e) => patch({ retries: parseInt(e.target.value, 10) || 0 }),
|
|
463
|
+
slotProps: { htmlInput: { min: 0 } }
|
|
464
|
+
}
|
|
465
|
+
)), /* @__PURE__ */ import_react3.default.createElement(import_material3.Stack, { direction: "row", spacing: 2, alignItems: "center", flexWrap: "wrap", useFlexGap: true }, /* @__PURE__ */ import_react3.default.createElement(import_material3.FormControl, { size: "small", sx: { width: 160 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.InputLabel, null, "Response"), /* @__PURE__ */ import_react3.default.createElement(
|
|
466
|
+
import_material3.Select,
|
|
467
|
+
{
|
|
468
|
+
label: "Response",
|
|
469
|
+
value: form.responseType,
|
|
470
|
+
onChange: (e) => patch({ responseType: e.target.value })
|
|
471
|
+
},
|
|
472
|
+
/* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "json" /* JSON */ }, "JSON (parsed)"),
|
|
473
|
+
/* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "text" /* TEXT */ }, "Text (raw)")
|
|
474
|
+
)), /* @__PURE__ */ import_react3.default.createElement(import_material3.FormControl, { size: "small", sx: { width: 200 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.InputLabel, null, "Emit"), /* @__PURE__ */ import_react3.default.createElement(
|
|
475
|
+
import_material3.Select,
|
|
476
|
+
{
|
|
477
|
+
label: "Emit",
|
|
478
|
+
value: form.emitMode,
|
|
479
|
+
onChange: (e) => patch({ emitMode: e.target.value })
|
|
480
|
+
},
|
|
481
|
+
/* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "always" /* ALWAYS */ }, "Always"),
|
|
482
|
+
/* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "onChange" /* ON_CHANGE */ }, "Only when it changes")
|
|
483
|
+
)), /* @__PURE__ */ import_react3.default.createElement(import_material3.FormControlLabel, { label: "Accept self-signed certificates", control: /* @__PURE__ */ import_react3.default.createElement(
|
|
484
|
+
import_material3.Switch,
|
|
485
|
+
{
|
|
486
|
+
checked: form.allowInsecureTls,
|
|
487
|
+
disabled: !isHttps,
|
|
488
|
+
onChange: (_e, checked) => patch({ allowInsecureTls: checked })
|
|
489
|
+
}
|
|
490
|
+
) })), /* @__PURE__ */ import_react3.default.createElement(import_material3.Divider, null), /* @__PURE__ */ import_react3.default.createElement(import_material3.Stack, { direction: "row", spacing: 2, alignItems: "center", flexWrap: "wrap", useFlexGap: true }, /* @__PURE__ */ import_react3.default.createElement(import_material3.FormControl, { size: "small", sx: { width: 160 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.InputLabel, null, "Auth"), /* @__PURE__ */ import_react3.default.createElement(
|
|
491
|
+
import_material3.Select,
|
|
492
|
+
{
|
|
493
|
+
label: "Auth",
|
|
494
|
+
value: form.auth.type,
|
|
495
|
+
onChange: (e) => patchAuth({ type: e.target.value })
|
|
496
|
+
},
|
|
497
|
+
/* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "none" /* NONE */ }, "None"),
|
|
498
|
+
/* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "basic" /* BASIC */ }, "Basic"),
|
|
499
|
+
/* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "bearer" /* BEARER */ }, "Bearer token"),
|
|
500
|
+
/* @__PURE__ */ import_react3.default.createElement(import_material3.MenuItem, { value: "header" /* HEADER */ }, "Custom header")
|
|
501
|
+
)), form.auth.type === "basic" /* BASIC */ && /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, /* @__PURE__ */ import_react3.default.createElement(
|
|
502
|
+
import_material3.TextField,
|
|
503
|
+
{
|
|
504
|
+
size: "small",
|
|
505
|
+
label: "Username",
|
|
506
|
+
value: form.auth.username ?? "",
|
|
507
|
+
sx: { width: 200 },
|
|
508
|
+
onChange: (e) => patchAuth({ username: e.target.value })
|
|
509
|
+
}
|
|
510
|
+
), /* @__PURE__ */ import_react3.default.createElement(
|
|
511
|
+
SecretField_default,
|
|
512
|
+
{
|
|
513
|
+
label: "Password",
|
|
514
|
+
value: form.auth.password ?? "",
|
|
515
|
+
onChange: (v) => patchAuth({ password: v })
|
|
516
|
+
}
|
|
517
|
+
)), form.auth.type === "bearer" /* BEARER */ && /* @__PURE__ */ import_react3.default.createElement(
|
|
518
|
+
SecretField_default,
|
|
519
|
+
{
|
|
520
|
+
label: "Token",
|
|
521
|
+
value: form.auth.token ?? "",
|
|
522
|
+
width: 360,
|
|
523
|
+
onChange: (v) => patchAuth({ token: v })
|
|
524
|
+
}
|
|
525
|
+
), form.auth.type === "header" /* HEADER */ && /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, /* @__PURE__ */ import_react3.default.createElement(
|
|
526
|
+
import_material3.TextField,
|
|
527
|
+
{
|
|
528
|
+
size: "small",
|
|
529
|
+
label: "Header name",
|
|
530
|
+
value: form.auth.headerName ?? "",
|
|
531
|
+
sx: { width: 200 },
|
|
532
|
+
onChange: (e) => patchAuth({ headerName: e.target.value })
|
|
533
|
+
}
|
|
534
|
+
), /* @__PURE__ */ import_react3.default.createElement(
|
|
535
|
+
SecretField_default,
|
|
536
|
+
{
|
|
537
|
+
label: "Header value",
|
|
538
|
+
value: form.auth.headerValue ?? "",
|
|
539
|
+
onChange: (v) => patchAuth({ headerValue: v })
|
|
540
|
+
}
|
|
541
|
+
))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Divider, null), /* @__PURE__ */ import_react3.default.createElement(HeaderEditor_default, { headers: form.headers, onChange: (h) => patch({ headers: h }) }), showBody && /* @__PURE__ */ import_react3.default.createElement(
|
|
542
|
+
import_material3.TextField,
|
|
543
|
+
{
|
|
544
|
+
size: "small",
|
|
545
|
+
label: "Body",
|
|
546
|
+
multiline: true,
|
|
547
|
+
minRows: 3,
|
|
548
|
+
value: form.body ?? "",
|
|
549
|
+
onChange: (e) => patch({ body: e.target.value })
|
|
550
|
+
}
|
|
551
|
+
))), errors.length > 0 && /* @__PURE__ */ import_react3.default.createElement(import_material3.Alert, { severity: "warning", sx: { py: 0 } }, errors.map((e, i) => /* @__PURE__ */ import_react3.default.createElement("div", { key: i }, e))), error && /* @__PURE__ */ import_react3.default.createElement(import_material3.Alert, { severity: "error", sx: { py: 0 } }, error), testResult && /* @__PURE__ */ import_react3.default.createElement(import_material3.Alert, { severity: testResult.ok ? testResult.status && testResult.status < 400 ? "success" : "warning" : "error", sx: { py: 0 } }, testResult.ok ? /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "body2" }, "HTTP ", testResult.status, " \xB7 ", testResult.durationMs, " ms \xB7 ", testResult.bytes, " bytes", form.responseType === "json" /* JSON */ && testResult.jsonParsed === false && " \xB7 body is not valid JSON"), testResult.preview && /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { component: "pre", sx: { m: 0, mt: 0.5, maxHeight: 120, overflow: "auto", fontSize: 11, whiteSpace: "pre-wrap", wordBreak: "break-all" } }, testResult.preview)) : /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "body2" }, testResult.error, " (", testResult.durationMs, " ms)")), /* @__PURE__ */ import_react3.default.createElement(import_material3.Stack, { direction: "row", justifyContent: "flex-end", spacing: 1 }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Tooltip, { title: "Run the request now, from the Kwirth backend \u2014 the same network, certificates and identity the real polling uses" }, /* @__PURE__ */ import_react3.default.createElement("span", null, /* @__PURE__ */ import_react3.default.createElement(import_material3.Button, { size: "small", disabled: testing || saving, onClick: testConnection }, testing ? /* @__PURE__ */ import_react3.default.createElement(import_material3.CircularProgress, { size: 14 }) : "Test"))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Button, { size: "small", variant: "contained", disabled: saving || testing, onClick: submitForm }, saving ? /* @__PURE__ */ import_react3.default.createElement(import_material3.CircularProgress, { size: 14 }) : editingName ? "Update" : "Add"), /* @__PURE__ */ import_react3.default.createElement(import_material3.Button, { size: "small", disabled: saving, onClick: () => {
|
|
552
|
+
setShowForm(false);
|
|
553
|
+
setEditingName(void 0);
|
|
554
|
+
setErrors([]);
|
|
555
|
+
setTestResult(void 0);
|
|
556
|
+
} }, "Cancel")))))), /* @__PURE__ */ import_react3.default.createElement(import_material3.DialogActions, { sx: { justifyContent: "space-between", px: 2 } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Stack, { direction: "row", spacing: 1 }, /* @__PURE__ */ import_react3.default.createElement(
|
|
557
|
+
"input",
|
|
558
|
+
{
|
|
559
|
+
ref: importFileRef,
|
|
560
|
+
type: "file",
|
|
561
|
+
accept: ".json",
|
|
562
|
+
style: { display: "none" },
|
|
563
|
+
onChange: (e) => {
|
|
564
|
+
const f = e.target.files?.[0];
|
|
565
|
+
if (f) openImport(f);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
), /* @__PURE__ */ import_react3.default.createElement(import_material3.Tooltip, { title: "Export connections to JSON" }, /* @__PURE__ */ import_react3.default.createElement("span", null, /* @__PURE__ */ import_react3.default.createElement(
|
|
569
|
+
import_material3.Button,
|
|
570
|
+
{
|
|
571
|
+
size: "small",
|
|
572
|
+
startIcon: /* @__PURE__ */ import_react3.default.createElement(import_icons_material3.FileDownload, null),
|
|
573
|
+
disabled: configs.length === 0,
|
|
574
|
+
onClick: () => {
|
|
575
|
+
setExportSelected(new Set(configs.map((c) => c.name)));
|
|
576
|
+
setExportWithCredentials(false);
|
|
577
|
+
setExportOpen(true);
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
"Export"
|
|
581
|
+
))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Tooltip, { title: "Import connections from JSON" }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Button, { size: "small", startIcon: /* @__PURE__ */ import_react3.default.createElement(import_icons_material3.FileUpload, null), onClick: () => importFileRef.current?.click() }, "Import"))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Button, { onClick: onClose, disabled: saving }, "Close"))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Dialog, { open: exportOpen, maxWidth: false, sx: { "& .MuiDialog-paper": { width: "480px" } } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.DialogTitle, null, "Export connections"), /* @__PURE__ */ import_react3.default.createElement(import_material3.DialogContent, { sx: { pt: "16px !important" } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Stack, { spacing: 1 }, /* @__PURE__ */ import_react3.default.createElement(
|
|
582
|
+
import_material3.FormControlLabel,
|
|
583
|
+
{
|
|
584
|
+
label: "Select all",
|
|
585
|
+
control: /* @__PURE__ */ import_react3.default.createElement(
|
|
586
|
+
import_material3.Checkbox,
|
|
587
|
+
{
|
|
588
|
+
checked: exportSelected.size === configs.length && configs.length > 0,
|
|
589
|
+
indeterminate: exportSelected.size > 0 && exportSelected.size < configs.length,
|
|
590
|
+
onChange: (_e, checked) => setExportSelected(checked ? new Set(configs.map((c) => c.name)) : /* @__PURE__ */ new Set())
|
|
591
|
+
}
|
|
592
|
+
)
|
|
593
|
+
}
|
|
594
|
+
), /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { sx: { maxHeight: 220, overflowY: "auto", border: 1, borderColor: "divider", borderRadius: 1, px: 1 } }, configs.map((config) => /* @__PURE__ */ import_react3.default.createElement(
|
|
595
|
+
import_material3.FormControlLabel,
|
|
596
|
+
{
|
|
597
|
+
key: config.name,
|
|
598
|
+
label: config.name,
|
|
599
|
+
sx: { display: "block" },
|
|
600
|
+
control: /* @__PURE__ */ import_react3.default.createElement(
|
|
601
|
+
import_material3.Checkbox,
|
|
602
|
+
{
|
|
603
|
+
checked: exportSelected.has(config.name),
|
|
604
|
+
onChange: (_e, checked) => setExportSelected((prev) => {
|
|
605
|
+
const next = new Set(prev);
|
|
606
|
+
if (checked) next.add(config.name);
|
|
607
|
+
else next.delete(config.name);
|
|
608
|
+
return next;
|
|
609
|
+
})
|
|
610
|
+
}
|
|
611
|
+
)
|
|
612
|
+
}
|
|
613
|
+
))), /* @__PURE__ */ import_react3.default.createElement(
|
|
614
|
+
import_material3.FormControlLabel,
|
|
615
|
+
{
|
|
616
|
+
label: "Include credentials",
|
|
617
|
+
control: /* @__PURE__ */ import_react3.default.createElement(import_material3.Checkbox, { checked: exportWithCredentials, onChange: (_e, checked) => setExportWithCredentials(checked) })
|
|
618
|
+
}
|
|
619
|
+
), exportWithCredentials ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Alert, { severity: "warning" }, "Passwords, tokens and header values will be written to the file in clear text. Treat it as a secret.") : /* @__PURE__ */ import_react3.default.createElement(import_material3.Alert, { severity: "info" }, "Credentials are left empty. Whoever imports the file will have to type them again."))), /* @__PURE__ */ import_react3.default.createElement(import_material3.DialogActions, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.Button, { variant: "contained", disabled: exportSelected.size === 0, onClick: doExport }, "Export"), /* @__PURE__ */ import_react3.default.createElement(import_material3.Button, { onClick: () => setExportOpen(false) }, "Cancel"))), /* @__PURE__ */ import_react3.default.createElement(import_material3.Dialog, { open: importData !== void 0, maxWidth: false, sx: { "& .MuiDialog-paper": { width: "480px" } } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.DialogTitle, null, "Import connections"), /* @__PURE__ */ import_react3.default.createElement(import_material3.DialogContent, { sx: { pt: "16px !important" } }, /* @__PURE__ */ import_react3.default.createElement(import_material3.Stack, { spacing: 1 }, importData?.configs.length === 0 ? /* @__PURE__ */ import_react3.default.createElement(import_material3.Typography, { variant: "body2", color: "text.secondary" }, "The file has no connections.") : /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, /* @__PURE__ */ import_react3.default.createElement(
|
|
620
|
+
import_material3.FormControlLabel,
|
|
621
|
+
{
|
|
622
|
+
label: "Select all",
|
|
623
|
+
control: /* @__PURE__ */ import_react3.default.createElement(
|
|
624
|
+
import_material3.Checkbox,
|
|
625
|
+
{
|
|
626
|
+
checked: importSelected.size === (importData?.configs.length ?? 0) && (importData?.configs.length ?? 0) > 0,
|
|
627
|
+
indeterminate: importSelected.size > 0 && importSelected.size < (importData?.configs.length ?? 0),
|
|
628
|
+
onChange: (_e, checked) => setImportSelected(checked ? new Set(importData?.configs.map((c) => c.name) ?? []) : /* @__PURE__ */ new Set())
|
|
629
|
+
}
|
|
630
|
+
)
|
|
631
|
+
}
|
|
632
|
+
), /* @__PURE__ */ import_react3.default.createElement(import_material3.Box, { sx: { maxHeight: 220, overflowY: "auto", border: 1, borderColor: "divider", borderRadius: 1, px: 1 } }, importData?.configs.map((config) => /* @__PURE__ */ import_react3.default.createElement(
|
|
633
|
+
import_material3.FormControlLabel,
|
|
634
|
+
{
|
|
635
|
+
key: config.name,
|
|
636
|
+
sx: { display: "block" },
|
|
637
|
+
label: /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, config.name, configs.some((c) => c.name === config.name) && /* @__PURE__ */ import_react3.default.createElement(import_material3.Chip, { size: "small", label: "replaces", color: "warning", variant: "outlined", sx: { ml: 1 } })),
|
|
638
|
+
control: /* @__PURE__ */ import_react3.default.createElement(
|
|
639
|
+
import_material3.Checkbox,
|
|
640
|
+
{
|
|
641
|
+
checked: importSelected.has(config.name),
|
|
642
|
+
onChange: (_e, checked) => setImportSelected((prev) => {
|
|
643
|
+
const next = new Set(prev);
|
|
644
|
+
if (checked) next.add(config.name);
|
|
645
|
+
else next.delete(config.name);
|
|
646
|
+
return next;
|
|
647
|
+
})
|
|
648
|
+
}
|
|
649
|
+
)
|
|
650
|
+
}
|
|
651
|
+
))), importReplacing > 0 && /* @__PURE__ */ import_react3.default.createElement(import_material3.Alert, { severity: "warning" }, importReplacing, " existing connection(s) will be replaced."), importData?.credentialsIncluded === false && /* @__PURE__ */ import_react3.default.createElement(import_material3.Alert, { severity: "info" }, "The file carries no credentials: you will have to type them after importing.")), errors.length > 0 && /* @__PURE__ */ import_react3.default.createElement(import_material3.Alert, { severity: "warning" }, errors.map((e, i) => /* @__PURE__ */ import_react3.default.createElement("div", { key: i }, e))))), /* @__PURE__ */ import_react3.default.createElement(import_material3.DialogActions, null, /* @__PURE__ */ import_react3.default.createElement(import_material3.Button, { variant: "contained", disabled: importSelected.size === 0 || saving, onClick: doImport }, "Import"), /* @__PURE__ */ import_react3.default.createElement(import_material3.Button, { onClick: () => {
|
|
652
|
+
setImportData(void 0);
|
|
653
|
+
setErrors([]);
|
|
654
|
+
} }, "Cancel"))));
|
|
655
|
+
};
|
|
656
|
+
var HttpPullPushConfigDialog_default = HttpPullPushConfigDialog;
|
|
657
|
+
|
|
658
|
+
// src/front/index.tsx
|
|
659
|
+
window.__kwirth_providers__ = window.__kwirth_providers__ ?? {};
|
|
660
|
+
window.__kwirth_providers__["http-pull-push"] = { ConfigDialog: HttpPullPushConfigDialog_default };
|
|
661
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "commonjs",
|
|
3
|
+
"extensionType": "provider",
|
|
4
|
+
"id": "http-pull-push",
|
|
5
|
+
"name": "@kwirthmagnify/kwirth-provider-http-pull-push",
|
|
6
|
+
"displayName": "HTTP Pull-Push Provider",
|
|
7
|
+
"version": "0.1.0",
|
|
8
|
+
"description": "HTTP polling provider for Kwirth — pulls remote HTTP endpoints on a schedule and pushes each result to the subscribed channels",
|
|
9
|
+
"website": "https://kwirthmagnify.dev",
|
|
10
|
+
"requiresRestart": false,
|
|
11
|
+
"requiresExtension": []
|
|
12
|
+
}
|