@bpmnkit/operate 0.0.7 → 0.0.9

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.
@@ -0,0 +1,1007 @@
1
+ import { badge } from "../components/badge.js";
2
+ import { createFilterTable } from "../components/filter-table.js";
3
+ import { MOCK_VARIABLES } from "../mock-data.js";
4
+ import { InstancesStore } from "../stores/instances.js";
5
+ // ── Shared template helpers ──────────────────────────────────────────────────
6
+ const INST_TEMPLATE_KEY = "bpmnkit-operate:search-templates";
7
+ const VAR_TEMPLATE_KEY = "bpmnkit-operate:var-search-templates";
8
+ let condIdCounter = 0;
9
+ function newCondId() {
10
+ return `c${++condIdCounter}`;
11
+ }
12
+ function loadTemplates(key) {
13
+ try {
14
+ const raw = localStorage.getItem(key);
15
+ if (!raw)
16
+ return [];
17
+ return JSON.parse(raw);
18
+ }
19
+ catch {
20
+ return [];
21
+ }
22
+ }
23
+ function saveTemplates(key, templates) {
24
+ localStorage.setItem(key, JSON.stringify(templates));
25
+ }
26
+ function relTime(iso) {
27
+ if (!iso)
28
+ return "—";
29
+ const diff = Date.now() - new Date(iso).getTime();
30
+ const m = Math.floor(diff / 60_000);
31
+ if (m < 1)
32
+ return "just now";
33
+ if (m < 60)
34
+ return `${m}m ago`;
35
+ const h = Math.floor(m / 60);
36
+ if (h < 24)
37
+ return `${h}h ago`;
38
+ return `${Math.floor(h / 24)}d ago`;
39
+ }
40
+ const INST_FIELDS = [
41
+ { id: "state", label: "State", type: "state-select", serverSide: true },
42
+ { id: "processDefinitionKey", label: "Process Definition Key", type: "text", serverSide: true },
43
+ { id: "processDefinitionId", label: "Process Definition ID", type: "text" },
44
+ { id: "processDefinitionName", label: "Process Definition Name", type: "text" },
45
+ { id: "processInstanceKey", label: "Instance Key", type: "text" },
46
+ { id: "businessId", label: "Business ID", type: "text" },
47
+ { id: "hasIncident", label: "Has Incident", type: "bool-select" },
48
+ { id: "startDateFrom", label: "Started After", type: "date" },
49
+ { id: "startDateTo", label: "Started Before", type: "date" },
50
+ { id: "endDateFrom", label: "Ended After", type: "date" },
51
+ { id: "endDateTo", label: "Ended Before", type: "date" },
52
+ { id: "parentProcessInstanceKey", label: "Parent Instance Key", type: "text" },
53
+ ];
54
+ const VAR_FIELDS = [
55
+ { id: "name", label: "Name", type: "text" },
56
+ {
57
+ id: "value",
58
+ label: "Value",
59
+ type: "text",
60
+ hint: 'JSON format: "hello", 42, true',
61
+ },
62
+ { id: "processInstanceKey", label: "Process Instance Key", type: "text" },
63
+ { id: "scopeKey", label: "Scope Key", type: "text" },
64
+ { id: "variableKey", label: "Variable Key", type: "text" },
65
+ { id: "tenantId", label: "Tenant ID", type: "text" },
66
+ { id: "isTruncated", label: "Is Truncated", type: "bool-select" },
67
+ ];
68
+ function createValueInput(fieldDef, value) {
69
+ if (fieldDef.type === "state-select") {
70
+ const sel = document.createElement("select");
71
+ sel.className = "op-search-value-select";
72
+ for (const opt of ["ACTIVE", "COMPLETED", "TERMINATED"]) {
73
+ const o = document.createElement("option");
74
+ o.value = opt;
75
+ o.textContent = opt;
76
+ if (value === opt)
77
+ o.selected = true;
78
+ sel.appendChild(o);
79
+ }
80
+ if (!value)
81
+ sel.value = "ACTIVE";
82
+ return sel;
83
+ }
84
+ if (fieldDef.type === "bool-select") {
85
+ const sel = document.createElement("select");
86
+ sel.className = "op-search-value-select";
87
+ for (const [v, l] of [
88
+ ["true", "Yes"],
89
+ ["false", "No"],
90
+ ]) {
91
+ const o = document.createElement("option");
92
+ o.value = v;
93
+ o.textContent = l;
94
+ if (value === v)
95
+ o.selected = true;
96
+ sel.appendChild(o);
97
+ }
98
+ if (!value)
99
+ sel.value = "true";
100
+ return sel;
101
+ }
102
+ const inp = document.createElement("input");
103
+ inp.type = fieldDef.type === "date" ? "date" : "text";
104
+ inp.className = "op-search-value-input";
105
+ if (fieldDef.hint)
106
+ inp.placeholder = fieldDef.hint;
107
+ else if (fieldDef.type !== "date")
108
+ inp.placeholder = "value…";
109
+ inp.value = value;
110
+ return inp;
111
+ }
112
+ function attachValueListener(input, cond) {
113
+ const update = () => {
114
+ cond.value = input.value;
115
+ };
116
+ input.addEventListener("input", update);
117
+ input.addEventListener("change", update);
118
+ }
119
+ function buildConditionsEl(conditionsEl, conditions, fields, onChange) {
120
+ conditionsEl.innerHTML = "";
121
+ if (conditions.length === 0) {
122
+ const hint = document.createElement("div");
123
+ hint.className = "op-search-empty-hint";
124
+ hint.textContent = "No conditions — will return all results (up to limit).";
125
+ conditionsEl.appendChild(hint);
126
+ return;
127
+ }
128
+ for (const cond of conditions) {
129
+ const row = document.createElement("div");
130
+ row.className = "op-search-cond-row";
131
+ const fieldSel = document.createElement("select");
132
+ fieldSel.className = "op-search-field-select";
133
+ for (const f of fields) {
134
+ const o = document.createElement("option");
135
+ o.value = f.id;
136
+ o.textContent = f.label;
137
+ if (cond.field === f.id)
138
+ o.selected = true;
139
+ fieldSel.appendChild(o);
140
+ }
141
+ const fieldDef = fields.find((f) => f.id === cond.field);
142
+ if (!fieldDef)
143
+ continue;
144
+ let valueEl = createValueInput(fieldDef, cond.value);
145
+ attachValueListener(valueEl, cond);
146
+ fieldSel.addEventListener("change", () => {
147
+ const newDef = fields.find((f) => f.id === fieldSel.value);
148
+ if (!newDef)
149
+ return;
150
+ cond.field = newDef.id;
151
+ cond.value = "";
152
+ const newInput = createValueInput(newDef, "");
153
+ attachValueListener(newInput, cond);
154
+ row.replaceChild(newInput, valueEl);
155
+ valueEl = newInput;
156
+ });
157
+ const removeBtn = document.createElement("button");
158
+ removeBtn.className = "op-search-cond-remove";
159
+ removeBtn.textContent = "✕";
160
+ removeBtn.title = "Remove condition";
161
+ removeBtn.addEventListener("click", () => {
162
+ const idx = conditions.indexOf(cond);
163
+ if (idx !== -1)
164
+ conditions.splice(idx, 1);
165
+ onChange();
166
+ });
167
+ row.appendChild(fieldSel);
168
+ row.appendChild(valueEl);
169
+ row.appendChild(removeBtn);
170
+ conditionsEl.appendChild(row);
171
+ }
172
+ }
173
+ // ── Template section builder ─────────────────────────────────────────────────
174
+ function buildTemplateRow(storageKey, templates, onLoad, onUpdate, getConditions, mountEl) {
175
+ const row = document.createElement("div");
176
+ row.className = "op-search-template-row";
177
+ const label = document.createElement("span");
178
+ label.className = "op-search-template-label";
179
+ label.textContent = "Template:";
180
+ row.appendChild(label);
181
+ const select = document.createElement("select");
182
+ select.className = "op-search-template-select";
183
+ row.appendChild(select);
184
+ const deleteBtn = document.createElement("button");
185
+ deleteBtn.className = "op-action-btn op-action-btn--danger";
186
+ deleteBtn.textContent = "Delete";
187
+ row.appendChild(deleteBtn);
188
+ const saveBtn = document.createElement("button");
189
+ saveBtn.className = "op-action-btn";
190
+ saveBtn.textContent = "Save as Template…";
191
+ row.appendChild(saveBtn);
192
+ function refresh() {
193
+ select.innerHTML = "";
194
+ const none = document.createElement("option");
195
+ none.value = "";
196
+ none.textContent = templates.length === 0 ? "No saved templates" : "Select template…";
197
+ select.appendChild(none);
198
+ for (const t of templates) {
199
+ const o = document.createElement("option");
200
+ o.value = t.id;
201
+ o.textContent = t.name;
202
+ select.appendChild(o);
203
+ }
204
+ deleteBtn.disabled = templates.length === 0;
205
+ }
206
+ select.addEventListener("change", () => {
207
+ const tmpl = templates.find((t) => t.id === select.value);
208
+ if (!tmpl)
209
+ return;
210
+ onLoad(tmpl.conditions.map((c) => ({ ...c, id: newCondId() })));
211
+ });
212
+ deleteBtn.addEventListener("click", () => {
213
+ const id = select.value;
214
+ if (!id)
215
+ return;
216
+ const updated = templates.filter((t) => t.id !== id);
217
+ saveTemplates(storageKey, updated);
218
+ onUpdate(updated);
219
+ refresh();
220
+ });
221
+ saveBtn.addEventListener("click", () => {
222
+ showSaveDialog(storageKey, templates, getConditions, mountEl, (updated, newId) => {
223
+ onUpdate(updated);
224
+ refresh();
225
+ select.value = newId;
226
+ });
227
+ });
228
+ refresh();
229
+ return { el: row, refresh };
230
+ }
231
+ function showSaveDialog(storageKey, templates, getConditions, mountEl, onSaved) {
232
+ const overlay = document.createElement("div");
233
+ overlay.className = "op-modal-overlay";
234
+ const dialog = document.createElement("div");
235
+ dialog.className = "op-modal op-modal--form";
236
+ const header = document.createElement("div");
237
+ header.className = "op-modal-header";
238
+ const titleSpan = document.createElement("span");
239
+ titleSpan.className = "op-modal-title";
240
+ titleSpan.textContent = "Save Search Template";
241
+ header.appendChild(titleSpan);
242
+ const closeBtn = document.createElement("button");
243
+ closeBtn.className = "op-modal-close";
244
+ closeBtn.textContent = "✕";
245
+ closeBtn.addEventListener("click", () => overlay.remove());
246
+ header.appendChild(closeBtn);
247
+ const body = document.createElement("div");
248
+ body.className = "op-modal-form-body";
249
+ const group = document.createElement("div");
250
+ group.className = "op-form-group";
251
+ const lbl = document.createElement("label");
252
+ lbl.className = "op-form-label";
253
+ lbl.textContent = "Template Name";
254
+ const nameInput = document.createElement("input");
255
+ nameInput.type = "text";
256
+ nameInput.className = "op-form-input";
257
+ nameInput.placeholder = "e.g. Active instances with incidents";
258
+ group.appendChild(lbl);
259
+ group.appendChild(nameInput);
260
+ body.appendChild(group);
261
+ const footer = document.createElement("div");
262
+ footer.className = "op-modal-form-footer";
263
+ const errorEl = document.createElement("div");
264
+ errorEl.className = "op-form-error";
265
+ errorEl.style.display = "none";
266
+ footer.appendChild(errorEl);
267
+ const cancelBtn = document.createElement("button");
268
+ cancelBtn.className = "op-action-btn";
269
+ cancelBtn.textContent = "Cancel";
270
+ cancelBtn.style.marginLeft = "auto";
271
+ cancelBtn.addEventListener("click", () => overlay.remove());
272
+ footer.appendChild(cancelBtn);
273
+ const saveBtn = document.createElement("button");
274
+ saveBtn.className = "op-action-btn op-action-btn--primary";
275
+ saveBtn.textContent = "Save";
276
+ footer.appendChild(saveBtn);
277
+ saveBtn.addEventListener("click", () => {
278
+ const name = nameInput.value.trim();
279
+ if (!name) {
280
+ errorEl.textContent = "Please enter a template name.";
281
+ errorEl.style.display = "";
282
+ return;
283
+ }
284
+ const newTmpl = {
285
+ id: `tmpl-${Date.now()}`,
286
+ name,
287
+ conditions: getConditions().map((c) => ({ ...c })),
288
+ };
289
+ const updated = [...templates, newTmpl];
290
+ saveTemplates(storageKey, updated);
291
+ onSaved(updated, newTmpl.id);
292
+ overlay.remove();
293
+ });
294
+ dialog.appendChild(header);
295
+ dialog.appendChild(body);
296
+ dialog.appendChild(footer);
297
+ overlay.appendChild(dialog);
298
+ overlay.addEventListener("click", (e) => {
299
+ if (e.target === overlay)
300
+ overlay.remove();
301
+ });
302
+ mountEl.appendChild(overlay);
303
+ setTimeout(() => nameInput.focus(), 0);
304
+ }
305
+ // ── Main export ──────────────────────────────────────────────────────────────
306
+ export function createSearchView(cfg, onNavigate) {
307
+ const el = document.createElement("div");
308
+ el.className = "op-view op-search-view";
309
+ // ── Tab bar ──────────────────────────────────────────────────────────────
310
+ const tabBar = document.createElement("div");
311
+ tabBar.className = "op-search-tab-bar";
312
+ const instTabBtn = document.createElement("button");
313
+ instTabBtn.className = "op-search-tab op-search-tab--active";
314
+ instTabBtn.textContent = "Instances";
315
+ tabBar.appendChild(instTabBtn);
316
+ const varTabBtn = document.createElement("button");
317
+ varTabBtn.className = "op-search-tab";
318
+ varTabBtn.textContent = "Variables";
319
+ tabBar.appendChild(varTabBtn);
320
+ const aiTabBtn = document.createElement("button");
321
+ aiTabBtn.className = "op-search-tab op-search-tab--ai";
322
+ aiTabBtn.textContent = "AI Search";
323
+ aiTabBtn.style.display = "none"; // revealed after proxy status check
324
+ tabBar.appendChild(aiTabBtn);
325
+ el.appendChild(tabBar);
326
+ // ── Panes ─────────────────────────────────────────────────────────────────
327
+ const instPane = document.createElement("div");
328
+ instPane.className = "op-search-pane";
329
+ el.appendChild(instPane);
330
+ const varPane = document.createElement("div");
331
+ varPane.className = "op-search-pane";
332
+ varPane.style.display = "none";
333
+ el.appendChild(varPane);
334
+ const aiPane = document.createElement("div");
335
+ aiPane.className = "op-search-pane";
336
+ aiPane.style.display = "none";
337
+ el.appendChild(aiPane);
338
+ // ── Tab switching ─────────────────────────────────────────────────────────
339
+ const allTabBtns = [instTabBtn, varTabBtn, aiTabBtn];
340
+ const allPanes = [instPane, varPane, aiPane];
341
+ function switchToTab(activeBtn) {
342
+ allTabBtns.forEach((btn, i) => {
343
+ btn.classList.toggle("op-search-tab--active", btn === activeBtn);
344
+ const pane = allPanes[i];
345
+ if (pane)
346
+ pane.style.display = btn === activeBtn ? "" : "none";
347
+ });
348
+ }
349
+ instTabBtn.addEventListener("click", () => switchToTab(instTabBtn));
350
+ varTabBtn.addEventListener("click", () => switchToTab(varTabBtn));
351
+ aiTabBtn.addEventListener("click", () => switchToTab(aiTabBtn));
352
+ // ════════════════════════════════════════════════════════════════════════
353
+ // INSTANCE SEARCH
354
+ // ════════════════════════════════════════════════════════════════════════
355
+ let instConditions = [];
356
+ let instTemplates = loadTemplates(INST_TEMPLATE_KEY);
357
+ let instStore = null;
358
+ const instTemplateHeader = document.createElement("div");
359
+ instTemplateHeader.className = "op-search-header";
360
+ instPane.appendChild(instTemplateHeader);
361
+ const instBuilderSection = document.createElement("div");
362
+ instBuilderSection.className = "op-search-builder";
363
+ instPane.appendChild(instBuilderSection);
364
+ const instConditionsEl = document.createElement("div");
365
+ instConditionsEl.className = "op-search-conditions";
366
+ instBuilderSection.appendChild(instConditionsEl);
367
+ const instAddBtn = document.createElement("button");
368
+ instAddBtn.className = "op-search-add-btn";
369
+ instAddBtn.textContent = "+ Add Condition";
370
+ instBuilderSection.appendChild(instAddBtn);
371
+ const instActionRow = document.createElement("div");
372
+ instActionRow.className = "op-search-actions";
373
+ const instRunBtn = document.createElement("button");
374
+ instRunBtn.className = "op-action-btn op-action-btn--primary";
375
+ instRunBtn.textContent = "▶ Run Search";
376
+ instActionRow.appendChild(instRunBtn);
377
+ const instClearBtn = document.createElement("button");
378
+ instClearBtn.className = "op-action-btn";
379
+ instClearBtn.textContent = "Clear";
380
+ instActionRow.appendChild(instClearBtn);
381
+ const instStatusEl = document.createElement("span");
382
+ instStatusEl.className = "op-search-status";
383
+ instActionRow.appendChild(instStatusEl);
384
+ instBuilderSection.appendChild(instActionRow);
385
+ const instResultsSection = document.createElement("div");
386
+ instResultsSection.className = "op-search-results";
387
+ instResultsSection.style.display = "none";
388
+ instPane.appendChild(instResultsSection);
389
+ const instResultsHeading = document.createElement("div");
390
+ instResultsHeading.className = "op-search-results-heading";
391
+ instResultsSection.appendChild(instResultsHeading);
392
+ const { el: instTableEl, setRows: setInstRows } = createFilterTable({
393
+ columns: [
394
+ {
395
+ label: "Key",
396
+ width: "140px",
397
+ render: (row) => row.processInstanceKey,
398
+ sortValue: (row) => row.processInstanceKey,
399
+ },
400
+ {
401
+ label: "Process",
402
+ render: (row) => row.processDefinitionName ?? row.processDefinitionId ?? "—",
403
+ sortValue: (row) => row.processDefinitionName ?? row.processDefinitionId ?? "",
404
+ },
405
+ {
406
+ label: "Business ID",
407
+ width: "130px",
408
+ render: (row) => row.businessId || "—",
409
+ sortValue: (row) => row.businessId ?? "",
410
+ },
411
+ {
412
+ label: "State",
413
+ width: "110px",
414
+ render: (row) => {
415
+ const wrap = document.createElement("div");
416
+ wrap.className = "bpmnkit-badge-wrap";
417
+ wrap.appendChild(badge(row.state));
418
+ if (row.hasIncident) {
419
+ const inc = document.createElement("span");
420
+ inc.className = "bpmnkit-badge bpmnkit-badge--incident-dot";
421
+ inc.title = "Has incident";
422
+ inc.textContent = "⚠";
423
+ wrap.appendChild(inc);
424
+ }
425
+ return wrap;
426
+ },
427
+ sortValue: (row) => row.state,
428
+ },
429
+ {
430
+ label: "Started",
431
+ width: "90px",
432
+ render: (row) => relTime(row.startDate),
433
+ sortValue: (row) => row.startDate ?? "",
434
+ },
435
+ {
436
+ label: "Ended",
437
+ width: "90px",
438
+ render: (row) => relTime(row.endDate),
439
+ sortValue: (row) => row.endDate ?? "",
440
+ },
441
+ ],
442
+ searchFn: (row) => [
443
+ row.processInstanceKey,
444
+ row.processDefinitionId,
445
+ row.processDefinitionName,
446
+ row.businessId,
447
+ row.state,
448
+ ]
449
+ .filter(Boolean)
450
+ .join(" "),
451
+ onRowClick: (row) => onNavigate(`/instances/${row.processInstanceKey}`),
452
+ emptyText: "No instances found matching your query",
453
+ });
454
+ instResultsSection.appendChild(instTableEl);
455
+ function renderInstConditions() {
456
+ buildConditionsEl(instConditionsEl, instConditions, INST_FIELDS, renderInstConditions);
457
+ }
458
+ const { el: instTmplRowEl } = buildTemplateRow(INST_TEMPLATE_KEY, instTemplates, (conds) => {
459
+ instConditions = conds;
460
+ renderInstConditions();
461
+ }, (updated) => {
462
+ instTemplates = updated;
463
+ }, () => instConditions, el);
464
+ instTemplateHeader.appendChild(instTmplRowEl);
465
+ instAddBtn.addEventListener("click", () => {
466
+ instConditions.push({ id: newCondId(), field: "state", value: "ACTIVE" });
467
+ renderInstConditions();
468
+ });
469
+ instClearBtn.addEventListener("click", () => {
470
+ instConditions = [];
471
+ renderInstConditions();
472
+ instResultsSection.style.display = "none";
473
+ instStatusEl.textContent = "";
474
+ });
475
+ function applyInstClientFilters(items) {
476
+ let result = items;
477
+ for (const cond of instConditions) {
478
+ const v = cond.value.trim();
479
+ if (!v)
480
+ continue;
481
+ const vl = v.toLowerCase();
482
+ switch (cond.field) {
483
+ case "state":
484
+ result = result.filter((i) => i.state === v.toUpperCase());
485
+ break;
486
+ case "processDefinitionKey":
487
+ result = result.filter((i) => i.processDefinitionKey === v);
488
+ break;
489
+ case "processDefinitionId":
490
+ result = result.filter((i) => i.processDefinitionId?.toLowerCase().includes(vl));
491
+ break;
492
+ case "processDefinitionName":
493
+ result = result.filter((i) => i.processDefinitionName?.toLowerCase().includes(vl));
494
+ break;
495
+ case "processInstanceKey":
496
+ result = result.filter((i) => i.processInstanceKey === v);
497
+ break;
498
+ case "businessId":
499
+ result = result.filter((i) => i.businessId?.toLowerCase().includes(vl));
500
+ break;
501
+ case "hasIncident":
502
+ result = result.filter((i) => String(i.hasIncident) === v);
503
+ break;
504
+ case "startDateFrom":
505
+ result = result.filter((i) => !!i.startDate && i.startDate >= v);
506
+ break;
507
+ case "startDateTo":
508
+ result = result.filter((i) => !!i.startDate && i.startDate <= `${v}T23:59:59`);
509
+ break;
510
+ case "endDateFrom":
511
+ result = result.filter((i) => !!i.endDate && i.endDate >= v);
512
+ break;
513
+ case "endDateTo":
514
+ result = result.filter((i) => !!i.endDate && i.endDate <= `${v}T23:59:59`);
515
+ break;
516
+ case "parentProcessInstanceKey":
517
+ result = result.filter((i) => i.parentProcessInstanceKey === v);
518
+ break;
519
+ default:
520
+ break;
521
+ }
522
+ }
523
+ return result;
524
+ }
525
+ instRunBtn.addEventListener("click", () => {
526
+ instRunBtn.disabled = true;
527
+ instRunBtn.textContent = "Searching…";
528
+ instStatusEl.textContent = "";
529
+ instResultsSection.style.display = "none";
530
+ instStore?.destroy();
531
+ instStore = new InstancesStore();
532
+ const stateCond = instConditions.find((c) => c.field === "state" && c.value.trim());
533
+ const defKeyCond = instConditions.find((c) => c.field === "processDefinitionKey" && c.value.trim());
534
+ const unsub = instStore.subscribe(() => {
535
+ if (instStore?.state.loading)
536
+ return;
537
+ instStore?.disconnect();
538
+ unsub();
539
+ instRunBtn.disabled = false;
540
+ instRunBtn.textContent = "▶ Run Search";
541
+ if (instStore?.state.error) {
542
+ instStatusEl.textContent = `Error: ${instStore.state.error}`;
543
+ return;
544
+ }
545
+ const rawItems = instStore?.state.data?.items ?? [];
546
+ const filtered = applyInstClientFilters(rawItems);
547
+ const suffix = rawItems.length !== filtered.length
548
+ ? ` (from ${rawItems.length} server results, filtered client-side)`
549
+ : "";
550
+ instResultsHeading.textContent = `${filtered.length} result${filtered.length !== 1 ? "s" : ""}${suffix}`;
551
+ setInstRows(filtered);
552
+ instResultsSection.style.display = "";
553
+ });
554
+ instStore.connect(cfg.proxyUrl, cfg.profile, 0, cfg.mock, {
555
+ state: stateCond?.value.trim() || undefined,
556
+ processDefinitionKey: defKeyCond?.value.trim() || undefined,
557
+ });
558
+ });
559
+ renderInstConditions();
560
+ // ════════════════════════════════════════════════════════════════════════
561
+ // VARIABLE SEARCH
562
+ // ════════════════════════════════════════════════════════════════════════
563
+ let varConditions = [];
564
+ let varTemplates = loadTemplates(VAR_TEMPLATE_KEY);
565
+ let varSearchAbort = null;
566
+ const varTemplateHeader = document.createElement("div");
567
+ varTemplateHeader.className = "op-search-header";
568
+ varPane.appendChild(varTemplateHeader);
569
+ const varBuilderSection = document.createElement("div");
570
+ varBuilderSection.className = "op-search-builder";
571
+ varPane.appendChild(varBuilderSection);
572
+ const varConditionsEl = document.createElement("div");
573
+ varConditionsEl.className = "op-search-conditions";
574
+ varBuilderSection.appendChild(varConditionsEl);
575
+ const varAddBtn = document.createElement("button");
576
+ varAddBtn.className = "op-search-add-btn";
577
+ varAddBtn.textContent = "+ Add Condition";
578
+ varBuilderSection.appendChild(varAddBtn);
579
+ const varActionRow = document.createElement("div");
580
+ varActionRow.className = "op-search-actions";
581
+ const varRunBtn = document.createElement("button");
582
+ varRunBtn.className = "op-action-btn op-action-btn--primary";
583
+ varRunBtn.textContent = "▶ Run Search";
584
+ varActionRow.appendChild(varRunBtn);
585
+ const varClearBtn = document.createElement("button");
586
+ varClearBtn.className = "op-action-btn";
587
+ varClearBtn.textContent = "Clear";
588
+ varActionRow.appendChild(varClearBtn);
589
+ const varStatusEl = document.createElement("span");
590
+ varStatusEl.className = "op-search-status";
591
+ varActionRow.appendChild(varStatusEl);
592
+ varBuilderSection.appendChild(varActionRow);
593
+ const varResultsSection = document.createElement("div");
594
+ varResultsSection.className = "op-search-results";
595
+ varResultsSection.style.display = "none";
596
+ varPane.appendChild(varResultsSection);
597
+ const varResultsHeading = document.createElement("div");
598
+ varResultsHeading.className = "op-search-results-heading";
599
+ varResultsSection.appendChild(varResultsHeading);
600
+ const { el: varTableEl, setRows: setVarRows } = createFilterTable({
601
+ columns: [
602
+ {
603
+ label: "Name",
604
+ width: "160px",
605
+ render: (row) => {
606
+ const span = document.createElement("span");
607
+ span.className = "op-mono-cell";
608
+ span.style.color = "var(--bpmnkit-fg)";
609
+ span.textContent = row.name;
610
+ return span;
611
+ },
612
+ sortValue: (row) => row.name,
613
+ },
614
+ {
615
+ label: "Value",
616
+ render: (row) => {
617
+ const span = document.createElement("span");
618
+ span.className = "op-search-var-value";
619
+ span.textContent = row.value;
620
+ if (row.isTruncated)
621
+ span.title = "Value is truncated";
622
+ return span;
623
+ },
624
+ sortValue: (row) => row.value,
625
+ },
626
+ {
627
+ label: "Truncated",
628
+ width: "80px",
629
+ render: (row) => (row.isTruncated ? "yes" : "no"),
630
+ sortValue: (row) => (row.isTruncated ? "1" : "0"),
631
+ },
632
+ {
633
+ label: "Instance Key",
634
+ width: "140px",
635
+ render: (row) => {
636
+ const btn = document.createElement("button");
637
+ btn.className = "op-back-btn";
638
+ btn.textContent = row.processInstanceKey;
639
+ btn.addEventListener("click", (e) => {
640
+ e.stopPropagation();
641
+ onNavigate(`/instances/${row.processInstanceKey}`);
642
+ });
643
+ return btn;
644
+ },
645
+ sortValue: (row) => row.processInstanceKey,
646
+ },
647
+ {
648
+ label: "Scope Key",
649
+ width: "130px",
650
+ render: (row) => {
651
+ const span = document.createElement("span");
652
+ span.className = "op-mono-cell";
653
+ span.textContent = row.scopeKey;
654
+ return span;
655
+ },
656
+ sortValue: (row) => row.scopeKey,
657
+ },
658
+ ],
659
+ searchFn: (row) => [row.name, row.value, row.processInstanceKey, row.scopeKey].join(" "),
660
+ emptyText: "No variables found matching your query",
661
+ });
662
+ varResultsSection.appendChild(varTableEl);
663
+ function renderVarConditions() {
664
+ buildConditionsEl(varConditionsEl, varConditions, VAR_FIELDS, renderVarConditions);
665
+ }
666
+ const { el: varTmplRowEl } = buildTemplateRow(VAR_TEMPLATE_KEY, varTemplates, (conds) => {
667
+ varConditions = conds;
668
+ renderVarConditions();
669
+ }, (updated) => {
670
+ varTemplates = updated;
671
+ }, () => varConditions, el);
672
+ varTemplateHeader.appendChild(varTmplRowEl);
673
+ varAddBtn.addEventListener("click", () => {
674
+ varConditions.push({ id: newCondId(), field: "name", value: "" });
675
+ renderVarConditions();
676
+ });
677
+ varClearBtn.addEventListener("click", () => {
678
+ varConditions = [];
679
+ renderVarConditions();
680
+ varResultsSection.style.display = "none";
681
+ varStatusEl.textContent = "";
682
+ });
683
+ function runVarSearch() {
684
+ varRunBtn.disabled = true;
685
+ varRunBtn.textContent = "Searching…";
686
+ varStatusEl.textContent = "";
687
+ varResultsSection.style.display = "none";
688
+ varSearchAbort?.();
689
+ varSearchAbort = null;
690
+ if (cfg.mock) {
691
+ const items = applyVarMockFilters(MOCK_VARIABLES);
692
+ varResultsHeading.textContent = `${items.length} result${items.length !== 1 ? "s" : ""}`;
693
+ setVarRows(items);
694
+ varResultsSection.style.display = "";
695
+ varRunBtn.disabled = false;
696
+ varRunBtn.textContent = "▶ Run Search";
697
+ return;
698
+ }
699
+ const filter = {};
700
+ for (const cond of varConditions) {
701
+ const v = cond.value.trim();
702
+ if (!v)
703
+ continue;
704
+ switch (cond.field) {
705
+ case "name":
706
+ filter.name = v;
707
+ break;
708
+ case "value":
709
+ filter.value = coerceVarValue(v);
710
+ break;
711
+ case "processInstanceKey":
712
+ filter.processInstanceKey = v;
713
+ break;
714
+ case "scopeKey":
715
+ filter.scopeKey = v;
716
+ break;
717
+ case "variableKey":
718
+ filter.variableKey = v;
719
+ break;
720
+ case "tenantId":
721
+ filter.tenantId = v;
722
+ break;
723
+ case "isTruncated":
724
+ filter.isTruncated = v === "true";
725
+ break;
726
+ default:
727
+ break;
728
+ }
729
+ }
730
+ const headers = { "Content-Type": "application/json" };
731
+ if (cfg.profile)
732
+ headers["x-profile"] = cfg.profile;
733
+ let aborted = false;
734
+ varSearchAbort = () => {
735
+ aborted = true;
736
+ };
737
+ fetch(`${cfg.proxyUrl}/api/variables/search`, {
738
+ method: "POST",
739
+ headers,
740
+ body: JSON.stringify({ filter, page: { limit: 100 } }),
741
+ })
742
+ .then((r) => r.ok ? r.json() : r.text().then((t) => Promise.reject(new Error(`${r.status}: ${t}`))))
743
+ .then((result) => {
744
+ if (aborted)
745
+ return;
746
+ const items = result.items ?? [];
747
+ varResultsHeading.textContent = `${items.length} result${items.length !== 1 ? "s" : ""}`;
748
+ setVarRows(items);
749
+ varResultsSection.style.display = "";
750
+ })
751
+ .catch((err) => {
752
+ if (aborted)
753
+ return;
754
+ varStatusEl.textContent = `Error: ${String(err)}`;
755
+ })
756
+ .finally(() => {
757
+ if (aborted)
758
+ return;
759
+ varRunBtn.disabled = false;
760
+ varRunBtn.textContent = "▶ Run Search";
761
+ });
762
+ }
763
+ function applyVarMockFilters(items) {
764
+ let result = items;
765
+ for (const cond of varConditions) {
766
+ const v = cond.value.trim();
767
+ if (!v)
768
+ continue;
769
+ const vl = v.toLowerCase();
770
+ switch (cond.field) {
771
+ case "name":
772
+ result = result.filter((i) => i.name.toLowerCase().includes(vl));
773
+ break;
774
+ case "value":
775
+ result = result.filter((i) => i.value.toLowerCase().includes(vl));
776
+ break;
777
+ case "processInstanceKey":
778
+ result = result.filter((i) => i.processInstanceKey === v);
779
+ break;
780
+ case "scopeKey":
781
+ result = result.filter((i) => i.scopeKey === v);
782
+ break;
783
+ case "variableKey":
784
+ result = result.filter((i) => i.variableKey === v);
785
+ break;
786
+ case "tenantId":
787
+ result = result.filter((i) => i.tenantId?.toLowerCase().includes(vl));
788
+ break;
789
+ case "isTruncated":
790
+ result = result.filter((i) => String(Boolean(i.isTruncated)) === v);
791
+ break;
792
+ default:
793
+ break;
794
+ }
795
+ }
796
+ return result;
797
+ }
798
+ varRunBtn.addEventListener("click", runVarSearch);
799
+ renderVarConditions();
800
+ // ════════════════════════════════════════════════════════════════════════
801
+ // AI SEARCH
802
+ // ════════════════════════════════════════════════════════════════════════
803
+ const aiBuilderSection = document.createElement("div");
804
+ aiBuilderSection.className = "op-search-builder";
805
+ aiPane.appendChild(aiBuilderSection);
806
+ const aiInputRow = document.createElement("div");
807
+ aiInputRow.className = "op-ai-search-row";
808
+ aiBuilderSection.appendChild(aiInputRow);
809
+ const aiInput = document.createElement("input");
810
+ aiInput.type = "text";
811
+ aiInput.className = "op-ai-search-input";
812
+ aiInput.placeholder =
813
+ 'e.g. "active instances with incidents", "variable amount greater than 1000"';
814
+ aiInputRow.appendChild(aiInput);
815
+ const aiRunBtn = document.createElement("button");
816
+ aiRunBtn.className = "op-action-btn op-action-btn--primary";
817
+ aiRunBtn.textContent = "▶ Search";
818
+ aiInputRow.appendChild(aiRunBtn);
819
+ const aiHint = document.createElement("div");
820
+ aiHint.className = "op-ai-search-hint";
821
+ aiHint.textContent = "Ask in plain language — AI will translate your query to a Camunda search.";
822
+ aiBuilderSection.appendChild(aiHint);
823
+ const aiStatusEl = document.createElement("div");
824
+ aiStatusEl.className = "op-search-status";
825
+ aiBuilderSection.appendChild(aiStatusEl);
826
+ const aiResultsSection = document.createElement("div");
827
+ aiResultsSection.className = "op-search-results";
828
+ aiResultsSection.style.display = "none";
829
+ aiPane.appendChild(aiResultsSection);
830
+ const aiResultsHeading = document.createElement("div");
831
+ aiResultsHeading.className = "op-search-results-heading";
832
+ aiResultsSection.appendChild(aiResultsHeading);
833
+ const aiFilterEl = document.createElement("div");
834
+ aiFilterEl.className = "op-ai-search-filter";
835
+ aiResultsSection.appendChild(aiFilterEl);
836
+ // Results are either instances or variables depending on what the AI decides
837
+ const { el: aiInstTableEl, setRows: setAiInstRows } = createFilterTable({
838
+ columns: [
839
+ {
840
+ label: "Key",
841
+ width: "140px",
842
+ render: (row) => row.processInstanceKey,
843
+ sortValue: (row) => row.processInstanceKey,
844
+ },
845
+ {
846
+ label: "Process",
847
+ render: (row) => row.processDefinitionName ?? row.processDefinitionId ?? "—",
848
+ sortValue: (row) => row.processDefinitionName ?? row.processDefinitionId ?? "",
849
+ },
850
+ {
851
+ label: "State",
852
+ width: "110px",
853
+ render: (row) => {
854
+ const wrap = document.createElement("div");
855
+ wrap.className = "bpmnkit-badge-wrap";
856
+ wrap.appendChild(badge(row.state));
857
+ return wrap;
858
+ },
859
+ sortValue: (row) => row.state,
860
+ },
861
+ {
862
+ label: "Started",
863
+ width: "90px",
864
+ render: (row) => relTime(row.startDate),
865
+ sortValue: (row) => row.startDate ?? "",
866
+ },
867
+ ],
868
+ searchFn: (row) => [row.processInstanceKey, row.processDefinitionId, row.processDefinitionName, row.state]
869
+ .filter(Boolean)
870
+ .join(" "),
871
+ onRowClick: (row) => onNavigate(`/instances/${row.processInstanceKey}`),
872
+ emptyText: "No instances found",
873
+ });
874
+ aiResultsSection.appendChild(aiInstTableEl);
875
+ const { el: aiVarTableEl, setRows: setAiVarRows } = createFilterTable({
876
+ columns: [
877
+ {
878
+ label: "Name",
879
+ width: "160px",
880
+ render: (row) => {
881
+ const span = document.createElement("span");
882
+ span.className = "op-mono-cell";
883
+ span.textContent = row.name;
884
+ return span;
885
+ },
886
+ sortValue: (row) => row.name,
887
+ },
888
+ {
889
+ label: "Value",
890
+ render: (row) => {
891
+ const span = document.createElement("span");
892
+ span.className = "op-search-var-value";
893
+ span.textContent = row.value;
894
+ return span;
895
+ },
896
+ sortValue: (row) => row.value,
897
+ },
898
+ {
899
+ label: "Instance Key",
900
+ width: "140px",
901
+ render: (row) => {
902
+ const btn = document.createElement("button");
903
+ btn.className = "op-back-btn";
904
+ btn.textContent = row.processInstanceKey;
905
+ btn.addEventListener("click", (e) => {
906
+ e.stopPropagation();
907
+ onNavigate(`/instances/${row.processInstanceKey}`);
908
+ });
909
+ return btn;
910
+ },
911
+ sortValue: (row) => row.processInstanceKey,
912
+ },
913
+ ],
914
+ searchFn: (row) => [row.name, row.value, row.processInstanceKey].join(" "),
915
+ emptyText: "No variables found",
916
+ });
917
+ aiResultsSection.appendChild(aiVarTableEl);
918
+ aiVarTableEl.style.display = "none";
919
+ let aiAbort = false;
920
+ function runAiSearch() {
921
+ const query = aiInput.value.trim();
922
+ if (!query)
923
+ return;
924
+ aiRunBtn.disabled = true;
925
+ aiRunBtn.textContent = "Searching…";
926
+ aiStatusEl.textContent = "";
927
+ aiResultsSection.style.display = "none";
928
+ aiAbort = false;
929
+ const headers = { "Content-Type": "application/json" };
930
+ if (cfg.profile)
931
+ headers["x-profile"] = cfg.profile;
932
+ fetch(`${cfg.proxyUrl}/operate/ai-search`, {
933
+ method: "POST",
934
+ headers,
935
+ body: JSON.stringify({ query }),
936
+ })
937
+ .then((r) => r.ok ? r.json() : r.text().then((t) => Promise.reject(new Error(`${r.status}: ${t}`))))
938
+ .then((result) => {
939
+ if (aiAbort)
940
+ return;
941
+ const count = result.items.length;
942
+ aiResultsHeading.textContent = `${count} result${count !== 1 ? "s" : ""} (${result.total} total)`;
943
+ aiFilterEl.textContent = `Interpreted as: ${JSON.stringify(result.filter)}`;
944
+ if (result.endpoint === "variables") {
945
+ aiInstTableEl.style.display = "none";
946
+ aiVarTableEl.style.display = "";
947
+ setAiVarRows(result.items);
948
+ }
949
+ else {
950
+ aiVarTableEl.style.display = "none";
951
+ aiInstTableEl.style.display = "";
952
+ setAiInstRows(result.items);
953
+ }
954
+ aiResultsSection.style.display = "";
955
+ })
956
+ .catch((err) => {
957
+ if (aiAbort)
958
+ return;
959
+ aiStatusEl.textContent = `Error: ${String(err)}`;
960
+ })
961
+ .finally(() => {
962
+ if (aiAbort)
963
+ return;
964
+ aiRunBtn.disabled = false;
965
+ aiRunBtn.textContent = "▶ Search";
966
+ });
967
+ }
968
+ aiRunBtn.addEventListener("click", runAiSearch);
969
+ aiInput.addEventListener("keydown", (e) => {
970
+ if (e.key === "Enter")
971
+ runAiSearch();
972
+ });
973
+ // ── Check proxy for AI availability (async, non-blocking) ─────────────────
974
+ if (!cfg.mock) {
975
+ fetch(`${cfg.proxyUrl}/status`)
976
+ .then((r) => r.ok ? r.json() : null)
977
+ .then((status) => {
978
+ if (status?.ready && status.backend !== null) {
979
+ aiTabBtn.style.display = "";
980
+ }
981
+ })
982
+ .catch(() => {
983
+ /* proxy not running — AI tab stays hidden */
984
+ });
985
+ }
986
+ return {
987
+ el,
988
+ destroy() {
989
+ instStore?.destroy();
990
+ varSearchAbort?.();
991
+ aiAbort = true;
992
+ },
993
+ };
994
+ }
995
+ /** Coerce a user-entered variable value to its JSON-serialized form.
996
+ * If the input is already valid JSON, use it as-is.
997
+ * Otherwise wrap it in double quotes (treat as string literal). */
998
+ function coerceVarValue(v) {
999
+ try {
1000
+ JSON.parse(v);
1001
+ return v;
1002
+ }
1003
+ catch {
1004
+ return JSON.stringify(v);
1005
+ }
1006
+ }
1007
+ //# sourceMappingURL=search.js.map