@elabs-ai/components-process 4.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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/core/index.d.ts +1029 -0
- package/dist/core/index.js +1553 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/process-worker.js +462 -0
- package/dist/core/process-worker.js.map +1 -0
- package/dist/index.d.ts +1153 -0
- package/dist/index.js +3146 -0
- package/dist/index.js.map +1 -0
- package/dist/test/index.d.ts +196 -0
- package/dist/test/index.js +527 -0
- package/dist/test/index.js.map +1 -0
- package/package.json +80 -0
- package/src/abstraction-controls/abstraction-controls-fixtures.ts +86 -0
- package/src/abstraction-controls/abstraction-controls.stories.tsx +188 -0
- package/src/abstraction-controls/abstraction-controls.test.tsx +226 -0
- package/src/abstraction-controls/abstraction-controls.tsx +288 -0
- package/src/abstraction-controls/auto-abstraction.test.ts +196 -0
- package/src/abstraction-controls/auto-abstraction.ts +128 -0
- package/src/abstraction-controls/index.ts +4 -0
- package/src/core/abstract-graph.test.ts +209 -0
- package/src/core/abstract-graph.ts +407 -0
- package/src/core/adapters/csv.test.ts +131 -0
- package/src/core/adapters/csv.ts +146 -0
- package/src/core/adapters/flat.test.ts +149 -0
- package/src/core/adapters/flat.ts +168 -0
- package/src/core/aggregate-performance.test.ts +208 -0
- package/src/core/aggregate-performance.ts +200 -0
- package/src/core/detect-rework.test.ts +134 -0
- package/src/core/detect-rework.ts +100 -0
- package/src/core/discover-graph.test.ts +378 -0
- package/src/core/discover-graph.ts +202 -0
- package/src/core/duration-stats.test.ts +116 -0
- package/src/core/duration-stats.ts +162 -0
- package/src/core/event-log.test.ts +224 -0
- package/src/core/event-log.ts +244 -0
- package/src/core/extract-variants.test.ts +126 -0
- package/src/core/extract-variants.ts +140 -0
- package/src/core/filter-log.test.ts +193 -0
- package/src/core/filter-log.ts +215 -0
- package/src/core/fixtures/generate-bpi-2012-subset.test.ts +50 -0
- package/src/core/fixtures/generate-bpi-2012-subset.ts +216 -0
- package/src/core/fixtures/generate-bpi-2012-subset.write.ts +40 -0
- package/src/core/fixtures/order-to-cash-small.json +200 -0
- package/src/core/fixtures/synthetic-log.test.ts +109 -0
- package/src/core/fixtures/synthetic-log.ts +167 -0
- package/src/core/index.ts +118 -0
- package/src/core/reconcile-graph.test.ts +175 -0
- package/src/core/reconcile-graph.ts +107 -0
- package/src/core/scale.test.ts +80 -0
- package/src/core/scale.ts +100 -0
- package/src/core/types.ts +151 -0
- package/src/core/worker/create-process-worker.test.ts +255 -0
- package/src/core/worker/create-process-worker.ts +211 -0
- package/src/core/worker/process-worker.ts +80 -0
- package/src/index.ts +29 -0
- package/src/metric-layer-switch/index.ts +6 -0
- package/src/metric-layer-switch/metric-layer-switch.stories.tsx +131 -0
- package/src/metric-layer-switch/metric-layer-switch.test.tsx +102 -0
- package/src/metric-layer-switch/metric-layer-switch.tsx +276 -0
- package/src/process-explorer.stories.tsx +392 -0
- package/src/process-kpi-strip/index.ts +6 -0
- package/src/process-kpi-strip/process-kpi-strip.stories.tsx +128 -0
- package/src/process-kpi-strip/process-kpi-strip.test.tsx +106 -0
- package/src/process-kpi-strip/process-kpi-strip.tsx +237 -0
- package/src/process-map/index.ts +13 -0
- package/src/process-map/map-model.test.ts +326 -0
- package/src/process-map/map-model.ts +873 -0
- package/src/process-map/process-activity-node.tsx +200 -0
- package/src/process-map/process-map-context.ts +71 -0
- package/src/process-map/process-map.stories.tsx +673 -0
- package/src/process-map/process-map.test.tsx +523 -0
- package/src/process-map/process-map.tsx +979 -0
- package/src/process-map/process-transition-edge.test.tsx +160 -0
- package/src/process-map/process-transition-edge.tsx +151 -0
- package/src/process-map/use-process-layout.test.tsx +265 -0
- package/src/process-map/use-process-layout.ts +315 -0
- package/src/test/contract.test.ts +99 -0
- package/src/test/contract.ts +118 -0
- package/src/test/doubles.test.tsx +51 -0
- package/src/test/doubles.tsx +82 -0
- package/src/test/index.ts +34 -0
- package/src/test/primitives.tsx +35 -0
- package/src/use-process-explorer/index.ts +8 -0
- package/src/use-process-explorer/use-process-explorer.test.ts +564 -0
- package/src/use-process-explorer/use-process-explorer.ts +540 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3146 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/core/scale.ts
|
|
4
|
+
function ascending(a, b) {
|
|
5
|
+
return a - b;
|
|
6
|
+
}
|
|
7
|
+
function minMax(values) {
|
|
8
|
+
let min = Number.POSITIVE_INFINITY;
|
|
9
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
10
|
+
let seen = false;
|
|
11
|
+
for (const v of values) {
|
|
12
|
+
if (!Number.isFinite(v)) continue;
|
|
13
|
+
seen = true;
|
|
14
|
+
if (v < min) min = v;
|
|
15
|
+
if (v > max) max = v;
|
|
16
|
+
}
|
|
17
|
+
return seen ? [min, max] : [0, 0];
|
|
18
|
+
}
|
|
19
|
+
function quantileSorted(sorted, q) {
|
|
20
|
+
const n = sorted.length;
|
|
21
|
+
if (n === 0) return 0;
|
|
22
|
+
if (n === 1) return sorted[0];
|
|
23
|
+
const clamped = q < 0 ? 0 : q > 1 ? 1 : q;
|
|
24
|
+
const pos = clamped * (n - 1);
|
|
25
|
+
const lo = Math.floor(pos);
|
|
26
|
+
const hi = Math.ceil(pos);
|
|
27
|
+
const loValue = sorted[lo];
|
|
28
|
+
if (lo === hi) return loValue;
|
|
29
|
+
const hiValue = sorted[hi];
|
|
30
|
+
return loValue + (hiValue - loValue) * (pos - lo);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/core/duration-stats.ts
|
|
34
|
+
var TRIM_FRACTION = 0.1;
|
|
35
|
+
var EMPTY_DURATION_STATS = Object.freeze({
|
|
36
|
+
min: 0,
|
|
37
|
+
max: 0,
|
|
38
|
+
mean: 0,
|
|
39
|
+
median: 0,
|
|
40
|
+
p90: 0,
|
|
41
|
+
sum: 0,
|
|
42
|
+
trimmedMean: 0
|
|
43
|
+
});
|
|
44
|
+
function emptyDurationStats() {
|
|
45
|
+
return { ...EMPTY_DURATION_STATS };
|
|
46
|
+
}
|
|
47
|
+
function durationStats(samples) {
|
|
48
|
+
const sorted = [];
|
|
49
|
+
for (const s of samples) if (Number.isFinite(s)) sorted.push(s);
|
|
50
|
+
const n = sorted.length;
|
|
51
|
+
if (n === 0) return emptyDurationStats();
|
|
52
|
+
sorted.sort(ascending);
|
|
53
|
+
let sum = 0;
|
|
54
|
+
for (const s of sorted) sum += s;
|
|
55
|
+
const trim = Math.floor(n * TRIM_FRACTION);
|
|
56
|
+
const lo = trim;
|
|
57
|
+
const hi = n - trim;
|
|
58
|
+
let trimmedMean;
|
|
59
|
+
if (hi - lo <= 0) {
|
|
60
|
+
trimmedMean = sum / n;
|
|
61
|
+
} else {
|
|
62
|
+
let trimmedSum = 0;
|
|
63
|
+
for (let i = lo; i < hi; i += 1) trimmedSum += sorted[i];
|
|
64
|
+
trimmedMean = trimmedSum / (hi - lo);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
min: sorted[0],
|
|
68
|
+
max: sorted[n - 1],
|
|
69
|
+
mean: sum / n,
|
|
70
|
+
median: quantileSorted(sorted, 0.5),
|
|
71
|
+
p90: quantileSorted(sorted, 0.9),
|
|
72
|
+
sum,
|
|
73
|
+
trimmedMean
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
var DURATION_SAMPLE_CAP = 4096;
|
|
77
|
+
function mulberry32(seed) {
|
|
78
|
+
let a = seed >>> 0;
|
|
79
|
+
return () => {
|
|
80
|
+
a = a + 1831565813 >>> 0;
|
|
81
|
+
let t = a;
|
|
82
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
83
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
84
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
var DurationSampler = class {
|
|
88
|
+
capacity;
|
|
89
|
+
random;
|
|
90
|
+
reservoir = [];
|
|
91
|
+
/** Number of finite samples offered, including those the reservoir discarded. */
|
|
92
|
+
seen = 0;
|
|
93
|
+
total = 0;
|
|
94
|
+
lowest = Number.POSITIVE_INFINITY;
|
|
95
|
+
highest = Number.NEGATIVE_INFINITY;
|
|
96
|
+
constructor(seed = 2654435769, capacity = DURATION_SAMPLE_CAP) {
|
|
97
|
+
this.capacity = capacity > 0 ? capacity : 1;
|
|
98
|
+
this.random = mulberry32(seed);
|
|
99
|
+
}
|
|
100
|
+
/** Offer one sample. Non-finite values are ignored. */
|
|
101
|
+
add(sample) {
|
|
102
|
+
if (!Number.isFinite(sample)) return;
|
|
103
|
+
this.total += sample;
|
|
104
|
+
if (sample < this.lowest) this.lowest = sample;
|
|
105
|
+
if (sample > this.highest) this.highest = sample;
|
|
106
|
+
if (this.reservoir.length < this.capacity) {
|
|
107
|
+
this.reservoir.push(sample);
|
|
108
|
+
} else {
|
|
109
|
+
const j = Math.floor(this.random() * (this.seen + 1));
|
|
110
|
+
if (j < this.capacity) this.reservoir[j] = sample;
|
|
111
|
+
}
|
|
112
|
+
this.seen += 1;
|
|
113
|
+
}
|
|
114
|
+
/** True number of finite samples offered. */
|
|
115
|
+
get size() {
|
|
116
|
+
return this.seen;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Summarize what was collected. `sum`, `mean`, `min` and `max` are EXACT even past the
|
|
120
|
+
* cap (they are accumulated, not read off the reservoir); the remaining three are
|
|
121
|
+
* computed from the retained sample.
|
|
122
|
+
*/
|
|
123
|
+
stats() {
|
|
124
|
+
if (this.seen === 0) return emptyDurationStats();
|
|
125
|
+
const fromReservoir = durationStats(this.reservoir);
|
|
126
|
+
return {
|
|
127
|
+
...fromReservoir,
|
|
128
|
+
min: this.lowest,
|
|
129
|
+
max: this.highest,
|
|
130
|
+
sum: this.total,
|
|
131
|
+
mean: this.total / this.seen
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// src/core/event-log.ts
|
|
137
|
+
function isNormalizedLog(log) {
|
|
138
|
+
return Array.isArray(log.cases);
|
|
139
|
+
}
|
|
140
|
+
function asNormalizedLog(log) {
|
|
141
|
+
return isNormalizedLog(log) ? log : normalizeLog(log);
|
|
142
|
+
}
|
|
143
|
+
function toEpochMs(value) {
|
|
144
|
+
if (value === void 0 || value === null) return Number.NaN;
|
|
145
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : Number.NaN;
|
|
146
|
+
if (value instanceof Date) return value.getTime();
|
|
147
|
+
const parsed = Date.parse(value);
|
|
148
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
149
|
+
const asNumber = Number(value);
|
|
150
|
+
return Number.isFinite(asNumber) && value.trim() !== "" ? asNumber : Number.NaN;
|
|
151
|
+
}
|
|
152
|
+
function byTimestamp(a, b) {
|
|
153
|
+
if (Number.isNaN(a.at) || Number.isNaN(b.at)) return 0;
|
|
154
|
+
return a.at - b.at;
|
|
155
|
+
}
|
|
156
|
+
function normalizeLog(log) {
|
|
157
|
+
const staged = /* @__PURE__ */ new Map();
|
|
158
|
+
const order = [];
|
|
159
|
+
for (const row of log.events) {
|
|
160
|
+
if (!row || typeof row.caseId !== "string" || row.caseId === "") continue;
|
|
161
|
+
if (typeof row.activity !== "string" || row.activity === "") continue;
|
|
162
|
+
let bucket = staged.get(row.caseId);
|
|
163
|
+
if (bucket === void 0) {
|
|
164
|
+
bucket = [];
|
|
165
|
+
staged.set(row.caseId, bucket);
|
|
166
|
+
order.push(row.caseId);
|
|
167
|
+
}
|
|
168
|
+
bucket.push({ at: toEpochMs(row.timestamp), row });
|
|
169
|
+
}
|
|
170
|
+
const cases = [];
|
|
171
|
+
let events = 0;
|
|
172
|
+
for (const caseId of order) {
|
|
173
|
+
const bucket = staged.get(caseId);
|
|
174
|
+
bucket.sort(byTimestamp);
|
|
175
|
+
const instances = [];
|
|
176
|
+
const open = /* @__PURE__ */ new Map();
|
|
177
|
+
for (const { at, row } of bucket) {
|
|
178
|
+
if (row.lifecycle === "start") {
|
|
179
|
+
const index2 = instances.length;
|
|
180
|
+
instances.push(makeInstance(row, at, at, true));
|
|
181
|
+
const queue = open.get(row.activity);
|
|
182
|
+
if (queue === void 0) open.set(row.activity, [index2]);
|
|
183
|
+
else queue.push(index2);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (row.lifecycle === "complete") {
|
|
187
|
+
const queue = open.get(row.activity);
|
|
188
|
+
const index2 = queue?.shift();
|
|
189
|
+
if (index2 !== void 0) {
|
|
190
|
+
const instance = instances[index2];
|
|
191
|
+
instance.end = at;
|
|
192
|
+
instance.duration = durationOf(instance.start, at);
|
|
193
|
+
instance.isOpen = false;
|
|
194
|
+
if (instance.resource === void 0 && row.resource !== void 0) {
|
|
195
|
+
instance.resource = row.resource;
|
|
196
|
+
}
|
|
197
|
+
if (instance.attributes === void 0 && row.attributes !== void 0) {
|
|
198
|
+
instance.attributes = row.attributes;
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const end = at;
|
|
204
|
+
const explicitStart = toEpochMs(row.startTimestamp);
|
|
205
|
+
const start = Number.isNaN(explicitStart) ? end : explicitStart;
|
|
206
|
+
instances.push(makeInstance(row, start, end, false));
|
|
207
|
+
}
|
|
208
|
+
if (instances.length === 0) continue;
|
|
209
|
+
instances.sort(byStart);
|
|
210
|
+
let caseStart = Number.POSITIVE_INFINITY;
|
|
211
|
+
let caseEnd = Number.NEGATIVE_INFINITY;
|
|
212
|
+
for (const instance of instances) {
|
|
213
|
+
if (Number.isFinite(instance.start) && instance.start < caseStart) caseStart = instance.start;
|
|
214
|
+
if (Number.isFinite(instance.end) && instance.end > caseEnd) caseEnd = instance.end;
|
|
215
|
+
}
|
|
216
|
+
const hasExtent = caseStart !== Number.POSITIVE_INFINITY && caseEnd !== Number.NEGATIVE_INFINITY;
|
|
217
|
+
const normalizedCase = {
|
|
218
|
+
caseId,
|
|
219
|
+
events: instances,
|
|
220
|
+
start: hasExtent ? caseStart : Number.NaN,
|
|
221
|
+
end: hasExtent ? caseEnd : Number.NaN,
|
|
222
|
+
duration: hasExtent ? durationOf(caseStart, caseEnd) : Number.NaN
|
|
223
|
+
};
|
|
224
|
+
const caseAttributes = log.caseAttributes?.[caseId];
|
|
225
|
+
if (caseAttributes !== void 0) normalizedCase.attributes = caseAttributes;
|
|
226
|
+
cases.push(normalizedCase);
|
|
227
|
+
events += instances.length;
|
|
228
|
+
}
|
|
229
|
+
return { cases, totals: { cases: cases.length, events } };
|
|
230
|
+
}
|
|
231
|
+
function byStart(a, b) {
|
|
232
|
+
if (Number.isNaN(a.start) || Number.isNaN(b.start)) return 0;
|
|
233
|
+
return a.start - b.start;
|
|
234
|
+
}
|
|
235
|
+
function durationOf(start, end) {
|
|
236
|
+
const delta = end - start;
|
|
237
|
+
if (!Number.isFinite(delta)) return Number.NaN;
|
|
238
|
+
return delta < 0 ? 0 : delta;
|
|
239
|
+
}
|
|
240
|
+
function makeInstance(row, start, end, isOpen) {
|
|
241
|
+
const instance = {
|
|
242
|
+
activity: row.activity,
|
|
243
|
+
start,
|
|
244
|
+
end,
|
|
245
|
+
duration: durationOf(start, end),
|
|
246
|
+
isOpen
|
|
247
|
+
};
|
|
248
|
+
if (row.resource !== void 0) instance.resource = row.resource;
|
|
249
|
+
if (row.attributes !== void 0) instance.attributes = row.attributes;
|
|
250
|
+
return instance;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/core/extract-variants.ts
|
|
254
|
+
var VARIANT_KEY_SEPARATOR = "";
|
|
255
|
+
function variantKey(sequence) {
|
|
256
|
+
return sequence.join(VARIANT_KEY_SEPARATOR);
|
|
257
|
+
}
|
|
258
|
+
function fnv1a32(text, basis) {
|
|
259
|
+
let hash = basis >>> 0;
|
|
260
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
261
|
+
hash ^= text.charCodeAt(i);
|
|
262
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
263
|
+
}
|
|
264
|
+
return hash >>> 0;
|
|
265
|
+
}
|
|
266
|
+
function hex8(value) {
|
|
267
|
+
return (value >>> 0).toString(16).padStart(8, "0");
|
|
268
|
+
}
|
|
269
|
+
function variantId(sequence) {
|
|
270
|
+
const key = variantKey(sequence);
|
|
271
|
+
return `v${hex8(fnv1a32(key, 2166136261))}${hex8(fnv1a32(key, 2216829733))}`;
|
|
272
|
+
}
|
|
273
|
+
function extractVariants(log) {
|
|
274
|
+
const normalized = asNormalizedLog(log);
|
|
275
|
+
const totalCases = normalized.cases.length;
|
|
276
|
+
if (totalCases === 0) return [];
|
|
277
|
+
const groups = /* @__PURE__ */ new Map();
|
|
278
|
+
for (const kase of normalized.cases) {
|
|
279
|
+
const sequence = sequenceOf(kase);
|
|
280
|
+
const key = variantKey(sequence);
|
|
281
|
+
let group = groups.get(key);
|
|
282
|
+
if (group === void 0) {
|
|
283
|
+
group = { key, sequence, caseIds: [], durations: [] };
|
|
284
|
+
groups.set(key, group);
|
|
285
|
+
}
|
|
286
|
+
group.caseIds.push(kase.caseId);
|
|
287
|
+
group.durations.push(kase.duration);
|
|
288
|
+
}
|
|
289
|
+
const ranked = [...groups.values()].sort(
|
|
290
|
+
(a, b) => b.caseIds.length - a.caseIds.length || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)
|
|
291
|
+
);
|
|
292
|
+
const taken = /* @__PURE__ */ new Set();
|
|
293
|
+
const variants = [];
|
|
294
|
+
let cumulativeCount = 0;
|
|
295
|
+
for (const group of ranked) {
|
|
296
|
+
const count = group.caseIds.length;
|
|
297
|
+
cumulativeCount += count;
|
|
298
|
+
let id = variantId(group.sequence);
|
|
299
|
+
if (taken.has(id)) {
|
|
300
|
+
let suffix = 1;
|
|
301
|
+
while (taken.has(`${id}-${suffix}`)) suffix += 1;
|
|
302
|
+
id = `${id}-${suffix}`;
|
|
303
|
+
}
|
|
304
|
+
taken.add(id);
|
|
305
|
+
variants.push({
|
|
306
|
+
id,
|
|
307
|
+
sequence: group.sequence,
|
|
308
|
+
count,
|
|
309
|
+
share: count / totalCases,
|
|
310
|
+
cumulativeShare: cumulativeCount / totalCases,
|
|
311
|
+
caseIds: group.caseIds,
|
|
312
|
+
duration: durationStats(group.durations)
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return variants;
|
|
316
|
+
}
|
|
317
|
+
function sequenceOf(kase) {
|
|
318
|
+
const sequence = new Array(kase.events.length);
|
|
319
|
+
for (let i = 0; i < kase.events.length; i += 1) {
|
|
320
|
+
sequence[i] = kase.events[i].activity;
|
|
321
|
+
}
|
|
322
|
+
return sequence;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/core/discover-graph.ts
|
|
326
|
+
var EDGE_KEY_SEPARATOR = VARIANT_KEY_SEPARATOR;
|
|
327
|
+
function discoverGraph(log, options = {}) {
|
|
328
|
+
const flowTime = options.flowTime ?? "idle_time";
|
|
329
|
+
const capacity = options.maxDurationSamples ?? DURATION_SAMPLE_CAP;
|
|
330
|
+
const normalized = asNormalizedLog(log);
|
|
331
|
+
const activities = /* @__PURE__ */ new Map();
|
|
332
|
+
const transitions = /* @__PURE__ */ new Map();
|
|
333
|
+
const startActivities = /* @__PURE__ */ new Map();
|
|
334
|
+
const endActivities = /* @__PURE__ */ new Map();
|
|
335
|
+
const variantKeys = /* @__PURE__ */ new Set();
|
|
336
|
+
let samplerIndex = 0;
|
|
337
|
+
const nextSeed = () => {
|
|
338
|
+
samplerIndex += 1;
|
|
339
|
+
return (2654435769 ^ Math.imul(samplerIndex, 2654435761)) >>> 0;
|
|
340
|
+
};
|
|
341
|
+
let events = 0;
|
|
342
|
+
const seenActivities = /* @__PURE__ */ new Set();
|
|
343
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
344
|
+
for (const kase of normalized.cases) {
|
|
345
|
+
const trace = kase.events;
|
|
346
|
+
if (trace.length === 0) continue;
|
|
347
|
+
events += trace.length;
|
|
348
|
+
seenActivities.clear();
|
|
349
|
+
seenEdges.clear();
|
|
350
|
+
const sequence = new Array(trace.length);
|
|
351
|
+
for (let i = 0; i < trace.length; i += 1) {
|
|
352
|
+
const event = trace[i];
|
|
353
|
+
const name = event.activity;
|
|
354
|
+
sequence[i] = name;
|
|
355
|
+
let activity = activities.get(name);
|
|
356
|
+
if (activity === void 0) {
|
|
357
|
+
activity = { instances: 0, cases: 0, duration: new DurationSampler(nextSeed(), capacity) };
|
|
358
|
+
activities.set(name, activity);
|
|
359
|
+
}
|
|
360
|
+
activity.instances += 1;
|
|
361
|
+
activity.duration.add(event.duration);
|
|
362
|
+
if (!seenActivities.has(name)) {
|
|
363
|
+
seenActivities.add(name);
|
|
364
|
+
activity.cases += 1;
|
|
365
|
+
}
|
|
366
|
+
if (i === 0) continue;
|
|
367
|
+
const previous = trace[i - 1];
|
|
368
|
+
const source = previous.activity;
|
|
369
|
+
const key = `${source}${EDGE_KEY_SEPARATOR}${name}`;
|
|
370
|
+
let edge = transitions.get(key);
|
|
371
|
+
if (edge === void 0) {
|
|
372
|
+
edge = {
|
|
373
|
+
source,
|
|
374
|
+
target: name,
|
|
375
|
+
count: 0,
|
|
376
|
+
caseCount: 0,
|
|
377
|
+
duration: new DurationSampler(nextSeed(), capacity)
|
|
378
|
+
};
|
|
379
|
+
transitions.set(key, edge);
|
|
380
|
+
}
|
|
381
|
+
edge.count += 1;
|
|
382
|
+
edge.duration.add(
|
|
383
|
+
flowTime === "inter_start_time" ? event.start - previous.start : event.start - previous.end
|
|
384
|
+
);
|
|
385
|
+
if (!seenEdges.has(key)) {
|
|
386
|
+
seenEdges.add(key);
|
|
387
|
+
edge.caseCount += 1;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const first = trace[0];
|
|
391
|
+
const last = trace[trace.length - 1];
|
|
392
|
+
startActivities.set(first.activity, (startActivities.get(first.activity) ?? 0) + 1);
|
|
393
|
+
endActivities.set(last.activity, (endActivities.get(last.activity) ?? 0) + 1);
|
|
394
|
+
variantKeys.add(sequence.join(VARIANT_KEY_SEPARATOR));
|
|
395
|
+
}
|
|
396
|
+
const activityList = [];
|
|
397
|
+
for (const [id, accumulator] of activities) {
|
|
398
|
+
activityList.push({
|
|
399
|
+
id,
|
|
400
|
+
label: id,
|
|
401
|
+
instances: accumulator.instances,
|
|
402
|
+
cases: accumulator.cases,
|
|
403
|
+
isStart: startActivities.has(id),
|
|
404
|
+
isEnd: endActivities.has(id),
|
|
405
|
+
duration: accumulator.duration.stats()
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
activityList.sort((a, b) => b.instances - a.instances || compareStrings(a.id, b.id));
|
|
409
|
+
const transitionList = [];
|
|
410
|
+
for (const accumulator of transitions.values()) {
|
|
411
|
+
transitionList.push({
|
|
412
|
+
source: accumulator.source,
|
|
413
|
+
target: accumulator.target,
|
|
414
|
+
count: accumulator.count,
|
|
415
|
+
caseCount: accumulator.caseCount,
|
|
416
|
+
duration: accumulator.duration.stats(),
|
|
417
|
+
isSelfLoop: accumulator.source === accumulator.target,
|
|
418
|
+
isBackEdge: false
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
transitionList.sort(
|
|
422
|
+
(a, b) => b.count - a.count || compareStrings(a.source, b.source) || compareStrings(a.target, b.target)
|
|
423
|
+
);
|
|
424
|
+
return {
|
|
425
|
+
activities: activityList,
|
|
426
|
+
transitions: transitionList,
|
|
427
|
+
startActivities: toSortedRecord(startActivities),
|
|
428
|
+
endActivities: toSortedRecord(endActivities),
|
|
429
|
+
totals: { cases: normalized.cases.length, events, variants: variantKeys.size }
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function compareStrings(a, b) {
|
|
433
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
434
|
+
}
|
|
435
|
+
function toSortedRecord(counts) {
|
|
436
|
+
const out = {};
|
|
437
|
+
for (const key of [...counts.keys()].sort()) out[key] = counts.get(key);
|
|
438
|
+
return out;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/core/aggregate-performance.ts
|
|
442
|
+
var DURATION_UNIT_MS = Object.freeze({
|
|
443
|
+
ms: 1,
|
|
444
|
+
s: 1e3,
|
|
445
|
+
min: 6e4,
|
|
446
|
+
h: 36e5,
|
|
447
|
+
d: 864e5
|
|
448
|
+
});
|
|
449
|
+
function performanceValue(stats, agg) {
|
|
450
|
+
switch (agg) {
|
|
451
|
+
case "mean":
|
|
452
|
+
return stats.mean;
|
|
453
|
+
case "min":
|
|
454
|
+
return stats.min;
|
|
455
|
+
case "max":
|
|
456
|
+
return stats.max;
|
|
457
|
+
case "sum":
|
|
458
|
+
return stats.sum;
|
|
459
|
+
case "p90":
|
|
460
|
+
return stats.p90;
|
|
461
|
+
case "trimmed_mean":
|
|
462
|
+
return stats.trimmedMean;
|
|
463
|
+
case "median":
|
|
464
|
+
default:
|
|
465
|
+
return stats.median;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/process-map/map-model.ts
|
|
470
|
+
var PROCESS_FILTER_INTENT_KINDS = ["with", "without", "startsWith", "endsWith"];
|
|
471
|
+
var PROCESS_FILTER_INTENT_LABELS = Object.freeze({
|
|
472
|
+
with: "Keep cases containing",
|
|
473
|
+
without: "Keep cases without",
|
|
474
|
+
startsWith: "Keep cases starting with",
|
|
475
|
+
endsWith: "Keep cases ending with"
|
|
476
|
+
});
|
|
477
|
+
var PROCESS_FILTER_INTENT_MESSAGE_KEYS = Object.freeze({
|
|
478
|
+
with: "process.map.filterIntentWith",
|
|
479
|
+
without: "process.map.filterIntentWithout",
|
|
480
|
+
startsWith: "process.map.filterIntentStartsWith",
|
|
481
|
+
endsWith: "process.map.filterIntentEndsWith"
|
|
482
|
+
});
|
|
483
|
+
var GHOST_OPACITY = 0.35;
|
|
484
|
+
function resolveActivityFrequencyMode(mode) {
|
|
485
|
+
switch (mode) {
|
|
486
|
+
case "absolute_case":
|
|
487
|
+
return "absolute_case";
|
|
488
|
+
case "relative":
|
|
489
|
+
return "relative";
|
|
490
|
+
case "relative_case":
|
|
491
|
+
case "relative_antecedent":
|
|
492
|
+
case "relative_consequent":
|
|
493
|
+
return "relative_case";
|
|
494
|
+
case "max_repetitions":
|
|
495
|
+
case "absolute":
|
|
496
|
+
default:
|
|
497
|
+
return "absolute";
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function resolveTransitionFrequencyMode(mode) {
|
|
501
|
+
return mode === "max_repetitions" ? "absolute" : mode;
|
|
502
|
+
}
|
|
503
|
+
var PERFORMANCE_AGGS = /* @__PURE__ */ new Set([
|
|
504
|
+
"median",
|
|
505
|
+
"mean",
|
|
506
|
+
"min",
|
|
507
|
+
"max",
|
|
508
|
+
"sum",
|
|
509
|
+
"p90",
|
|
510
|
+
"trimmed_mean"
|
|
511
|
+
]);
|
|
512
|
+
function isPerformanceMetric(metric) {
|
|
513
|
+
return PERFORMANCE_AGGS.has(metric);
|
|
514
|
+
}
|
|
515
|
+
var PERFORMANCE_AGG_LABELS = Object.freeze({
|
|
516
|
+
median: "Median duration",
|
|
517
|
+
mean: "Mean duration",
|
|
518
|
+
min: "Fastest",
|
|
519
|
+
max: "Slowest",
|
|
520
|
+
sum: "Total duration",
|
|
521
|
+
p90: "90th percentile duration",
|
|
522
|
+
trimmed_mean: "Trimmed mean duration"
|
|
523
|
+
});
|
|
524
|
+
var ACTIVITY_FREQUENCY_LABELS = Object.freeze({
|
|
525
|
+
absolute: "Occurrences",
|
|
526
|
+
absolute_case: "Cases",
|
|
527
|
+
relative: "Share of events",
|
|
528
|
+
relative_case: "Share of cases"
|
|
529
|
+
});
|
|
530
|
+
var TRANSITION_FREQUENCY_LABELS = Object.freeze({
|
|
531
|
+
absolute: "Transitions",
|
|
532
|
+
absolute_case: "Cases",
|
|
533
|
+
relative: "Share of transitions",
|
|
534
|
+
relative_case: "Share of cases",
|
|
535
|
+
relative_antecedent: "Share of source traffic",
|
|
536
|
+
relative_consequent: "Share of target traffic"
|
|
537
|
+
});
|
|
538
|
+
function nodeMetricLabel(metric) {
|
|
539
|
+
return isPerformanceMetric(metric) ? PERFORMANCE_AGG_LABELS[metric] : ACTIVITY_FREQUENCY_LABELS[resolveActivityFrequencyMode(metric)];
|
|
540
|
+
}
|
|
541
|
+
function edgeMetricLabel(metric) {
|
|
542
|
+
return isPerformanceMetric(metric) ? PERFORMANCE_AGG_LABELS[metric] : TRANSITION_FREQUENCY_LABELS[resolveTransitionFrequencyMode(metric)];
|
|
543
|
+
}
|
|
544
|
+
function isShare(metric) {
|
|
545
|
+
if (isPerformanceMetric(metric)) return false;
|
|
546
|
+
const resolved = resolveTransitionFrequencyMode(metric);
|
|
547
|
+
return resolved !== "absolute" && resolved !== "absolute_case";
|
|
548
|
+
}
|
|
549
|
+
function activityMetricValue(activity, metric, totals) {
|
|
550
|
+
if (isPerformanceMetric(metric)) return performanceValue(activity.duration, metric);
|
|
551
|
+
switch (resolveActivityFrequencyMode(metric)) {
|
|
552
|
+
case "absolute_case":
|
|
553
|
+
return activity.cases;
|
|
554
|
+
case "relative":
|
|
555
|
+
return totals.events === 0 ? 0 : activity.instances / totals.events;
|
|
556
|
+
case "relative_case":
|
|
557
|
+
return totals.cases === 0 ? 0 : activity.cases / totals.cases;
|
|
558
|
+
case "absolute":
|
|
559
|
+
default:
|
|
560
|
+
return activity.instances;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function processEdgeDenominators(transitions) {
|
|
564
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
565
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
566
|
+
let total = 0;
|
|
567
|
+
for (const t of transitions) {
|
|
568
|
+
total += t.count;
|
|
569
|
+
outgoing.set(t.source, (outgoing.get(t.source) ?? 0) + t.count);
|
|
570
|
+
incoming.set(t.target, (incoming.get(t.target) ?? 0) + t.count);
|
|
571
|
+
}
|
|
572
|
+
return { total, outgoing, incoming };
|
|
573
|
+
}
|
|
574
|
+
function ratio(numerator, denominator) {
|
|
575
|
+
return denominator === 0 ? 0 : numerator / denominator;
|
|
576
|
+
}
|
|
577
|
+
function transitionMetricValue(transition, metric, totals, denominators) {
|
|
578
|
+
if (isPerformanceMetric(metric)) return performanceValue(transition.duration, metric);
|
|
579
|
+
switch (resolveTransitionFrequencyMode(metric)) {
|
|
580
|
+
case "absolute_case":
|
|
581
|
+
return transition.caseCount;
|
|
582
|
+
case "relative":
|
|
583
|
+
return ratio(transition.count, denominators.total);
|
|
584
|
+
case "relative_case":
|
|
585
|
+
return ratio(transition.caseCount, totals.cases);
|
|
586
|
+
case "relative_antecedent":
|
|
587
|
+
return ratio(transition.count, denominators.outgoing.get(transition.source) ?? 0);
|
|
588
|
+
case "relative_consequent":
|
|
589
|
+
return ratio(transition.count, denominators.incoming.get(transition.target) ?? 0);
|
|
590
|
+
case "absolute":
|
|
591
|
+
default:
|
|
592
|
+
return transition.count;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
var DURATION_LADDER = [
|
|
596
|
+
[864e5, "d"],
|
|
597
|
+
[36e5, "h"],
|
|
598
|
+
[6e4, "min"],
|
|
599
|
+
[1e3, "s"]
|
|
600
|
+
];
|
|
601
|
+
function formatDurationMs(ms) {
|
|
602
|
+
if (!Number.isFinite(ms) || ms < 0) return "\u2014";
|
|
603
|
+
if (ms === 0) return "0 s";
|
|
604
|
+
for (const [size, unit] of DURATION_LADDER) {
|
|
605
|
+
if (ms >= size) {
|
|
606
|
+
const scaled = ms / size;
|
|
607
|
+
return `${scaled < 10 ? scaled.toFixed(1) : Math.round(scaled)} ${unit}`;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return `${Math.round(ms)} ms`;
|
|
611
|
+
}
|
|
612
|
+
function formatMetricValue(value, metric) {
|
|
613
|
+
if (isPerformanceMetric(metric)) return formatDurationMs(value);
|
|
614
|
+
if (!Number.isFinite(value)) return "\u2014";
|
|
615
|
+
if (isShare(metric)) return `${(value * 100).toFixed(1)}%`;
|
|
616
|
+
return value.toLocaleString();
|
|
617
|
+
}
|
|
618
|
+
function processEdgeId(source, target) {
|
|
619
|
+
return `${source}${EDGE_KEY_SEPARATOR}${target}`;
|
|
620
|
+
}
|
|
621
|
+
function processGraphStructureKey(graph) {
|
|
622
|
+
const activities = graph.activities.map((a) => a.id).sort();
|
|
623
|
+
const transitions = graph.transitions.map((t) => processEdgeId(t.source, t.target)).sort();
|
|
624
|
+
return `${activities.join(EDGE_KEY_SEPARATOR)}|${transitions.join(EDGE_KEY_SEPARATOR)}`;
|
|
625
|
+
}
|
|
626
|
+
function selectionNeighbourhood(graph, selection) {
|
|
627
|
+
if (!selection) return null;
|
|
628
|
+
const activities = /* @__PURE__ */ new Set();
|
|
629
|
+
const transitions = /* @__PURE__ */ new Set();
|
|
630
|
+
if (selection.kind === "activity") {
|
|
631
|
+
activities.add(selection.id);
|
|
632
|
+
for (const t of graph.transitions) {
|
|
633
|
+
if (t.source !== selection.id && t.target !== selection.id) continue;
|
|
634
|
+
transitions.add(processEdgeId(t.source, t.target));
|
|
635
|
+
activities.add(t.source);
|
|
636
|
+
activities.add(t.target);
|
|
637
|
+
}
|
|
638
|
+
return { activities, transitions };
|
|
639
|
+
}
|
|
640
|
+
for (const t of graph.transitions) {
|
|
641
|
+
if (processEdgeId(t.source, t.target) !== selection.id) continue;
|
|
642
|
+
transitions.add(selection.id);
|
|
643
|
+
activities.add(t.source);
|
|
644
|
+
activities.add(t.target);
|
|
645
|
+
}
|
|
646
|
+
return { activities, transitions };
|
|
647
|
+
}
|
|
648
|
+
function resolveSelectionState(id, kind, selection, neighbourhood, states) {
|
|
649
|
+
if (selection && selection.kind === kind && selection.id === id) return "selected";
|
|
650
|
+
const namespace = kind === "activity" ? states?.activities : states?.transitions;
|
|
651
|
+
const filterState = namespace?.[id];
|
|
652
|
+
if (filterState === "excluded") return "excluded";
|
|
653
|
+
if (selection && neighbourhood) {
|
|
654
|
+
const kept = kind === "activity" ? neighbourhood.activities : neighbourhood.transitions;
|
|
655
|
+
if (!kept.has(id)) return "excluded";
|
|
656
|
+
}
|
|
657
|
+
if (filterState !== void 0) return filterState;
|
|
658
|
+
return "associated";
|
|
659
|
+
}
|
|
660
|
+
function buildProcessMapModel({
|
|
661
|
+
graph,
|
|
662
|
+
metric,
|
|
663
|
+
rework,
|
|
664
|
+
selection,
|
|
665
|
+
selectionStates,
|
|
666
|
+
backEdgeIds
|
|
667
|
+
}) {
|
|
668
|
+
const neighbourhood = selectionNeighbourhood(graph, selection);
|
|
669
|
+
const denominators = processEdgeDenominators(graph.transitions);
|
|
670
|
+
const nodeValues = graph.activities.map((a) => activityMetricValue(a, metric.node, graph.totals));
|
|
671
|
+
const nodeDomain = minMax(nodeValues);
|
|
672
|
+
const edgeValues = graph.transitions.map(
|
|
673
|
+
(t) => transitionMetricValue(t, metric.edge, graph.totals, denominators)
|
|
674
|
+
);
|
|
675
|
+
const edgeDomain = minMax(edgeValues);
|
|
676
|
+
const resolvedNodeMetricLabel = nodeMetricLabel(metric.node);
|
|
677
|
+
const resolvedEdgeMetricLabel = edgeMetricLabel(metric.edge);
|
|
678
|
+
const nodeSpan = nodeDomain[1] - nodeDomain[0];
|
|
679
|
+
const nodes = graph.activities.map((activity, index2) => {
|
|
680
|
+
const primaryValue = nodeValues[index2];
|
|
681
|
+
const secondaryLabel = metric.secondary === void 0 ? void 0 : formatMetricValue(
|
|
682
|
+
activityMetricValue(activity, metric.secondary, graph.totals),
|
|
683
|
+
metric.secondary
|
|
684
|
+
);
|
|
685
|
+
const reworkEntry = rework?.perActivity[activity.id];
|
|
686
|
+
const reworkCount = reworkEntry === void 0 ? void 0 : reworkEntry.selfLoops + reworkEntry.loops;
|
|
687
|
+
const selectionState = resolveSelectionState(
|
|
688
|
+
activity.id,
|
|
689
|
+
"activity",
|
|
690
|
+
selection,
|
|
691
|
+
neighbourhood,
|
|
692
|
+
selectionStates
|
|
693
|
+
);
|
|
694
|
+
const data = {
|
|
695
|
+
title: activity.label || activity.id,
|
|
696
|
+
metricLabel: resolvedNodeMetricLabel,
|
|
697
|
+
primaryLabel: formatMetricValue(primaryValue, metric.node),
|
|
698
|
+
primaryValue,
|
|
699
|
+
secondaryLabel,
|
|
700
|
+
// A single-activity graph (or a flat metric) has no domain to sit in; half
|
|
701
|
+
// saturation is the honest answer — "no comparison available" — rather than a
|
|
702
|
+
// full-strength fill claiming this is the busiest node in a set of one.
|
|
703
|
+
saturation: nodeSpan === 0 ? 0.5 : (primaryValue - nodeDomain[0]) / nodeSpan,
|
|
704
|
+
isStart: activity.isStart,
|
|
705
|
+
isEnd: activity.isEnd,
|
|
706
|
+
reworkCount,
|
|
707
|
+
selectionState
|
|
708
|
+
};
|
|
709
|
+
return {
|
|
710
|
+
id: activity.id,
|
|
711
|
+
type: "process-activity",
|
|
712
|
+
position: { x: 0, y: 0 },
|
|
713
|
+
data,
|
|
714
|
+
draggable: false,
|
|
715
|
+
// React Flow reads a node's accessible name from the node OBJECT, not from the
|
|
716
|
+
// component — the same seam `withWeightedEdgeAria` exists for on the edge side
|
|
717
|
+
// (#285). Without this the node announces only its id and the metric the map
|
|
718
|
+
// exists to show reaches no assistive technology.
|
|
719
|
+
ariaLabel: activityAriaLabel(data),
|
|
720
|
+
// No `aria-disabled` here even when `selectionState === "excluded"` — an excluded
|
|
721
|
+
// node stays fully operable (clicking it is how a reader filters it back in), and
|
|
722
|
+
// `activityAriaLabel` already appends the word "excluded" to its accessible name, so
|
|
723
|
+
// assistive technology gets the state as real text rather than a lie about
|
|
724
|
+
// disablement. See `ProcessSelectionState`'s own doc comment.
|
|
725
|
+
domAttributes: {
|
|
726
|
+
"data-selection": selectionState,
|
|
727
|
+
"data-activity": activity.id
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
});
|
|
731
|
+
const edges = graph.transitions.map((transition, index2) => {
|
|
732
|
+
const value = edgeValues[index2];
|
|
733
|
+
const id = processEdgeId(transition.source, transition.target);
|
|
734
|
+
const secondaryLabel = metric.secondary === void 0 ? void 0 : formatMetricValue(
|
|
735
|
+
transitionMetricValue(transition, metric.secondary, graph.totals, denominators),
|
|
736
|
+
metric.secondary
|
|
737
|
+
);
|
|
738
|
+
const selectionState = resolveSelectionState(
|
|
739
|
+
id,
|
|
740
|
+
"transition",
|
|
741
|
+
selection,
|
|
742
|
+
neighbourhood,
|
|
743
|
+
selectionStates
|
|
744
|
+
);
|
|
745
|
+
const data = {
|
|
746
|
+
source: transition.source,
|
|
747
|
+
target: transition.target,
|
|
748
|
+
weight: value,
|
|
749
|
+
value,
|
|
750
|
+
valueDomain: edgeDomain,
|
|
751
|
+
label: formatMetricValue(value, metric.edge),
|
|
752
|
+
secondaryLabel,
|
|
753
|
+
isSelfLoop: transition.isSelfLoop,
|
|
754
|
+
isBackEdge: backEdgeIds?.has(id) ?? transition.isBackEdge,
|
|
755
|
+
selectionState
|
|
756
|
+
};
|
|
757
|
+
return {
|
|
758
|
+
id,
|
|
759
|
+
source: transition.source,
|
|
760
|
+
target: transition.target,
|
|
761
|
+
type: "process-transition",
|
|
762
|
+
data,
|
|
763
|
+
ariaLabel: transitionAriaLabel(data, resolvedEdgeMetricLabel)
|
|
764
|
+
};
|
|
765
|
+
});
|
|
766
|
+
const activityRows = nodes.map((node) => ({
|
|
767
|
+
id: node.id,
|
|
768
|
+
title: node.data.title,
|
|
769
|
+
primaryLabel: node.data.primaryLabel,
|
|
770
|
+
secondaryLabel: node.data.secondaryLabel,
|
|
771
|
+
reworkCount: node.data.reworkCount,
|
|
772
|
+
role: activityRole(node.data),
|
|
773
|
+
selectionState: node.data.selectionState
|
|
774
|
+
}));
|
|
775
|
+
const transitionRows = edges.map((edge) => {
|
|
776
|
+
const data = edge.data;
|
|
777
|
+
return {
|
|
778
|
+
id: edge.id,
|
|
779
|
+
source: data.source,
|
|
780
|
+
target: data.target,
|
|
781
|
+
primaryLabel: data.label,
|
|
782
|
+
secondaryLabel: data.secondaryLabel,
|
|
783
|
+
shape: transitionShape(data),
|
|
784
|
+
selectionState: data.selectionState
|
|
785
|
+
};
|
|
786
|
+
});
|
|
787
|
+
const excludedCounts = {
|
|
788
|
+
activities: nodes.reduce((n, node) => n + (node.data.selectionState === "excluded" ? 1 : 0), 0),
|
|
789
|
+
totalActivities: nodes.length,
|
|
790
|
+
transitions: edges.reduce(
|
|
791
|
+
(n, edge) => n + (edge.data.selectionState === "excluded" ? 1 : 0),
|
|
792
|
+
0
|
|
793
|
+
),
|
|
794
|
+
totalTransitions: edges.length
|
|
795
|
+
};
|
|
796
|
+
return {
|
|
797
|
+
nodes,
|
|
798
|
+
edges,
|
|
799
|
+
nodeDomain,
|
|
800
|
+
edgeDomain,
|
|
801
|
+
nodeMetricLabel: resolvedNodeMetricLabel,
|
|
802
|
+
edgeMetricLabel: resolvedEdgeMetricLabel,
|
|
803
|
+
activityRows,
|
|
804
|
+
transitionRows,
|
|
805
|
+
formatEdgeValue: (value) => formatMetricValue(value, metric.edge),
|
|
806
|
+
excludedCounts
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function activityRole(data) {
|
|
810
|
+
if (data.isStart && data.isEnd) return "Start and end";
|
|
811
|
+
if (data.isStart) return "Start";
|
|
812
|
+
if (data.isEnd) return "End";
|
|
813
|
+
return "Step";
|
|
814
|
+
}
|
|
815
|
+
function transitionShape(data) {
|
|
816
|
+
if (data.isSelfLoop) return "Self-loop";
|
|
817
|
+
if (data.isBackEdge) return "Back edge";
|
|
818
|
+
return "Forward";
|
|
819
|
+
}
|
|
820
|
+
function selectionStateLabel(state) {
|
|
821
|
+
if (state === "selected") return "Selected";
|
|
822
|
+
if (state === "excluded") return "Excluded";
|
|
823
|
+
return "";
|
|
824
|
+
}
|
|
825
|
+
var PROCESS_SELECTION_STATE_MESSAGE_KEYS = Object.freeze({
|
|
826
|
+
selected: "process.map.stateSelected",
|
|
827
|
+
excluded: "process.map.stateExcluded"
|
|
828
|
+
});
|
|
829
|
+
function activityAriaLabel(data) {
|
|
830
|
+
const parts = [`${data.title} \u2014 ${activityRole(data).toLowerCase()}`];
|
|
831
|
+
parts.push(`${data.metricLabel} ${data.primaryLabel}`);
|
|
832
|
+
if (data.secondaryLabel) parts.push(data.secondaryLabel);
|
|
833
|
+
if (data.reworkCount) parts.push(`${data.reworkCount} repeated executions`);
|
|
834
|
+
if (data.selectionState !== "associated") parts.push(data.selectionState);
|
|
835
|
+
return parts.join(", ");
|
|
836
|
+
}
|
|
837
|
+
function transitionAriaLabel(data, metricLabel) {
|
|
838
|
+
const parts = [
|
|
839
|
+
data.isSelfLoop ? `Self-loop on ${data.source}` : `${data.isBackEdge ? "Back edge" : "Transition"} from ${data.source} to ${data.target}`,
|
|
840
|
+
`${metricLabel} ${data.label}`
|
|
841
|
+
];
|
|
842
|
+
if (data.secondaryLabel) parts.push(data.secondaryLabel);
|
|
843
|
+
if (data.selectionState !== "associated") parts.push(data.selectionState);
|
|
844
|
+
return parts.join(", ");
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/process-map/process-map.tsx
|
|
848
|
+
import {
|
|
849
|
+
useCallback as useCallback2,
|
|
850
|
+
useEffect as useEffect2,
|
|
851
|
+
useMemo as useMemo4,
|
|
852
|
+
useRef as useRef2,
|
|
853
|
+
useState as useState2
|
|
854
|
+
} from "react";
|
|
855
|
+
import { Filter } from "lucide-react";
|
|
856
|
+
import {
|
|
857
|
+
Button,
|
|
858
|
+
DropdownMenu,
|
|
859
|
+
DropdownMenuContent,
|
|
860
|
+
DropdownMenuItem,
|
|
861
|
+
DropdownMenuLabel,
|
|
862
|
+
DropdownMenuSeparator,
|
|
863
|
+
DropdownMenuTrigger,
|
|
864
|
+
StatePanel,
|
|
865
|
+
Table,
|
|
866
|
+
TableBody,
|
|
867
|
+
TableCaption,
|
|
868
|
+
TableCell,
|
|
869
|
+
TableHead,
|
|
870
|
+
TableHeader,
|
|
871
|
+
TableRow,
|
|
872
|
+
useLocale
|
|
873
|
+
} from "@elabs-ai/components-ui";
|
|
874
|
+
import { cn as cn2 } from "@elabs-ai/components-ui/lib/cn";
|
|
875
|
+
import {
|
|
876
|
+
CanvasShell,
|
|
877
|
+
FlowMiniMap,
|
|
878
|
+
Legend,
|
|
879
|
+
ZoomControls
|
|
880
|
+
} from "@elabs-ai/components-flow";
|
|
881
|
+
|
|
882
|
+
// src/core/abstract-graph.ts
|
|
883
|
+
function edgeKey(source, target) {
|
|
884
|
+
return `${source}${EDGE_KEY_SEPARATOR}${target}`;
|
|
885
|
+
}
|
|
886
|
+
function compareStrings2(a, b) {
|
|
887
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
888
|
+
}
|
|
889
|
+
function clampFraction(value) {
|
|
890
|
+
if (!Number.isFinite(value)) return 1;
|
|
891
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
892
|
+
}
|
|
893
|
+
function countToKeep(total, fraction, atLeast) {
|
|
894
|
+
if (total === 0) return 0;
|
|
895
|
+
const kept = Math.round(total * fraction);
|
|
896
|
+
return kept < atLeast ? Math.min(atLeast, total) : kept;
|
|
897
|
+
}
|
|
898
|
+
var MinHeap = class {
|
|
899
|
+
nodes = [];
|
|
900
|
+
costs = [];
|
|
901
|
+
get size() {
|
|
902
|
+
return this.nodes.length;
|
|
903
|
+
}
|
|
904
|
+
push(node, cost) {
|
|
905
|
+
this.nodes.push(node);
|
|
906
|
+
this.costs.push(cost);
|
|
907
|
+
let i = this.nodes.length - 1;
|
|
908
|
+
while (i > 0) {
|
|
909
|
+
const parent = i - 1 >> 1;
|
|
910
|
+
if (this.costs[parent] <= this.costs[i]) break;
|
|
911
|
+
this.swap(i, parent);
|
|
912
|
+
i = parent;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
pop() {
|
|
916
|
+
if (this.nodes.length === 0) return void 0;
|
|
917
|
+
const node = this.nodes[0];
|
|
918
|
+
const cost = this.costs[0];
|
|
919
|
+
const lastNode = this.nodes.pop();
|
|
920
|
+
const lastCost = this.costs.pop();
|
|
921
|
+
if (this.nodes.length > 0) {
|
|
922
|
+
this.nodes[0] = lastNode;
|
|
923
|
+
this.costs[0] = lastCost;
|
|
924
|
+
let i = 0;
|
|
925
|
+
for (; ; ) {
|
|
926
|
+
const left = i * 2 + 1;
|
|
927
|
+
const right = left + 1;
|
|
928
|
+
let smallest = i;
|
|
929
|
+
const size = this.nodes.length;
|
|
930
|
+
if (left < size && this.costs[left] < this.costs[smallest]) {
|
|
931
|
+
smallest = left;
|
|
932
|
+
}
|
|
933
|
+
if (right < size && this.costs[right] < this.costs[smallest]) {
|
|
934
|
+
smallest = right;
|
|
935
|
+
}
|
|
936
|
+
if (smallest === i) break;
|
|
937
|
+
this.swap(i, smallest);
|
|
938
|
+
i = smallest;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return { node, cost };
|
|
942
|
+
}
|
|
943
|
+
swap(a, b) {
|
|
944
|
+
const node = this.nodes[a];
|
|
945
|
+
this.nodes[a] = this.nodes[b];
|
|
946
|
+
this.nodes[b] = node;
|
|
947
|
+
const cost = this.costs[a];
|
|
948
|
+
this.costs[a] = this.costs[b];
|
|
949
|
+
this.costs[b] = cost;
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
function abstractGraph(graph, opts) {
|
|
953
|
+
const keepConnected = opts.keepConnected ?? true;
|
|
954
|
+
const invert = opts.invert ?? false;
|
|
955
|
+
const activityFraction = clampFraction(opts.activities);
|
|
956
|
+
const pathFraction = clampFraction(opts.paths);
|
|
957
|
+
const rankedActivities = [...graph.activities].sort(
|
|
958
|
+
(a, b) => b.cases - a.cases || b.instances - a.instances || compareStrings2(a.id, b.id)
|
|
959
|
+
);
|
|
960
|
+
const activityKeepCount = countToKeep(rankedActivities.length, activityFraction, 1);
|
|
961
|
+
const keptActivities = new Set(
|
|
962
|
+
(invert ? rankedActivities.slice(rankedActivities.length - activityKeepCount) : rankedActivities.slice(0, activityKeepCount)).map((activity) => activity.id)
|
|
963
|
+
);
|
|
964
|
+
const candidateEdges = graph.transitions.filter(
|
|
965
|
+
(edge) => keptActivities.has(edge.source) && keptActivities.has(edge.target)
|
|
966
|
+
);
|
|
967
|
+
const rankedEdges = [...candidateEdges].sort(
|
|
968
|
+
(a, b) => b.count - a.count || b.caseCount - a.caseCount || compareStrings2(a.source, b.source) || compareStrings2(a.target, b.target)
|
|
969
|
+
);
|
|
970
|
+
const edgeKeepCount = countToKeep(rankedEdges.length, pathFraction, 0);
|
|
971
|
+
const keptEdges = new Set(
|
|
972
|
+
(invert ? rankedEdges.slice(rankedEdges.length - edgeKeepCount) : rankedEdges.slice(0, edgeKeepCount)).map((edge) => edgeKey(edge.source, edge.target))
|
|
973
|
+
);
|
|
974
|
+
if (keepConnected && keptActivities.size > 0) {
|
|
975
|
+
reconnect(graph, keptActivities, keptEdges);
|
|
976
|
+
}
|
|
977
|
+
const activities = graph.activities.filter(
|
|
978
|
+
(activity) => keptActivities.has(activity.id)
|
|
979
|
+
);
|
|
980
|
+
const transitions = graph.transitions.filter(
|
|
981
|
+
(edge) => keptEdges.has(edgeKey(edge.source, edge.target))
|
|
982
|
+
);
|
|
983
|
+
return {
|
|
984
|
+
activities,
|
|
985
|
+
transitions,
|
|
986
|
+
startActivities: pick(graph.startActivities, keptActivities),
|
|
987
|
+
endActivities: pick(graph.endActivities, keptActivities),
|
|
988
|
+
// Passed through by reference: totals describe the LOG, and abstraction is a view.
|
|
989
|
+
totals: graph.totals,
|
|
990
|
+
hidden: {
|
|
991
|
+
activities: graph.activities.length - activities.length,
|
|
992
|
+
paths: graph.transitions.length - transitions.length
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
function pick(record, keep) {
|
|
997
|
+
const out = {};
|
|
998
|
+
for (const name of Object.keys(record)) {
|
|
999
|
+
if (keep.has(name)) out[name] = record[name];
|
|
1000
|
+
}
|
|
1001
|
+
return out;
|
|
1002
|
+
}
|
|
1003
|
+
function reconnect(graph, keptActivities, keptEdges) {
|
|
1004
|
+
const forward = /* @__PURE__ */ new Map();
|
|
1005
|
+
const backward = /* @__PURE__ */ new Map();
|
|
1006
|
+
let maxCount = 1;
|
|
1007
|
+
for (const edge of graph.transitions) {
|
|
1008
|
+
if (edge.count > maxCount) maxCount = edge.count;
|
|
1009
|
+
index(forward, edge.source, edge);
|
|
1010
|
+
index(backward, edge.target, edge);
|
|
1011
|
+
}
|
|
1012
|
+
const logMax = Math.log(maxCount);
|
|
1013
|
+
const cost = (count) => count > 0 ? logMax - Math.log(count) : logMax;
|
|
1014
|
+
const bound = graph.activities.length + graph.transitions.length + 2;
|
|
1015
|
+
for (let round = 0; round < bound; round += 1) {
|
|
1016
|
+
const before = keptActivities.size + keptEdges.size;
|
|
1017
|
+
repairDirection(graph.startActivities, forward, true);
|
|
1018
|
+
repairDirection(graph.endActivities, backward, false);
|
|
1019
|
+
if (keptActivities.size + keptEdges.size === before) break;
|
|
1020
|
+
}
|
|
1021
|
+
function repairDirection(seedCounts, adjacency, downstream) {
|
|
1022
|
+
const allSeeds = Object.keys(seedCounts);
|
|
1023
|
+
if (allSeeds.length === 0) return;
|
|
1024
|
+
if (!allSeeds.some((id) => keptActivities.has(id))) anchor(seedCounts, keptActivities);
|
|
1025
|
+
const keptSeeds = allSeeds.filter((id) => keptActivities.has(id));
|
|
1026
|
+
if (keptSeeds.length === 0) return;
|
|
1027
|
+
const reached = spread(keptSeeds, adjacency, downstream);
|
|
1028
|
+
for (const activity of graph.activities) {
|
|
1029
|
+
if (!keptActivities.has(activity.id) || reached.has(activity.id)) continue;
|
|
1030
|
+
const route = shortestRoute(keptSeeds, activity.id, adjacency, downstream, true) ?? shortestRoute(keptSeeds, activity.id, adjacency, downstream, false) ?? shortestRoute(allSeeds, activity.id, adjacency, downstream, false);
|
|
1031
|
+
if (route === void 0) continue;
|
|
1032
|
+
for (const id of route.activities) keptActivities.add(id);
|
|
1033
|
+
for (const key of route.edges) keptEdges.add(key);
|
|
1034
|
+
for (const id of spread([...route.activities], adjacency, downstream)) reached.add(id);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function spread(seeds, adjacency, downstream) {
|
|
1038
|
+
const seen = new Set(seeds.filter((id) => keptActivities.has(id)));
|
|
1039
|
+
const queue = [...seen];
|
|
1040
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
1041
|
+
const node = queue[head];
|
|
1042
|
+
for (const edge of adjacency.get(node) ?? []) {
|
|
1043
|
+
const next = downstream ? edge.target : edge.source;
|
|
1044
|
+
if (!keptActivities.has(next)) continue;
|
|
1045
|
+
if (!keptEdges.has(edgeKey(edge.source, edge.target))) continue;
|
|
1046
|
+
if (seen.has(next)) continue;
|
|
1047
|
+
seen.add(next);
|
|
1048
|
+
queue.push(next);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return seen;
|
|
1052
|
+
}
|
|
1053
|
+
function shortestRoute(seeds, target, adjacency, downstream, keptOnly) {
|
|
1054
|
+
const distance = /* @__PURE__ */ new Map();
|
|
1055
|
+
const previous = /* @__PURE__ */ new Map();
|
|
1056
|
+
const settled = /* @__PURE__ */ new Set();
|
|
1057
|
+
const heap = new MinHeap();
|
|
1058
|
+
for (const seed of seeds) {
|
|
1059
|
+
distance.set(seed, 0);
|
|
1060
|
+
heap.push(seed, 0);
|
|
1061
|
+
}
|
|
1062
|
+
while (heap.size > 0) {
|
|
1063
|
+
const top = heap.pop();
|
|
1064
|
+
if (settled.has(top.node)) continue;
|
|
1065
|
+
settled.add(top.node);
|
|
1066
|
+
if (top.node === target) break;
|
|
1067
|
+
for (const edge of adjacency.get(top.node) ?? []) {
|
|
1068
|
+
const next = downstream ? edge.target : edge.source;
|
|
1069
|
+
if (next === top.node) continue;
|
|
1070
|
+
if (keptOnly && !keptActivities.has(next)) continue;
|
|
1071
|
+
const candidate = top.cost + cost(edge.count);
|
|
1072
|
+
const known = distance.get(next);
|
|
1073
|
+
if (known !== void 0 && known <= candidate) continue;
|
|
1074
|
+
distance.set(next, candidate);
|
|
1075
|
+
previous.set(next, edge);
|
|
1076
|
+
heap.push(next, candidate);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
if (!settled.has(target)) return void 0;
|
|
1080
|
+
const route = { activities: /* @__PURE__ */ new Set([target]), edges: /* @__PURE__ */ new Set() };
|
|
1081
|
+
let cursor = target;
|
|
1082
|
+
for (; ; ) {
|
|
1083
|
+
const edge = previous.get(cursor);
|
|
1084
|
+
if (edge === void 0) break;
|
|
1085
|
+
route.edges.add(edgeKey(edge.source, edge.target));
|
|
1086
|
+
route.activities.add(edge.source);
|
|
1087
|
+
route.activities.add(edge.target);
|
|
1088
|
+
cursor = downstream ? edge.source : edge.target;
|
|
1089
|
+
}
|
|
1090
|
+
return route;
|
|
1091
|
+
}
|
|
1092
|
+
function anchor(counts, kept) {
|
|
1093
|
+
const names = Object.keys(counts);
|
|
1094
|
+
if (names.length === 0) return;
|
|
1095
|
+
if (names.some((name) => kept.has(name))) return;
|
|
1096
|
+
let best = names[0];
|
|
1097
|
+
for (const name of names) {
|
|
1098
|
+
const value = counts[name];
|
|
1099
|
+
const bestValue = counts[best];
|
|
1100
|
+
if (value > bestValue || value === bestValue && compareStrings2(name, best) < 0) best = name;
|
|
1101
|
+
}
|
|
1102
|
+
kept.add(best);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function index(map, key, edge) {
|
|
1106
|
+
const bucket = map.get(key);
|
|
1107
|
+
if (bucket === void 0) map.set(key, [edge]);
|
|
1108
|
+
else bucket.push(edge);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// src/core/detect-rework.ts
|
|
1112
|
+
function detectRework(log) {
|
|
1113
|
+
const normalized = asNormalizedLog(log);
|
|
1114
|
+
const tallies = /* @__PURE__ */ new Map();
|
|
1115
|
+
let selfLoops = 0;
|
|
1116
|
+
let loops = 0;
|
|
1117
|
+
let casesWithRework = 0;
|
|
1118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1119
|
+
for (const kase of normalized.cases) {
|
|
1120
|
+
seen.clear();
|
|
1121
|
+
let caseHasRework = false;
|
|
1122
|
+
let previous;
|
|
1123
|
+
for (const event of kase.events) {
|
|
1124
|
+
const name = event.activity;
|
|
1125
|
+
let tally = tallies.get(name);
|
|
1126
|
+
if (tally === void 0) {
|
|
1127
|
+
tally = { selfLoops: 0, loops: 0 };
|
|
1128
|
+
tallies.set(name, tally);
|
|
1129
|
+
}
|
|
1130
|
+
if (seen.has(name)) {
|
|
1131
|
+
caseHasRework = true;
|
|
1132
|
+
if (name === previous) {
|
|
1133
|
+
tally.selfLoops += 1;
|
|
1134
|
+
selfLoops += 1;
|
|
1135
|
+
} else {
|
|
1136
|
+
tally.loops += 1;
|
|
1137
|
+
loops += 1;
|
|
1138
|
+
}
|
|
1139
|
+
} else {
|
|
1140
|
+
seen.add(name);
|
|
1141
|
+
}
|
|
1142
|
+
previous = name;
|
|
1143
|
+
}
|
|
1144
|
+
if (caseHasRework) casesWithRework += 1;
|
|
1145
|
+
}
|
|
1146
|
+
const perActivity = {};
|
|
1147
|
+
for (const name of [...tallies.keys()].sort()) {
|
|
1148
|
+
perActivity[name] = tallies.get(name);
|
|
1149
|
+
}
|
|
1150
|
+
return {
|
|
1151
|
+
selfLoops,
|
|
1152
|
+
loops,
|
|
1153
|
+
caseReworkRate: normalized.cases.length === 0 ? 0 : casesWithRework / normalized.cases.length,
|
|
1154
|
+
perActivity
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/process-map/process-activity-node.tsx
|
|
1159
|
+
import { useMemo } from "react";
|
|
1160
|
+
import { CircleDot, Flag, Play, RefreshCw } from "lucide-react";
|
|
1161
|
+
import { Badge } from "@elabs-ai/components-ui";
|
|
1162
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
1163
|
+
import { FlowNode } from "@elabs-ai/components-flow";
|
|
1164
|
+
|
|
1165
|
+
// src/process-map/process-map-context.ts
|
|
1166
|
+
import { createContext, use } from "react";
|
|
1167
|
+
var EMPTY_PROCESS_MAP_HOVER = Object.freeze({
|
|
1168
|
+
activityId: null,
|
|
1169
|
+
incidentEdgeIds: /* @__PURE__ */ new Set()
|
|
1170
|
+
});
|
|
1171
|
+
var ProcessMapHoverContext = createContext(EMPTY_PROCESS_MAP_HOVER);
|
|
1172
|
+
function useProcessMapHover() {
|
|
1173
|
+
return use(ProcessMapHoverContext);
|
|
1174
|
+
}
|
|
1175
|
+
var NOOP_EDGE_KEY_HANDLER = () => {
|
|
1176
|
+
};
|
|
1177
|
+
var ProcessMapEdgeKeyContext = createContext(NOOP_EDGE_KEY_HANDLER);
|
|
1178
|
+
function useProcessMapEdgeKeys() {
|
|
1179
|
+
return use(ProcessMapEdgeKeyContext);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// src/process-map/process-activity-node.tsx
|
|
1183
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1184
|
+
var GHOST_FRAME_STYLE = {
|
|
1185
|
+
"--border": "var(--border-strong)",
|
|
1186
|
+
"--flow-node": "color-mix(in oklab, var(--card) 60%, var(--surface-muted) 40%)"
|
|
1187
|
+
};
|
|
1188
|
+
function meterFill(saturation) {
|
|
1189
|
+
const percent = Math.round(Math.min(1, Math.max(0, saturation)) * 100);
|
|
1190
|
+
return `color-mix(in oklab, var(--primary) ${percent}%, var(--surface-muted))`;
|
|
1191
|
+
}
|
|
1192
|
+
function roleIcon(isStart, isEnd) {
|
|
1193
|
+
if (isStart && isEnd) return CircleDot;
|
|
1194
|
+
if (isStart) return Play;
|
|
1195
|
+
if (isEnd) return Flag;
|
|
1196
|
+
return void 0;
|
|
1197
|
+
}
|
|
1198
|
+
function ProcessActivityNode(props) {
|
|
1199
|
+
const { data } = props;
|
|
1200
|
+
const hover = useProcessMapHover();
|
|
1201
|
+
const RoleIcon = roleIcon(data.isStart, data.isEnd);
|
|
1202
|
+
const isHovered = hover.activityId === props.id;
|
|
1203
|
+
const isDimmed = data.selectionState === "excluded";
|
|
1204
|
+
const percent = Math.round(Math.min(1, Math.max(0, data.saturation)) * 100);
|
|
1205
|
+
const flowData = useMemo(
|
|
1206
|
+
() => ({
|
|
1207
|
+
title: data.title,
|
|
1208
|
+
kind: data.metricLabel,
|
|
1209
|
+
subtitle: data.secondaryLabel ? `${data.primaryLabel} \xB7 ${data.secondaryLabel}` : data.primaryLabel,
|
|
1210
|
+
icon: RoleIcon ? /* @__PURE__ */ jsx(RoleIcon, { "aria-hidden": "true" }) : void 0,
|
|
1211
|
+
// `tone` is a COLOUR axis in FlowNode. The process map never uses it to carry a
|
|
1212
|
+
// metric — the fill would then be the only channel — so it stays default and the
|
|
1213
|
+
// role/rework signals are carried by the glyph, the badge and the accessible name.
|
|
1214
|
+
tone: "default",
|
|
1215
|
+
// The metric's second, colour-free channel: bar LENGTH. `aria-hidden` because the
|
|
1216
|
+
// same number is already printed in the subtitle above and repeated in the node's
|
|
1217
|
+
// accessible name — a third announcement would be noise, not access. The fill (not
|
|
1218
|
+
// the text) is the one thing here that still dims at the shared ghost rung.
|
|
1219
|
+
footer: /* @__PURE__ */ jsx(
|
|
1220
|
+
"div",
|
|
1221
|
+
{
|
|
1222
|
+
"aria-hidden": "true",
|
|
1223
|
+
"data-slot": "process-activity-node-meter",
|
|
1224
|
+
"data-percent": percent,
|
|
1225
|
+
className: "h-1.5 w-full overflow-hidden rounded-full bg-surface-muted transition-opacity duration-fast ease-standard motion-reduce:transition-none",
|
|
1226
|
+
style: isDimmed ? { opacity: GHOST_OPACITY } : void 0,
|
|
1227
|
+
children: /* @__PURE__ */ jsx(
|
|
1228
|
+
"div",
|
|
1229
|
+
{
|
|
1230
|
+
className: "h-full rounded-full transition-[width] duration-base ease-standard motion-reduce:transition-none",
|
|
1231
|
+
style: { width: `${percent}%`, background: meterFill(data.saturation) }
|
|
1232
|
+
}
|
|
1233
|
+
)
|
|
1234
|
+
}
|
|
1235
|
+
)
|
|
1236
|
+
}),
|
|
1237
|
+
[
|
|
1238
|
+
data.title,
|
|
1239
|
+
data.metricLabel,
|
|
1240
|
+
data.primaryLabel,
|
|
1241
|
+
data.secondaryLabel,
|
|
1242
|
+
data.saturation,
|
|
1243
|
+
RoleIcon,
|
|
1244
|
+
percent,
|
|
1245
|
+
isDimmed
|
|
1246
|
+
]
|
|
1247
|
+
);
|
|
1248
|
+
return /* @__PURE__ */ jsxs(
|
|
1249
|
+
"div",
|
|
1250
|
+
{
|
|
1251
|
+
"data-slot": "process-activity-node",
|
|
1252
|
+
"data-selection": data.selectionState,
|
|
1253
|
+
"data-role": activityRole(data).toLowerCase(),
|
|
1254
|
+
"data-hover": isHovered ? "true" : void 0,
|
|
1255
|
+
className: cn("relative", isHovered && "z-10"),
|
|
1256
|
+
children: [
|
|
1257
|
+
data.reworkCount ? /* @__PURE__ */ jsxs(
|
|
1258
|
+
Badge,
|
|
1259
|
+
{
|
|
1260
|
+
variant: "warning",
|
|
1261
|
+
"data-slot": "process-activity-node-rework",
|
|
1262
|
+
className: "absolute -end-2 -top-2 z-10 gap-1 px-1.5 py-0 text-meta tabular-nums",
|
|
1263
|
+
children: [
|
|
1264
|
+
/* @__PURE__ */ jsx(RefreshCw, { "aria-hidden": "true", className: "size-3" }),
|
|
1265
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: data.reworkCount }),
|
|
1266
|
+
/* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
|
|
1267
|
+
data.reworkCount,
|
|
1268
|
+
" repeated executions"
|
|
1269
|
+
] })
|
|
1270
|
+
]
|
|
1271
|
+
}
|
|
1272
|
+
) : null,
|
|
1273
|
+
/* @__PURE__ */ jsx("div", { "data-slot": "process-activity-node-frame", style: isDimmed ? GHOST_FRAME_STYLE : void 0, children: /* @__PURE__ */ jsx(FlowNode, { ...props, type: "brand", data: flowData }) })
|
|
1274
|
+
]
|
|
1275
|
+
}
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/process-map/process-transition-edge.tsx
|
|
1280
|
+
import { useMemo as useMemo2 } from "react";
|
|
1281
|
+
import {
|
|
1282
|
+
FlowSelfLoopEdge,
|
|
1283
|
+
FlowWeightedEdge
|
|
1284
|
+
} from "@elabs-ai/components-flow";
|
|
1285
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
1286
|
+
var PROCESS_MAP_EDGE_SCALE_GROUP = "process-map";
|
|
1287
|
+
var RESTING_OPACITY = 1;
|
|
1288
|
+
var UNRELATED_OPACITY = 0.25;
|
|
1289
|
+
var EXCLUDED_LABEL_PROPS = { className: "border-dashed", "data-selection": "excluded" };
|
|
1290
|
+
function ProcessTransitionEdge(props) {
|
|
1291
|
+
const { data } = props;
|
|
1292
|
+
const hover = useProcessMapHover();
|
|
1293
|
+
const onEdgeKey = useProcessMapEdgeKeys();
|
|
1294
|
+
const isExcluded = data?.selectionState === "excluded";
|
|
1295
|
+
const labelProps = isExcluded ? EXCLUDED_LABEL_PROPS : void 0;
|
|
1296
|
+
const weightedData = useMemo2(
|
|
1297
|
+
() => ({
|
|
1298
|
+
weight: data?.weight,
|
|
1299
|
+
scaleGroup: PROCESS_MAP_EDGE_SCALE_GROUP,
|
|
1300
|
+
value: data?.value,
|
|
1301
|
+
valueDomain: data?.valueDomain,
|
|
1302
|
+
label: data?.label,
|
|
1303
|
+
secondaryLabel: data?.secondaryLabel,
|
|
1304
|
+
variant: data?.isBackEdge ? "back" : "forward",
|
|
1305
|
+
labelProps
|
|
1306
|
+
}),
|
|
1307
|
+
[
|
|
1308
|
+
data?.weight,
|
|
1309
|
+
data?.value,
|
|
1310
|
+
data?.valueDomain,
|
|
1311
|
+
data?.label,
|
|
1312
|
+
data?.secondaryLabel,
|
|
1313
|
+
data?.isBackEdge,
|
|
1314
|
+
labelProps
|
|
1315
|
+
]
|
|
1316
|
+
);
|
|
1317
|
+
const selfLoopData = useMemo2(
|
|
1318
|
+
() => ({
|
|
1319
|
+
weight: data?.weight,
|
|
1320
|
+
scaleGroup: PROCESS_MAP_EDGE_SCALE_GROUP,
|
|
1321
|
+
label: data?.label,
|
|
1322
|
+
secondaryLabel: data?.secondaryLabel,
|
|
1323
|
+
labelProps
|
|
1324
|
+
}),
|
|
1325
|
+
[data?.weight, data?.label, data?.secondaryLabel, labelProps]
|
|
1326
|
+
);
|
|
1327
|
+
const opacity = isExcluded ? GHOST_OPACITY : hover.activityId !== null && !hover.incidentEdgeIds.has(props.id) ? UNRELATED_OPACITY : RESTING_OPACITY;
|
|
1328
|
+
return /* @__PURE__ */ jsx2(
|
|
1329
|
+
"g",
|
|
1330
|
+
{
|
|
1331
|
+
"data-slot": "process-transition-edge",
|
|
1332
|
+
"data-shape": data?.isSelfLoop ? "self-loop" : data?.isBackEdge ? "back" : "forward",
|
|
1333
|
+
"data-selection": data?.selectionState,
|
|
1334
|
+
"data-incident": hover.incidentEdgeIds.has(props.id) ? "true" : void 0,
|
|
1335
|
+
className: "transition-opacity duration-fast ease-standard motion-reduce:transition-none",
|
|
1336
|
+
style: { opacity },
|
|
1337
|
+
onKeyDown: (event) => onEdgeKey(props.id, event),
|
|
1338
|
+
children: data?.isSelfLoop ? /* @__PURE__ */ jsx2(FlowSelfLoopEdge, { ...props, type: "self-loop", data: selfLoopData }) : /* @__PURE__ */ jsx2(FlowWeightedEdge, { ...props, type: "weighted", data: weightedData })
|
|
1339
|
+
}
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// src/process-map/use-process-layout.ts
|
|
1344
|
+
import { useCallback, useEffect, useMemo as useMemo3, useRef, useState } from "react";
|
|
1345
|
+
import { layoutFlow } from "@elabs-ai/components-flow";
|
|
1346
|
+
var NODE_SPACING = 72;
|
|
1347
|
+
var RANK_SPACING = { TB: 72, BT: 72, LR: 120, RL: 120 };
|
|
1348
|
+
var DEFAULT_LAYOUT_DEBOUNCE_MS = 80;
|
|
1349
|
+
var PROCESS_MAP_NODE_MOTION_CLASS = "[&_div[data-id]:not([data-handlepos])]:transition-transform [&_div[data-id]:not([data-handlepos])]:duration-base [&_div[data-id]:not([data-handlepos])]:ease-standard motion-reduce:[&_div[data-id]:not([data-handlepos])]:transition-none";
|
|
1350
|
+
var EMPTY_SET = /* @__PURE__ */ new Set();
|
|
1351
|
+
function toSnapshot(result, durationMs) {
|
|
1352
|
+
const positions = {};
|
|
1353
|
+
const sourcePosition = {};
|
|
1354
|
+
const targetPosition = {};
|
|
1355
|
+
for (const node of result.nodes) {
|
|
1356
|
+
positions[node.id] = node.position;
|
|
1357
|
+
sourcePosition[node.id] = node.sourcePosition;
|
|
1358
|
+
targetPosition[node.id] = node.targetPosition;
|
|
1359
|
+
}
|
|
1360
|
+
return {
|
|
1361
|
+
positions,
|
|
1362
|
+
sourcePosition,
|
|
1363
|
+
targetPosition,
|
|
1364
|
+
backEdges: result.backEdges,
|
|
1365
|
+
selfLoops: result.selfLoops,
|
|
1366
|
+
durationMs
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
function applyLayoutSnapshot(nodes, snapshot) {
|
|
1370
|
+
return nodes.map((node) => {
|
|
1371
|
+
const position = snapshot.positions[node.id];
|
|
1372
|
+
if (!position) return node;
|
|
1373
|
+
return {
|
|
1374
|
+
...node,
|
|
1375
|
+
position,
|
|
1376
|
+
sourcePosition: snapshot.sourcePosition[node.id],
|
|
1377
|
+
targetPosition: snapshot.targetPosition[node.id]
|
|
1378
|
+
};
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
function useProcessLayout({
|
|
1382
|
+
nodes,
|
|
1383
|
+
edges,
|
|
1384
|
+
structureKey,
|
|
1385
|
+
direction,
|
|
1386
|
+
debounceMs = DEFAULT_LAYOUT_DEBOUNCE_MS
|
|
1387
|
+
}) {
|
|
1388
|
+
const cacheKey = `${structureKey}::${direction}`;
|
|
1389
|
+
const cache = useRef(/* @__PURE__ */ new Map());
|
|
1390
|
+
const runs = useRef(0);
|
|
1391
|
+
const [applied, setApplied] = useState(
|
|
1392
|
+
null
|
|
1393
|
+
);
|
|
1394
|
+
const compute = useCallback(
|
|
1395
|
+
(key, currentNodes, currentEdges) => {
|
|
1396
|
+
const started = performance.now();
|
|
1397
|
+
const result = layoutFlow(currentNodes, currentEdges, {
|
|
1398
|
+
direction,
|
|
1399
|
+
nodeSpacing: NODE_SPACING,
|
|
1400
|
+
rankSpacing: RANK_SPACING[direction]
|
|
1401
|
+
});
|
|
1402
|
+
const snapshot2 = toSnapshot(result, performance.now() - started);
|
|
1403
|
+
runs.current += 1;
|
|
1404
|
+
cache.current.set(key, snapshot2);
|
|
1405
|
+
setApplied({ key, snapshot: snapshot2 });
|
|
1406
|
+
},
|
|
1407
|
+
[direction]
|
|
1408
|
+
);
|
|
1409
|
+
const latest = useRef({ nodes, edges });
|
|
1410
|
+
latest.current = { nodes, edges };
|
|
1411
|
+
const cachedForKey = cache.current.get(cacheKey);
|
|
1412
|
+
const hasLayout = applied?.key === cacheKey || cachedForKey !== void 0;
|
|
1413
|
+
useEffect(() => {
|
|
1414
|
+
const cached = cache.current.get(cacheKey);
|
|
1415
|
+
if (cached) {
|
|
1416
|
+
setApplied(
|
|
1417
|
+
(current) => current?.key === cacheKey ? current : { key: cacheKey, snapshot: cached }
|
|
1418
|
+
);
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
if (latest.current.nodes.length === 0) {
|
|
1422
|
+
setApplied({
|
|
1423
|
+
key: cacheKey,
|
|
1424
|
+
snapshot: {
|
|
1425
|
+
positions: {},
|
|
1426
|
+
sourcePosition: {},
|
|
1427
|
+
targetPosition: {},
|
|
1428
|
+
backEdges: [],
|
|
1429
|
+
selfLoops: [],
|
|
1430
|
+
durationMs: 0
|
|
1431
|
+
}
|
|
1432
|
+
});
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
if (runs.current === 0 || debounceMs <= 0) {
|
|
1436
|
+
compute(cacheKey, latest.current.nodes, latest.current.edges);
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
const timer = setTimeout(
|
|
1440
|
+
() => compute(cacheKey, latest.current.nodes, latest.current.edges),
|
|
1441
|
+
debounceMs
|
|
1442
|
+
);
|
|
1443
|
+
return () => clearTimeout(timer);
|
|
1444
|
+
}, [cacheKey, compute, debounceMs]);
|
|
1445
|
+
const snapshot = applied?.key === cacheKey ? applied.snapshot : cachedForKey;
|
|
1446
|
+
const positionedNodes = useMemo3(
|
|
1447
|
+
() => snapshot ? applyLayoutSnapshot(nodes, snapshot) : nodes,
|
|
1448
|
+
[nodes, snapshot]
|
|
1449
|
+
);
|
|
1450
|
+
const backEdgeIds = useMemo3(
|
|
1451
|
+
() => snapshot ? new Set(snapshot.backEdges) : EMPTY_SET,
|
|
1452
|
+
[snapshot]
|
|
1453
|
+
);
|
|
1454
|
+
const selfLoopIds = useMemo3(
|
|
1455
|
+
() => snapshot ? new Set(snapshot.selfLoops) : EMPTY_SET,
|
|
1456
|
+
[snapshot]
|
|
1457
|
+
);
|
|
1458
|
+
return {
|
|
1459
|
+
nodes: positionedNodes,
|
|
1460
|
+
backEdgeIds,
|
|
1461
|
+
selfLoopIds,
|
|
1462
|
+
layoutRuns: runs.current,
|
|
1463
|
+
lastLayoutMs: snapshot?.durationMs ?? 0,
|
|
1464
|
+
pending: !hasLayout && nodes.length > 0
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// src/process-map/process-map.tsx
|
|
1469
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1470
|
+
var NODE_TYPES = { "process-activity": ProcessActivityNode };
|
|
1471
|
+
var EDGE_TYPES = { "process-transition": ProcessTransitionEdge };
|
|
1472
|
+
var PROCESS_MAP_LEGIBLE_ZOOM = 0.75;
|
|
1473
|
+
var FIT_VIEW_OPTIONS = {
|
|
1474
|
+
padding: 0.15,
|
|
1475
|
+
maxZoom: 1,
|
|
1476
|
+
minZoom: PROCESS_MAP_LEGIBLE_ZOOM
|
|
1477
|
+
};
|
|
1478
|
+
var MIN_ZOOM = 0.1;
|
|
1479
|
+
function useResolvedGraph(graph, log, abstraction) {
|
|
1480
|
+
const discovered = useMemo4(
|
|
1481
|
+
() => graph ? void 0 : log ? discoverGraph(log) : void 0,
|
|
1482
|
+
[graph, log]
|
|
1483
|
+
);
|
|
1484
|
+
const base = graph ?? discovered;
|
|
1485
|
+
const activities = abstraction?.activities;
|
|
1486
|
+
const paths = abstraction?.paths;
|
|
1487
|
+
return useMemo4(() => {
|
|
1488
|
+
if (!base) return void 0;
|
|
1489
|
+
if (activities === void 0 && paths === void 0) return base;
|
|
1490
|
+
return abstractGraph(base, { activities: activities ?? 1, paths: paths ?? 1 });
|
|
1491
|
+
}, [base, activities, paths]);
|
|
1492
|
+
}
|
|
1493
|
+
function ProcessMap({
|
|
1494
|
+
graph,
|
|
1495
|
+
log,
|
|
1496
|
+
abstraction,
|
|
1497
|
+
metric,
|
|
1498
|
+
rework,
|
|
1499
|
+
selection,
|
|
1500
|
+
selectionStates,
|
|
1501
|
+
onSelect,
|
|
1502
|
+
onFilterIntent,
|
|
1503
|
+
direction = "TB",
|
|
1504
|
+
showMiniMap = true,
|
|
1505
|
+
showLegend = true,
|
|
1506
|
+
tableView = false,
|
|
1507
|
+
loading = false,
|
|
1508
|
+
label,
|
|
1509
|
+
className,
|
|
1510
|
+
...props
|
|
1511
|
+
}) {
|
|
1512
|
+
const { t } = useLocale();
|
|
1513
|
+
const mapLabel = label ?? t("process.map.label");
|
|
1514
|
+
const selectionStateText = useCallback2(
|
|
1515
|
+
(state) => {
|
|
1516
|
+
const key = PROCESS_SELECTION_STATE_MESSAGE_KEYS[state];
|
|
1517
|
+
return key ? t(key) : "";
|
|
1518
|
+
},
|
|
1519
|
+
[t]
|
|
1520
|
+
);
|
|
1521
|
+
const resolved = useResolvedGraph(graph, log, abstraction);
|
|
1522
|
+
const derivedRework = useMemo4(
|
|
1523
|
+
() => rework ? void 0 : log ? detectRework(log) : void 0,
|
|
1524
|
+
[rework, log]
|
|
1525
|
+
);
|
|
1526
|
+
const activeRework = rework ?? derivedRework;
|
|
1527
|
+
const isControlled = selection !== void 0;
|
|
1528
|
+
const [ownSelection, setOwnSelection] = useState2(null);
|
|
1529
|
+
const activeSelection = isControlled ? selection ?? null : ownSelection;
|
|
1530
|
+
const applySelection = useCallback2(
|
|
1531
|
+
(next) => {
|
|
1532
|
+
if (!isControlled) setOwnSelection(next);
|
|
1533
|
+
onSelect?.(next);
|
|
1534
|
+
},
|
|
1535
|
+
[isControlled, onSelect]
|
|
1536
|
+
);
|
|
1537
|
+
const structureKey = useMemo4(
|
|
1538
|
+
() => resolved ? processGraphStructureKey(resolved) : "",
|
|
1539
|
+
[resolved]
|
|
1540
|
+
);
|
|
1541
|
+
const firstPass = useMemo4(
|
|
1542
|
+
() => resolved ? buildProcessMapModel({
|
|
1543
|
+
graph: resolved,
|
|
1544
|
+
metric,
|
|
1545
|
+
rework: activeRework,
|
|
1546
|
+
selection: activeSelection,
|
|
1547
|
+
selectionStates
|
|
1548
|
+
}) : null,
|
|
1549
|
+
[resolved, metric, activeRework, activeSelection, selectionStates]
|
|
1550
|
+
);
|
|
1551
|
+
const [nodeSizes, setNodeSizes] = useState2({});
|
|
1552
|
+
const handleNodesChange = useCallback2((changes) => {
|
|
1553
|
+
setNodeSizes((current) => {
|
|
1554
|
+
let next = null;
|
|
1555
|
+
for (const change of changes) {
|
|
1556
|
+
if (change.type !== "dimensions" || !change.dimensions) continue;
|
|
1557
|
+
const previous = current[change.id];
|
|
1558
|
+
if (previous?.width === change.dimensions.width && previous.height === change.dimensions.height) {
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1561
|
+
next ??= { ...current };
|
|
1562
|
+
next[change.id] = { width: change.dimensions.width, height: change.dimensions.height };
|
|
1563
|
+
}
|
|
1564
|
+
return next ?? current;
|
|
1565
|
+
});
|
|
1566
|
+
}, []);
|
|
1567
|
+
const layoutNodes = useMemo4(() => {
|
|
1568
|
+
const source = firstPass?.nodes ?? EMPTY_NODES;
|
|
1569
|
+
let changed = false;
|
|
1570
|
+
const next = source.map((node) => {
|
|
1571
|
+
const measured = nodeSizes[node.id];
|
|
1572
|
+
if (!measured) return node;
|
|
1573
|
+
changed = true;
|
|
1574
|
+
return { ...node, measured };
|
|
1575
|
+
});
|
|
1576
|
+
return changed ? next : source;
|
|
1577
|
+
}, [firstPass, nodeSizes]);
|
|
1578
|
+
const sizeKey = useMemo4(() => {
|
|
1579
|
+
let width = 0;
|
|
1580
|
+
let height = 0;
|
|
1581
|
+
for (const size of Object.values(nodeSizes)) {
|
|
1582
|
+
if (size.width > width) width = size.width;
|
|
1583
|
+
if (size.height > height) height = size.height;
|
|
1584
|
+
}
|
|
1585
|
+
if (width === 0 && height === 0) return "unmeasured";
|
|
1586
|
+
return `${Math.round(width / 8)}x${Math.round(height / 8)}`;
|
|
1587
|
+
}, [nodeSizes]);
|
|
1588
|
+
const layoutKey = `${structureKey}::${sizeKey}`;
|
|
1589
|
+
const layout = useProcessLayout({
|
|
1590
|
+
nodes: layoutNodes,
|
|
1591
|
+
edges: firstPass?.edges ?? EMPTY_EDGES,
|
|
1592
|
+
structureKey: layoutKey,
|
|
1593
|
+
direction
|
|
1594
|
+
});
|
|
1595
|
+
const model = useMemo4(
|
|
1596
|
+
() => resolved ? buildProcessMapModel({
|
|
1597
|
+
graph: resolved,
|
|
1598
|
+
metric,
|
|
1599
|
+
rework: activeRework,
|
|
1600
|
+
selection: activeSelection,
|
|
1601
|
+
selectionStates,
|
|
1602
|
+
backEdgeIds: layout.backEdgeIds
|
|
1603
|
+
}) : null,
|
|
1604
|
+
[resolved, metric, activeRework, activeSelection, selectionStates, layout.backEdgeIds]
|
|
1605
|
+
);
|
|
1606
|
+
const positionedNodes = useMemo4(
|
|
1607
|
+
() => model ? applyPositions(model, layout, direction) : EMPTY_NODES,
|
|
1608
|
+
[model, layout, direction]
|
|
1609
|
+
);
|
|
1610
|
+
const [hover, setHover] = useState2(EMPTY_PROCESS_MAP_HOVER);
|
|
1611
|
+
const edgesRef = useRef2([]);
|
|
1612
|
+
edgesRef.current = model?.edges ?? [];
|
|
1613
|
+
const handleNodeEnter = useCallback2((_event, node) => {
|
|
1614
|
+
const incident = /* @__PURE__ */ new Set();
|
|
1615
|
+
for (const edge of edgesRef.current) {
|
|
1616
|
+
if (edge.source === node.id || edge.target === node.id) incident.add(edge.id);
|
|
1617
|
+
}
|
|
1618
|
+
setHover({ activityId: node.id, incidentEdgeIds: incident });
|
|
1619
|
+
}, []);
|
|
1620
|
+
const handleNodeLeave = useCallback2(() => setHover(EMPTY_PROCESS_MAP_HOVER), []);
|
|
1621
|
+
const [menuTarget, setMenuTarget] = useState2(null);
|
|
1622
|
+
const menuOpen = menuTarget !== null;
|
|
1623
|
+
const returnFocusRef = useRef2(null);
|
|
1624
|
+
const openMenuFor = useCallback2(
|
|
1625
|
+
(target, restoreFocus = null) => {
|
|
1626
|
+
if (!target) return;
|
|
1627
|
+
returnFocusRef.current = restoreFocus;
|
|
1628
|
+
setMenuTarget(target);
|
|
1629
|
+
},
|
|
1630
|
+
[]
|
|
1631
|
+
);
|
|
1632
|
+
const menuActivities = useMemo4(() => {
|
|
1633
|
+
if (!menuTarget) return [];
|
|
1634
|
+
if (menuTarget.kind === "activity") return [menuTarget.id];
|
|
1635
|
+
const edge = (model?.edges ?? []).find((e) => e.id === menuTarget.id);
|
|
1636
|
+
if (!edge) return [];
|
|
1637
|
+
return edge.source === edge.target ? [edge.source] : [edge.source, edge.target];
|
|
1638
|
+
}, [menuTarget, model]);
|
|
1639
|
+
const targetOfEvent = useCallback2(
|
|
1640
|
+
(element) => {
|
|
1641
|
+
const focusedId = element?.closest("[data-id]")?.dataset.id;
|
|
1642
|
+
if (!focusedId) return activeSelection;
|
|
1643
|
+
return model?.edges.some((edge) => edge.id === focusedId) ? { kind: "transition", id: focusedId } : { kind: "activity", id: focusedId };
|
|
1644
|
+
},
|
|
1645
|
+
[activeSelection, model]
|
|
1646
|
+
);
|
|
1647
|
+
const handleElementKey = useCallback2(
|
|
1648
|
+
(event, target) => {
|
|
1649
|
+
if (!target) return false;
|
|
1650
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return false;
|
|
1651
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1652
|
+
applySelection(
|
|
1653
|
+
activeSelection?.kind === target.kind && activeSelection.id === target.id ? null : target
|
|
1654
|
+
);
|
|
1655
|
+
return true;
|
|
1656
|
+
}
|
|
1657
|
+
if (event.key !== "f" && event.key !== "F") return false;
|
|
1658
|
+
event.preventDefault();
|
|
1659
|
+
const active = event.target?.ownerDocument.activeElement;
|
|
1660
|
+
openMenuFor(target, active instanceof HTMLElement ? focusRestorer(active) : null);
|
|
1661
|
+
return true;
|
|
1662
|
+
},
|
|
1663
|
+
[activeSelection, applySelection, openMenuFor]
|
|
1664
|
+
);
|
|
1665
|
+
const handleKeyDown = useCallback2(
|
|
1666
|
+
(event) => {
|
|
1667
|
+
const element = event.target;
|
|
1668
|
+
if (element?.closest("input, textarea, [contenteditable='true']")) return;
|
|
1669
|
+
const onFlowElement = Boolean(element?.closest("[data-id]"));
|
|
1670
|
+
if ((event.key === "Enter" || event.key === " ") && !onFlowElement) return;
|
|
1671
|
+
handleElementKey(event, targetOfEvent(element));
|
|
1672
|
+
},
|
|
1673
|
+
[handleElementKey, targetOfEvent]
|
|
1674
|
+
);
|
|
1675
|
+
const handleEdgeKey = useCallback2(
|
|
1676
|
+
(edgeId, event) => {
|
|
1677
|
+
if (handleElementKey(event, { kind: "transition", id: edgeId })) event.stopPropagation();
|
|
1678
|
+
},
|
|
1679
|
+
[handleElementKey]
|
|
1680
|
+
);
|
|
1681
|
+
const emitIntent = useCallback2(
|
|
1682
|
+
(kind, activity) => {
|
|
1683
|
+
onFilterIntent?.({ kind, activity });
|
|
1684
|
+
setMenuTarget(null);
|
|
1685
|
+
},
|
|
1686
|
+
[onFilterIntent]
|
|
1687
|
+
);
|
|
1688
|
+
useEffect2(() => {
|
|
1689
|
+
if (!menuTarget || !model) return;
|
|
1690
|
+
const stillThere = menuTarget.kind === "activity" ? model.nodes.some((n) => n.id === menuTarget.id) : model.edges.some((e) => e.id === menuTarget.id);
|
|
1691
|
+
if (!stillThere) setMenuTarget(null);
|
|
1692
|
+
}, [menuTarget, model]);
|
|
1693
|
+
if (loading) {
|
|
1694
|
+
return /* @__PURE__ */ jsx3(
|
|
1695
|
+
"div",
|
|
1696
|
+
{
|
|
1697
|
+
"data-slot": "process-map",
|
|
1698
|
+
"data-state": "loading",
|
|
1699
|
+
className: cn2("relative size-full min-h-64", className),
|
|
1700
|
+
...props,
|
|
1701
|
+
children: /* @__PURE__ */ jsx3(StatePanel, { kind: "loading", title: t("process.map.loading") })
|
|
1702
|
+
}
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
if (!model || model.nodes.length === 0) {
|
|
1706
|
+
return /* @__PURE__ */ jsx3(
|
|
1707
|
+
"div",
|
|
1708
|
+
{
|
|
1709
|
+
"data-slot": "process-map",
|
|
1710
|
+
"data-state": "empty",
|
|
1711
|
+
className: cn2("relative size-full min-h-64", className),
|
|
1712
|
+
...props,
|
|
1713
|
+
children: /* @__PURE__ */ jsx3(
|
|
1714
|
+
StatePanel,
|
|
1715
|
+
{
|
|
1716
|
+
kind: "empty",
|
|
1717
|
+
title: t("process.map.empty"),
|
|
1718
|
+
description: t("process.map.emptyBody")
|
|
1719
|
+
}
|
|
1720
|
+
)
|
|
1721
|
+
}
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
const filterMenu = /* @__PURE__ */ jsxs2(
|
|
1725
|
+
DropdownMenu,
|
|
1726
|
+
{
|
|
1727
|
+
modal: false,
|
|
1728
|
+
open: menuOpen,
|
|
1729
|
+
onOpenChange: (open) => {
|
|
1730
|
+
if (!open) setMenuTarget(null);
|
|
1731
|
+
},
|
|
1732
|
+
children: [
|
|
1733
|
+
/* @__PURE__ */ jsx3(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs2(
|
|
1734
|
+
Button,
|
|
1735
|
+
{
|
|
1736
|
+
type: "button",
|
|
1737
|
+
variant: "outline",
|
|
1738
|
+
size: "sm",
|
|
1739
|
+
"data-slot": "process-map-filter-trigger",
|
|
1740
|
+
"aria-keyshortcuts": "f",
|
|
1741
|
+
onClick: () => {
|
|
1742
|
+
if (menuOpen) return;
|
|
1743
|
+
openMenuFor(activeSelection ?? { kind: "activity", id: model.nodes[0].id });
|
|
1744
|
+
},
|
|
1745
|
+
children: [
|
|
1746
|
+
/* @__PURE__ */ jsx3(Filter, { "aria-hidden": "true", className: "size-4" }),
|
|
1747
|
+
t("process.map.filter")
|
|
1748
|
+
]
|
|
1749
|
+
}
|
|
1750
|
+
) }),
|
|
1751
|
+
/* @__PURE__ */ jsx3(
|
|
1752
|
+
DropdownMenuContent,
|
|
1753
|
+
{
|
|
1754
|
+
align: "end",
|
|
1755
|
+
"data-slot": "process-map-filter-menu",
|
|
1756
|
+
onCloseAutoFocus: (event) => {
|
|
1757
|
+
const restore = returnFocusRef.current;
|
|
1758
|
+
returnFocusRef.current = null;
|
|
1759
|
+
if (!restore) return;
|
|
1760
|
+
event.preventDefault();
|
|
1761
|
+
restore();
|
|
1762
|
+
},
|
|
1763
|
+
children: menuActivities.map((activity, index2) => /* @__PURE__ */ jsxs2("div", { children: [
|
|
1764
|
+
index2 > 0 ? /* @__PURE__ */ jsx3(DropdownMenuSeparator, {}) : null,
|
|
1765
|
+
/* @__PURE__ */ jsx3(DropdownMenuLabel, { children: activity }),
|
|
1766
|
+
PROCESS_FILTER_INTENT_KINDS.map((kind) => /* @__PURE__ */ jsx3(
|
|
1767
|
+
DropdownMenuItem,
|
|
1768
|
+
{
|
|
1769
|
+
"aria-label": t(PROCESS_FILTER_INTENT_MESSAGE_KEYS[kind], { activity }),
|
|
1770
|
+
onSelect: () => emitIntent(kind, activity),
|
|
1771
|
+
children: PROCESS_FILTER_INTENT_LABELS[kind]
|
|
1772
|
+
},
|
|
1773
|
+
kind
|
|
1774
|
+
))
|
|
1775
|
+
] }, activity))
|
|
1776
|
+
}
|
|
1777
|
+
)
|
|
1778
|
+
]
|
|
1779
|
+
}
|
|
1780
|
+
);
|
|
1781
|
+
const excludedActivityNames = model.activityRows.filter((row) => row.selectionState === "excluded").map((row) => row.title).sort((a, b) => a.localeCompare(b));
|
|
1782
|
+
const excludedTransitionNames = model.transitionRows.filter((row) => row.selectionState === "excluded").map((row) => `${row.source} \u2192 ${row.target}`).sort((a, b) => a.localeCompare(b));
|
|
1783
|
+
const selectionSummary = [
|
|
1784
|
+
t("process.map.excludedActivities", {
|
|
1785
|
+
count: model.excludedCounts.activities,
|
|
1786
|
+
total: model.excludedCounts.totalActivities
|
|
1787
|
+
}),
|
|
1788
|
+
excludedActivityNames.length > 0 ? t("process.map.excludedActivityNames", { names: excludedActivityNames.join(", ") }) : null,
|
|
1789
|
+
t("process.map.excludedTransitions", {
|
|
1790
|
+
count: model.excludedCounts.transitions,
|
|
1791
|
+
total: model.excludedCounts.totalTransitions
|
|
1792
|
+
}),
|
|
1793
|
+
excludedTransitionNames.length > 0 ? t("process.map.excludedTransitionNames", { names: excludedTransitionNames.join(", ") }) : null
|
|
1794
|
+
].filter((part) => Boolean(part)).join(" \xB7 ");
|
|
1795
|
+
if (tableView) {
|
|
1796
|
+
return /* @__PURE__ */ jsxs2(
|
|
1797
|
+
"div",
|
|
1798
|
+
{
|
|
1799
|
+
"data-slot": "process-map",
|
|
1800
|
+
"data-view": "table",
|
|
1801
|
+
className: cn2("flex size-full flex-col gap-4", className),
|
|
1802
|
+
...props,
|
|
1803
|
+
children: [
|
|
1804
|
+
/* @__PURE__ */ jsx3(
|
|
1805
|
+
"p",
|
|
1806
|
+
{
|
|
1807
|
+
"data-slot": "process-map-selection-summary",
|
|
1808
|
+
role: "status",
|
|
1809
|
+
"aria-live": "polite",
|
|
1810
|
+
className: "sr-only",
|
|
1811
|
+
children: selectionSummary
|
|
1812
|
+
}
|
|
1813
|
+
),
|
|
1814
|
+
/* @__PURE__ */ jsx3("div", { className: "flex items-center justify-end", children: filterMenu }),
|
|
1815
|
+
/* @__PURE__ */ jsxs2(Table, { "data-slot": "process-map-activity-table", children: [
|
|
1816
|
+
/* @__PURE__ */ jsx3(TableCaption, { children: t("process.map.activityCaption", { metric: model.nodeMetricLabel.toLowerCase() }) }),
|
|
1817
|
+
/* @__PURE__ */ jsx3(TableHeader, { children: /* @__PURE__ */ jsxs2(TableRow, { children: [
|
|
1818
|
+
/* @__PURE__ */ jsx3(TableHead, { scope: "col", children: t("process.map.columnActivity") }),
|
|
1819
|
+
/* @__PURE__ */ jsx3(TableHead, { scope: "col", children: t("process.map.columnRole") }),
|
|
1820
|
+
/* @__PURE__ */ jsx3(TableHead, { scope: "col", children: model.nodeMetricLabel }),
|
|
1821
|
+
/* @__PURE__ */ jsx3(TableHead, { scope: "col", children: t("process.map.columnRework") }),
|
|
1822
|
+
/* @__PURE__ */ jsx3(TableHead, { scope: "col", children: t("process.map.columnState") })
|
|
1823
|
+
] }) }),
|
|
1824
|
+
/* @__PURE__ */ jsx3(TableBody, { children: model.activityRows.map((row) => /* @__PURE__ */ jsxs2(
|
|
1825
|
+
TableRow,
|
|
1826
|
+
{
|
|
1827
|
+
"data-selection": row.selectionState,
|
|
1828
|
+
"data-state": row.selectionState === "selected" ? "selected" : void 0,
|
|
1829
|
+
children: [
|
|
1830
|
+
/* @__PURE__ */ jsx3(TableCell, { children: row.title }),
|
|
1831
|
+
/* @__PURE__ */ jsx3(TableCell, { children: row.role }),
|
|
1832
|
+
/* @__PURE__ */ jsx3(TableCell, { className: "tabular-nums", children: row.secondaryLabel ? `${row.primaryLabel} \xB7 ${row.secondaryLabel}` : row.primaryLabel }),
|
|
1833
|
+
/* @__PURE__ */ jsx3(TableCell, { className: "tabular-nums", children: row.reworkCount ?? 0 }),
|
|
1834
|
+
/* @__PURE__ */ jsx3(TableCell, { children: selectionStateText(row.selectionState) })
|
|
1835
|
+
]
|
|
1836
|
+
},
|
|
1837
|
+
row.id
|
|
1838
|
+
)) })
|
|
1839
|
+
] }),
|
|
1840
|
+
/* @__PURE__ */ jsxs2(Table, { "data-slot": "process-map-transition-table", children: [
|
|
1841
|
+
/* @__PURE__ */ jsx3(TableCaption, { children: t("process.map.transitionCaption", {
|
|
1842
|
+
metric: model.edgeMetricLabel.toLowerCase()
|
|
1843
|
+
}) }),
|
|
1844
|
+
/* @__PURE__ */ jsx3(TableHeader, { children: /* @__PURE__ */ jsxs2(TableRow, { children: [
|
|
1845
|
+
/* @__PURE__ */ jsx3(TableHead, { scope: "col", children: t("process.map.columnFrom") }),
|
|
1846
|
+
/* @__PURE__ */ jsx3(TableHead, { scope: "col", children: t("process.map.columnTo") }),
|
|
1847
|
+
/* @__PURE__ */ jsx3(TableHead, { scope: "col", children: t("process.map.columnShape") }),
|
|
1848
|
+
/* @__PURE__ */ jsx3(TableHead, { scope: "col", children: model.edgeMetricLabel }),
|
|
1849
|
+
/* @__PURE__ */ jsx3(TableHead, { scope: "col", children: t("process.map.columnState") })
|
|
1850
|
+
] }) }),
|
|
1851
|
+
/* @__PURE__ */ jsx3(TableBody, { children: model.transitionRows.map((row) => /* @__PURE__ */ jsxs2(
|
|
1852
|
+
TableRow,
|
|
1853
|
+
{
|
|
1854
|
+
"data-selection": row.selectionState,
|
|
1855
|
+
"data-state": row.selectionState === "selected" ? "selected" : void 0,
|
|
1856
|
+
children: [
|
|
1857
|
+
/* @__PURE__ */ jsx3(TableCell, { children: row.source }),
|
|
1858
|
+
/* @__PURE__ */ jsx3(TableCell, { children: row.target }),
|
|
1859
|
+
/* @__PURE__ */ jsx3(TableCell, { children: row.shape }),
|
|
1860
|
+
/* @__PURE__ */ jsx3(TableCell, { className: "tabular-nums", children: row.secondaryLabel ? `${row.primaryLabel} \xB7 ${row.secondaryLabel}` : row.primaryLabel }),
|
|
1861
|
+
/* @__PURE__ */ jsx3(TableCell, { children: selectionStateText(row.selectionState) })
|
|
1862
|
+
]
|
|
1863
|
+
},
|
|
1864
|
+
row.id
|
|
1865
|
+
)) })
|
|
1866
|
+
] })
|
|
1867
|
+
]
|
|
1868
|
+
}
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
return /* @__PURE__ */ jsxs2(
|
|
1872
|
+
"div",
|
|
1873
|
+
{
|
|
1874
|
+
"data-slot": "process-map",
|
|
1875
|
+
"data-view": "canvas",
|
|
1876
|
+
"data-direction": direction,
|
|
1877
|
+
className: cn2("relative size-full min-h-64", className),
|
|
1878
|
+
onKeyDown: handleKeyDown,
|
|
1879
|
+
...props,
|
|
1880
|
+
children: [
|
|
1881
|
+
/* @__PURE__ */ jsx3(
|
|
1882
|
+
"p",
|
|
1883
|
+
{
|
|
1884
|
+
"data-slot": "process-map-selection-summary",
|
|
1885
|
+
role: "status",
|
|
1886
|
+
"aria-live": "polite",
|
|
1887
|
+
className: "sr-only",
|
|
1888
|
+
children: selectionSummary
|
|
1889
|
+
}
|
|
1890
|
+
),
|
|
1891
|
+
/* @__PURE__ */ jsx3(ProcessMapHoverContext, { value: hover, children: /* @__PURE__ */ jsx3(ProcessMapEdgeKeyContext, { value: handleEdgeKey, children: /* @__PURE__ */ jsxs2(
|
|
1892
|
+
CanvasShell,
|
|
1893
|
+
{
|
|
1894
|
+
nodes: positionedNodes,
|
|
1895
|
+
edges: model.edges,
|
|
1896
|
+
nodeTypes: NODE_TYPES,
|
|
1897
|
+
edgeTypes: EDGE_TYPES,
|
|
1898
|
+
fitViewKey: `${layoutKey}::${direction}::${layout.layoutRuns}`,
|
|
1899
|
+
fitViewKeyOptions: FIT_VIEW_OPTIONS,
|
|
1900
|
+
minZoom: MIN_ZOOM,
|
|
1901
|
+
nodesDraggable: false,
|
|
1902
|
+
nodesConnectable: false,
|
|
1903
|
+
edgesFocusable: false,
|
|
1904
|
+
className: PROCESS_MAP_NODE_MOTION_CLASS,
|
|
1905
|
+
"aria-label": mapLabel,
|
|
1906
|
+
onNodeClick: (_event, node) => applySelection(
|
|
1907
|
+
activeSelection?.kind === "activity" && activeSelection.id === node.id ? null : { kind: "activity", id: node.id }
|
|
1908
|
+
),
|
|
1909
|
+
onEdgeClick: (_event, edge) => applySelection(
|
|
1910
|
+
activeSelection?.kind === "transition" && activeSelection.id === edge.id ? null : { kind: "transition", id: edge.id }
|
|
1911
|
+
),
|
|
1912
|
+
onNodeMouseEnter: handleNodeEnter,
|
|
1913
|
+
onNodeMouseLeave: handleNodeLeave,
|
|
1914
|
+
onNodeContextMenu: (event, node) => {
|
|
1915
|
+
event.preventDefault();
|
|
1916
|
+
openMenuFor({ kind: "activity", id: node.id });
|
|
1917
|
+
},
|
|
1918
|
+
onEdgeContextMenu: (event, edge) => {
|
|
1919
|
+
event.preventDefault();
|
|
1920
|
+
openMenuFor({ kind: "transition", id: edge.id });
|
|
1921
|
+
},
|
|
1922
|
+
onPaneClick: () => applySelection(null),
|
|
1923
|
+
onNodesChange: handleNodesChange,
|
|
1924
|
+
children: [
|
|
1925
|
+
/* @__PURE__ */ jsx3(ZoomControls, { position: "bottom-left" }),
|
|
1926
|
+
showMiniMap ? /* @__PURE__ */ jsx3(FlowMiniMap, { pannable: true, zoomable: true }) : null
|
|
1927
|
+
]
|
|
1928
|
+
}
|
|
1929
|
+
) }) }),
|
|
1930
|
+
/* @__PURE__ */ jsxs2(
|
|
1931
|
+
"div",
|
|
1932
|
+
{
|
|
1933
|
+
"data-slot": "process-map-top-rail",
|
|
1934
|
+
className: "pointer-events-none absolute inset-x-3 top-3 flex items-start justify-between gap-3",
|
|
1935
|
+
children: [
|
|
1936
|
+
showLegend ? /* @__PURE__ */ jsx3(
|
|
1937
|
+
Legend,
|
|
1938
|
+
{
|
|
1939
|
+
variant: "scale",
|
|
1940
|
+
kind: "width",
|
|
1941
|
+
domain: model.edgeDomain,
|
|
1942
|
+
format: model.formatEdgeValue,
|
|
1943
|
+
title: model.edgeMetricLabel,
|
|
1944
|
+
className: "pointer-events-auto"
|
|
1945
|
+
}
|
|
1946
|
+
) : /* @__PURE__ */ jsx3("span", {}),
|
|
1947
|
+
/* @__PURE__ */ jsx3("div", { className: "pointer-events-auto", children: filterMenu })
|
|
1948
|
+
]
|
|
1949
|
+
}
|
|
1950
|
+
)
|
|
1951
|
+
]
|
|
1952
|
+
}
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
function focusRestorer(element) {
|
|
1956
|
+
const flowId = element.closest("[data-id]")?.dataset.id;
|
|
1957
|
+
const slot = element.dataset.slot;
|
|
1958
|
+
const layer = element.closest(".react-flow__edgelabel-renderer");
|
|
1959
|
+
const peers = slot && layer ? [...layer.querySelectorAll(`[data-slot="${slot}"]`)] : [];
|
|
1960
|
+
const index2 = peers.indexOf(element);
|
|
1961
|
+
const root = element.closest('[data-slot="process-map"]');
|
|
1962
|
+
return () => {
|
|
1963
|
+
if (element.isConnected) {
|
|
1964
|
+
element.focus();
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
if (flowId && root) {
|
|
1968
|
+
root.querySelector(`[data-id="${CSS.escape(flowId)}"]`)?.focus();
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
if (slot && index2 >= 0 && layer?.isConnected) {
|
|
1972
|
+
layer.querySelectorAll(`[data-slot="${slot}"]`)[index2]?.focus();
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
var EMPTY_NODES = [];
|
|
1977
|
+
var EMPTY_EDGES = [];
|
|
1978
|
+
function applyPositions(model, layout, direction) {
|
|
1979
|
+
const byId = new Map(layout.nodes.map((node) => [node.id, node]));
|
|
1980
|
+
const positioned = model.nodes.map((node) => {
|
|
1981
|
+
const laidOut = byId.get(node.id);
|
|
1982
|
+
if (!laidOut) return node;
|
|
1983
|
+
return {
|
|
1984
|
+
...node,
|
|
1985
|
+
position: laidOut.position,
|
|
1986
|
+
sourcePosition: laidOut.sourcePosition,
|
|
1987
|
+
targetPosition: laidOut.targetPosition
|
|
1988
|
+
};
|
|
1989
|
+
});
|
|
1990
|
+
const alongFlow = direction === "LR" || direction === "RL" ? "x" : "y";
|
|
1991
|
+
const acrossFlow = alongFlow === "x" ? "y" : "x";
|
|
1992
|
+
const sign = direction === "BT" || direction === "RL" ? -1 : 1;
|
|
1993
|
+
return positioned.sort((a, b) => {
|
|
1994
|
+
const along = sign * (a.position[alongFlow] - b.position[alongFlow]);
|
|
1995
|
+
if (along !== 0) return along;
|
|
1996
|
+
const across = a.position[acrossFlow] - b.position[acrossFlow];
|
|
1997
|
+
if (across !== 0) return across;
|
|
1998
|
+
return a.id.localeCompare(b.id);
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// src/abstraction-controls/abstraction-controls.tsx
|
|
2003
|
+
import { forwardRef, useCallback as useCallback3, useId } from "react";
|
|
2004
|
+
import { Sparkles } from "lucide-react";
|
|
2005
|
+
import { Button as Button2, Label, Slider, Switch, useLocale as useLocale2 } from "@elabs-ai/components-ui";
|
|
2006
|
+
import { cn as cn3 } from "@elabs-ai/components-ui/lib/cn";
|
|
2007
|
+
|
|
2008
|
+
// src/abstraction-controls/auto-abstraction.ts
|
|
2009
|
+
var DEFAULT_MAX_ACTIVITIES = 25;
|
|
2010
|
+
var DEFAULT_MAX_STEPS = 8;
|
|
2011
|
+
var DEFAULT_MIN_FRACTION = 0.05;
|
|
2012
|
+
function keptActivityCount(total, fraction) {
|
|
2013
|
+
return Math.max(1, Math.round(total * fraction));
|
|
2014
|
+
}
|
|
2015
|
+
function computeAutoAbstraction(totalActivities, opts = {}) {
|
|
2016
|
+
const maxActivities = opts.maxActivities ?? DEFAULT_MAX_ACTIVITIES;
|
|
2017
|
+
const maxSteps = opts.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
2018
|
+
const minFraction = opts.minFraction ?? DEFAULT_MIN_FRACTION;
|
|
2019
|
+
const total = totalActivities;
|
|
2020
|
+
if (total === 0 || keptActivityCount(total, 1) <= maxActivities) {
|
|
2021
|
+
return { activities: 1, steps: 0 };
|
|
2022
|
+
}
|
|
2023
|
+
if (keptActivityCount(total, minFraction) > maxActivities) {
|
|
2024
|
+
return { activities: minFraction, steps: 1 };
|
|
2025
|
+
}
|
|
2026
|
+
let lo = minFraction;
|
|
2027
|
+
let hi = 1;
|
|
2028
|
+
let best = minFraction;
|
|
2029
|
+
for (let step = 1; step <= maxSteps; step += 1) {
|
|
2030
|
+
const mid = (lo + hi) / 2;
|
|
2031
|
+
if (keptActivityCount(total, mid) <= maxActivities) {
|
|
2032
|
+
best = mid;
|
|
2033
|
+
lo = mid;
|
|
2034
|
+
} else {
|
|
2035
|
+
hi = mid;
|
|
2036
|
+
}
|
|
2037
|
+
if (step === maxSteps) {
|
|
2038
|
+
return { activities: best, steps: step };
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
return { activities: best, steps: maxSteps };
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
// src/abstraction-controls/abstraction-controls.tsx
|
|
2045
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2046
|
+
var TICKS = [25, 50, 75, 100];
|
|
2047
|
+
var SLIDER_MIN = 0;
|
|
2048
|
+
var SLIDER_MAX = 100;
|
|
2049
|
+
function tickOffsetPercent(percent) {
|
|
2050
|
+
return (percent - SLIDER_MIN) / (SLIDER_MAX - SLIDER_MIN) * 100;
|
|
2051
|
+
}
|
|
2052
|
+
var AUTO_PATHS_OFFSET = 0.2;
|
|
2053
|
+
function toPercent(fraction) {
|
|
2054
|
+
return Math.round(fraction * 100);
|
|
2055
|
+
}
|
|
2056
|
+
function fromPercent(percent) {
|
|
2057
|
+
return percent / 100;
|
|
2058
|
+
}
|
|
2059
|
+
function Tick({
|
|
2060
|
+
percent,
|
|
2061
|
+
active,
|
|
2062
|
+
onSelect
|
|
2063
|
+
}) {
|
|
2064
|
+
const offsetPercent = tickOffsetPercent(percent);
|
|
2065
|
+
return /* @__PURE__ */ jsxs3(
|
|
2066
|
+
"button",
|
|
2067
|
+
{
|
|
2068
|
+
type: "button",
|
|
2069
|
+
"data-slot": "abstraction-controls-tick",
|
|
2070
|
+
"aria-pressed": active,
|
|
2071
|
+
onClick: () => onSelect(percent),
|
|
2072
|
+
style: { left: `${offsetPercent}%` },
|
|
2073
|
+
className: cn3(
|
|
2074
|
+
"focus-ring absolute top-0 rounded-sm text-meta text-muted-foreground transition-colors duration-fast ease-standard hover:text-foreground",
|
|
2075
|
+
offsetPercent <= 0 ? "translate-x-0" : offsetPercent >= 100 ? "-translate-x-full" : "-translate-x-1/2",
|
|
2076
|
+
active && "font-semibold text-foreground"
|
|
2077
|
+
),
|
|
2078
|
+
children: [
|
|
2079
|
+
percent,
|
|
2080
|
+
"%"
|
|
2081
|
+
]
|
|
2082
|
+
}
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
var AbstractionControls = forwardRef(
|
|
2086
|
+
function AbstractionControls2({
|
|
2087
|
+
abstraction,
|
|
2088
|
+
onAbstractionChange,
|
|
2089
|
+
graph,
|
|
2090
|
+
hiddenCounts,
|
|
2091
|
+
autoMaxActivities = 25,
|
|
2092
|
+
label,
|
|
2093
|
+
className,
|
|
2094
|
+
...props
|
|
2095
|
+
}, ref) {
|
|
2096
|
+
const { t } = useLocale2();
|
|
2097
|
+
const activitiesId = useId();
|
|
2098
|
+
const pathsId = useId();
|
|
2099
|
+
const activitiesPercent = toPercent(abstraction.activities);
|
|
2100
|
+
const pathsPercent = toPercent(abstraction.paths);
|
|
2101
|
+
const setActivitiesPercent = useCallback3(
|
|
2102
|
+
(percent) => onAbstractionChange({ activities: fromPercent(percent) }),
|
|
2103
|
+
[onAbstractionChange]
|
|
2104
|
+
);
|
|
2105
|
+
const setPathsPercent = useCallback3(
|
|
2106
|
+
(percent) => onAbstractionChange({ paths: fromPercent(percent) }),
|
|
2107
|
+
[onAbstractionChange]
|
|
2108
|
+
);
|
|
2109
|
+
const handleAuto = useCallback3(() => {
|
|
2110
|
+
const totalActivities = graph.activities.length + hiddenCounts.activities;
|
|
2111
|
+
const result = computeAutoAbstraction(totalActivities, {
|
|
2112
|
+
maxActivities: autoMaxActivities
|
|
2113
|
+
});
|
|
2114
|
+
onAbstractionChange({
|
|
2115
|
+
activities: result.activities,
|
|
2116
|
+
paths: Math.min(1, result.activities + AUTO_PATHS_OFFSET)
|
|
2117
|
+
});
|
|
2118
|
+
}, [graph.activities.length, hiddenCounts.activities, autoMaxActivities, onAbstractionChange]);
|
|
2119
|
+
const hiddenSummary = `${t("process.abstractionControls.hiddenActivities", {
|
|
2120
|
+
count: hiddenCounts.activities
|
|
2121
|
+
})} \xB7 ${t("process.abstractionControls.hiddenPaths", { count: hiddenCounts.paths })}`;
|
|
2122
|
+
return /* @__PURE__ */ jsxs3(
|
|
2123
|
+
"div",
|
|
2124
|
+
{
|
|
2125
|
+
ref,
|
|
2126
|
+
"data-slot": "abstraction-controls",
|
|
2127
|
+
role: "group",
|
|
2128
|
+
"aria-label": label ?? t("process.abstractionControls.label"),
|
|
2129
|
+
className: cn3("flex flex-col gap-4", className),
|
|
2130
|
+
...props,
|
|
2131
|
+
children: [
|
|
2132
|
+
/* @__PURE__ */ jsxs3(
|
|
2133
|
+
"div",
|
|
2134
|
+
{
|
|
2135
|
+
"data-slot": "abstraction-controls-activities",
|
|
2136
|
+
role: "group",
|
|
2137
|
+
"aria-labelledby": `${activitiesId}-label`,
|
|
2138
|
+
className: "flex flex-col gap-1.5",
|
|
2139
|
+
children: [
|
|
2140
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between gap-2", children: [
|
|
2141
|
+
/* @__PURE__ */ jsx4(Label, { id: `${activitiesId}-label`, htmlFor: activitiesId, className: "text-body", children: t("process.abstractionControls.activities") }),
|
|
2142
|
+
/* @__PURE__ */ jsxs3("span", { className: "text-meta text-muted-foreground tabular-nums", children: [
|
|
2143
|
+
activitiesPercent,
|
|
2144
|
+
"%"
|
|
2145
|
+
] })
|
|
2146
|
+
] }),
|
|
2147
|
+
/* @__PURE__ */ jsx4(
|
|
2148
|
+
Slider,
|
|
2149
|
+
{
|
|
2150
|
+
id: activitiesId,
|
|
2151
|
+
min: SLIDER_MIN,
|
|
2152
|
+
max: SLIDER_MAX,
|
|
2153
|
+
step: 1,
|
|
2154
|
+
value: [activitiesPercent],
|
|
2155
|
+
onValueChange: ([next]) => setActivitiesPercent(next ?? activitiesPercent),
|
|
2156
|
+
"aria-label": t("process.abstractionControls.activities")
|
|
2157
|
+
}
|
|
2158
|
+
),
|
|
2159
|
+
/* @__PURE__ */ jsx4("div", { className: "relative h-4", children: TICKS.map((percent) => /* @__PURE__ */ jsx4(
|
|
2160
|
+
Tick,
|
|
2161
|
+
{
|
|
2162
|
+
percent,
|
|
2163
|
+
active: activitiesPercent === percent,
|
|
2164
|
+
onSelect: setActivitiesPercent
|
|
2165
|
+
},
|
|
2166
|
+
percent
|
|
2167
|
+
)) })
|
|
2168
|
+
]
|
|
2169
|
+
}
|
|
2170
|
+
),
|
|
2171
|
+
/* @__PURE__ */ jsxs3(
|
|
2172
|
+
"div",
|
|
2173
|
+
{
|
|
2174
|
+
"data-slot": "abstraction-controls-paths",
|
|
2175
|
+
role: "group",
|
|
2176
|
+
"aria-labelledby": `${pathsId}-label`,
|
|
2177
|
+
className: "flex flex-col gap-1.5",
|
|
2178
|
+
children: [
|
|
2179
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between gap-2", children: [
|
|
2180
|
+
/* @__PURE__ */ jsx4(Label, { id: `${pathsId}-label`, htmlFor: pathsId, className: "text-body", children: t("process.abstractionControls.paths") }),
|
|
2181
|
+
/* @__PURE__ */ jsxs3("span", { className: "text-meta text-muted-foreground tabular-nums", children: [
|
|
2182
|
+
pathsPercent,
|
|
2183
|
+
"%"
|
|
2184
|
+
] })
|
|
2185
|
+
] }),
|
|
2186
|
+
/* @__PURE__ */ jsx4(
|
|
2187
|
+
Slider,
|
|
2188
|
+
{
|
|
2189
|
+
id: pathsId,
|
|
2190
|
+
min: SLIDER_MIN,
|
|
2191
|
+
max: SLIDER_MAX,
|
|
2192
|
+
step: 1,
|
|
2193
|
+
value: [pathsPercent],
|
|
2194
|
+
onValueChange: ([next]) => setPathsPercent(next ?? pathsPercent),
|
|
2195
|
+
"aria-label": t("process.abstractionControls.paths")
|
|
2196
|
+
}
|
|
2197
|
+
),
|
|
2198
|
+
/* @__PURE__ */ jsx4("div", { className: "relative h-4", children: TICKS.map((percent) => /* @__PURE__ */ jsx4(
|
|
2199
|
+
Tick,
|
|
2200
|
+
{
|
|
2201
|
+
percent,
|
|
2202
|
+
active: pathsPercent === percent,
|
|
2203
|
+
onSelect: setPathsPercent
|
|
2204
|
+
},
|
|
2205
|
+
percent
|
|
2206
|
+
)) })
|
|
2207
|
+
]
|
|
2208
|
+
}
|
|
2209
|
+
),
|
|
2210
|
+
/* @__PURE__ */ jsxs3(
|
|
2211
|
+
"div",
|
|
2212
|
+
{
|
|
2213
|
+
"data-slot": "abstraction-controls-footer",
|
|
2214
|
+
className: "flex items-center justify-between gap-3",
|
|
2215
|
+
children: [
|
|
2216
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
|
|
2217
|
+
/* @__PURE__ */ jsx4(
|
|
2218
|
+
Switch,
|
|
2219
|
+
{
|
|
2220
|
+
id: `${activitiesId}-invert`,
|
|
2221
|
+
checked: abstraction.invert ?? false,
|
|
2222
|
+
onCheckedChange: (checked) => onAbstractionChange({ invert: checked })
|
|
2223
|
+
}
|
|
2224
|
+
),
|
|
2225
|
+
/* @__PURE__ */ jsx4(Label, { htmlFor: `${activitiesId}-invert`, className: "text-body", children: t("process.abstractionControls.invert") })
|
|
2226
|
+
] }),
|
|
2227
|
+
/* @__PURE__ */ jsxs3(
|
|
2228
|
+
Button2,
|
|
2229
|
+
{
|
|
2230
|
+
type: "button",
|
|
2231
|
+
variant: "outline",
|
|
2232
|
+
size: "sm",
|
|
2233
|
+
onClick: handleAuto,
|
|
2234
|
+
"data-slot": "abstraction-controls-auto",
|
|
2235
|
+
children: [
|
|
2236
|
+
/* @__PURE__ */ jsx4(Sparkles, { "aria-hidden": "true" }),
|
|
2237
|
+
t("process.abstractionControls.auto")
|
|
2238
|
+
]
|
|
2239
|
+
}
|
|
2240
|
+
)
|
|
2241
|
+
]
|
|
2242
|
+
}
|
|
2243
|
+
),
|
|
2244
|
+
/* @__PURE__ */ jsx4(
|
|
2245
|
+
"p",
|
|
2246
|
+
{
|
|
2247
|
+
"data-slot": "abstraction-controls-hidden-summary",
|
|
2248
|
+
role: "status",
|
|
2249
|
+
"aria-live": "polite",
|
|
2250
|
+
className: "text-meta text-muted-foreground",
|
|
2251
|
+
children: hiddenSummary
|
|
2252
|
+
}
|
|
2253
|
+
)
|
|
2254
|
+
]
|
|
2255
|
+
}
|
|
2256
|
+
);
|
|
2257
|
+
}
|
|
2258
|
+
);
|
|
2259
|
+
|
|
2260
|
+
// src/metric-layer-switch/metric-layer-switch.tsx
|
|
2261
|
+
import { forwardRef as forwardRef2, useCallback as useCallback4, useId as useId2, useState as useState3 } from "react";
|
|
2262
|
+
import { Lock, LockOpen } from "lucide-react";
|
|
2263
|
+
import {
|
|
2264
|
+
Select,
|
|
2265
|
+
SelectContent,
|
|
2266
|
+
SelectItem,
|
|
2267
|
+
SelectTrigger,
|
|
2268
|
+
SelectValue,
|
|
2269
|
+
Toggle,
|
|
2270
|
+
ToggleGroup,
|
|
2271
|
+
ToggleGroupItem,
|
|
2272
|
+
useLocale as useLocale3
|
|
2273
|
+
} from "@elabs-ai/components-ui";
|
|
2274
|
+
import { cn as cn4 } from "@elabs-ai/components-ui/lib/cn";
|
|
2275
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2276
|
+
var FREQUENCY_NODE_OPTIONS = [
|
|
2277
|
+
"absolute",
|
|
2278
|
+
"absolute_case",
|
|
2279
|
+
"relative",
|
|
2280
|
+
"relative_case"
|
|
2281
|
+
];
|
|
2282
|
+
var FREQUENCY_EDGE_OPTIONS_LOCKED = FREQUENCY_NODE_OPTIONS;
|
|
2283
|
+
var FREQUENCY_EDGE_OPTIONS_UNLOCKED = [
|
|
2284
|
+
"absolute",
|
|
2285
|
+
"absolute_case",
|
|
2286
|
+
"relative",
|
|
2287
|
+
"relative_case",
|
|
2288
|
+
"relative_antecedent",
|
|
2289
|
+
"relative_consequent"
|
|
2290
|
+
];
|
|
2291
|
+
var PERFORMANCE_OPTIONS = [
|
|
2292
|
+
"median",
|
|
2293
|
+
"mean",
|
|
2294
|
+
"min",
|
|
2295
|
+
"max",
|
|
2296
|
+
"sum",
|
|
2297
|
+
"p90",
|
|
2298
|
+
"trimmed_mean"
|
|
2299
|
+
];
|
|
2300
|
+
var MetricLayerSwitch = forwardRef2(
|
|
2301
|
+
function MetricLayerSwitch2({
|
|
2302
|
+
layer,
|
|
2303
|
+
onLayerChange,
|
|
2304
|
+
metric,
|
|
2305
|
+
onMetricChange,
|
|
2306
|
+
locked: lockedProp,
|
|
2307
|
+
defaultLocked = true,
|
|
2308
|
+
onLockedChange,
|
|
2309
|
+
label,
|
|
2310
|
+
className,
|
|
2311
|
+
...props
|
|
2312
|
+
}, ref) {
|
|
2313
|
+
const { t } = useLocale3();
|
|
2314
|
+
const nodeId = useId2();
|
|
2315
|
+
const edgeId = useId2();
|
|
2316
|
+
const [uncontrolledLocked, setUncontrolledLocked] = useState3(defaultLocked);
|
|
2317
|
+
const locked = lockedProp ?? uncontrolledLocked;
|
|
2318
|
+
const setLocked = useCallback4(
|
|
2319
|
+
(next) => {
|
|
2320
|
+
if (lockedProp === void 0) setUncontrolledLocked(next);
|
|
2321
|
+
onLockedChange?.(next);
|
|
2322
|
+
},
|
|
2323
|
+
[lockedProp, onLockedChange]
|
|
2324
|
+
);
|
|
2325
|
+
const isRework = layer === "rework";
|
|
2326
|
+
const isPerformance = layer === "performance";
|
|
2327
|
+
const nodeOptions = isPerformance ? PERFORMANCE_OPTIONS : FREQUENCY_NODE_OPTIONS;
|
|
2328
|
+
const edgeOptions = isPerformance ? PERFORMANCE_OPTIONS : locked ? FREQUENCY_EDGE_OPTIONS_LOCKED : FREQUENCY_EDGE_OPTIONS_UNLOCKED;
|
|
2329
|
+
const handleLayerChange = useCallback4(
|
|
2330
|
+
(next) => {
|
|
2331
|
+
if (!next) return;
|
|
2332
|
+
const nextLayer = next;
|
|
2333
|
+
onLayerChange(nextLayer);
|
|
2334
|
+
if (nextLayer === "performance" && !isPerformanceMetric(metric.node)) {
|
|
2335
|
+
onMetricChange({ node: "median", edge: "median" });
|
|
2336
|
+
} else if (nextLayer === "frequency" && isPerformanceMetric(metric.node)) {
|
|
2337
|
+
onMetricChange({ node: "absolute", edge: "absolute" });
|
|
2338
|
+
}
|
|
2339
|
+
},
|
|
2340
|
+
[onLayerChange, onMetricChange, metric.node]
|
|
2341
|
+
);
|
|
2342
|
+
const handleNodeChange = useCallback4(
|
|
2343
|
+
(value) => {
|
|
2344
|
+
const next = value;
|
|
2345
|
+
onMetricChange(locked ? { node: next, edge: next } : { node: next });
|
|
2346
|
+
},
|
|
2347
|
+
[onMetricChange, locked]
|
|
2348
|
+
);
|
|
2349
|
+
const handleEdgeChange = useCallback4(
|
|
2350
|
+
(value) => {
|
|
2351
|
+
const next = value;
|
|
2352
|
+
onMetricChange(locked ? { node: next, edge: next } : { edge: next });
|
|
2353
|
+
},
|
|
2354
|
+
[onMetricChange, locked]
|
|
2355
|
+
);
|
|
2356
|
+
const handleLockToggle = useCallback4(
|
|
2357
|
+
(pressed) => {
|
|
2358
|
+
setLocked(pressed);
|
|
2359
|
+
if (pressed && metric.edge !== metric.node) {
|
|
2360
|
+
onMetricChange({ edge: metric.node });
|
|
2361
|
+
}
|
|
2362
|
+
},
|
|
2363
|
+
[setLocked, metric.edge, metric.node, onMetricChange]
|
|
2364
|
+
);
|
|
2365
|
+
return /* @__PURE__ */ jsxs4(
|
|
2366
|
+
"div",
|
|
2367
|
+
{
|
|
2368
|
+
ref,
|
|
2369
|
+
"data-slot": "metric-layer-switch",
|
|
2370
|
+
role: "group",
|
|
2371
|
+
"aria-label": label ?? t("process.metricLayerSwitch.label"),
|
|
2372
|
+
className: cn4("flex flex-col gap-3", className),
|
|
2373
|
+
...props,
|
|
2374
|
+
children: [
|
|
2375
|
+
/* @__PURE__ */ jsxs4(
|
|
2376
|
+
ToggleGroup,
|
|
2377
|
+
{
|
|
2378
|
+
type: "single",
|
|
2379
|
+
variant: "segmented",
|
|
2380
|
+
value: layer,
|
|
2381
|
+
onValueChange: handleLayerChange,
|
|
2382
|
+
"aria-label": t("process.metricLayerSwitch.layer"),
|
|
2383
|
+
children: [
|
|
2384
|
+
/* @__PURE__ */ jsx5(ToggleGroupItem, { value: "frequency", children: t("process.metricLayerSwitch.frequency") }),
|
|
2385
|
+
/* @__PURE__ */ jsx5(ToggleGroupItem, { value: "performance", children: t("process.metricLayerSwitch.performance") }),
|
|
2386
|
+
/* @__PURE__ */ jsx5(ToggleGroupItem, { value: "rework", children: t("process.metricLayerSwitch.rework") })
|
|
2387
|
+
]
|
|
2388
|
+
}
|
|
2389
|
+
),
|
|
2390
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-end gap-2", children: [
|
|
2391
|
+
/* @__PURE__ */ jsxs4(
|
|
2392
|
+
"div",
|
|
2393
|
+
{
|
|
2394
|
+
"data-slot": "metric-layer-switch-node",
|
|
2395
|
+
className: "flex min-w-0 flex-1 flex-col gap-1.5",
|
|
2396
|
+
children: [
|
|
2397
|
+
/* @__PURE__ */ jsx5("label", { htmlFor: nodeId, className: "text-meta text-muted-foreground", children: t("process.metricLayerSwitch.node") }),
|
|
2398
|
+
/* @__PURE__ */ jsxs4(Select, { value: metric.node, onValueChange: handleNodeChange, disabled: isRework, children: [
|
|
2399
|
+
/* @__PURE__ */ jsx5(SelectTrigger, { id: nodeId, "aria-label": t("process.metricLayerSwitch.node"), children: /* @__PURE__ */ jsx5(SelectValue, {}) }),
|
|
2400
|
+
/* @__PURE__ */ jsx5(SelectContent, { children: nodeOptions.map((option) => /* @__PURE__ */ jsx5(SelectItem, { value: option, children: nodeMetricLabel(option) }, option)) })
|
|
2401
|
+
] })
|
|
2402
|
+
]
|
|
2403
|
+
}
|
|
2404
|
+
),
|
|
2405
|
+
/* @__PURE__ */ jsx5(
|
|
2406
|
+
Toggle,
|
|
2407
|
+
{
|
|
2408
|
+
pressed: locked,
|
|
2409
|
+
onPressedChange: handleLockToggle,
|
|
2410
|
+
disabled: isRework,
|
|
2411
|
+
"aria-label": locked ? t("process.metricLayerSwitch.lockOn") : t("process.metricLayerSwitch.lockOff"),
|
|
2412
|
+
"data-slot": "metric-layer-switch-lock",
|
|
2413
|
+
children: locked ? /* @__PURE__ */ jsx5(Lock, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx5(LockOpen, { "aria-hidden": "true" })
|
|
2414
|
+
}
|
|
2415
|
+
),
|
|
2416
|
+
/* @__PURE__ */ jsxs4(
|
|
2417
|
+
"div",
|
|
2418
|
+
{
|
|
2419
|
+
"data-slot": "metric-layer-switch-edge",
|
|
2420
|
+
className: "flex min-w-0 flex-1 flex-col gap-1.5",
|
|
2421
|
+
children: [
|
|
2422
|
+
/* @__PURE__ */ jsx5("label", { htmlFor: edgeId, className: "text-meta text-muted-foreground", children: t("process.metricLayerSwitch.edge") }),
|
|
2423
|
+
/* @__PURE__ */ jsxs4(Select, { value: metric.edge, onValueChange: handleEdgeChange, disabled: isRework, children: [
|
|
2424
|
+
/* @__PURE__ */ jsx5(SelectTrigger, { id: edgeId, "aria-label": t("process.metricLayerSwitch.edge"), children: /* @__PURE__ */ jsx5(SelectValue, {}) }),
|
|
2425
|
+
/* @__PURE__ */ jsx5(SelectContent, { children: edgeOptions.map((option) => /* @__PURE__ */ jsx5(SelectItem, { value: option, children: edgeMetricLabel(option) }, option)) })
|
|
2426
|
+
] })
|
|
2427
|
+
]
|
|
2428
|
+
}
|
|
2429
|
+
)
|
|
2430
|
+
] })
|
|
2431
|
+
]
|
|
2432
|
+
}
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
);
|
|
2436
|
+
|
|
2437
|
+
// src/process-kpi-strip/process-kpi-strip.tsx
|
|
2438
|
+
import "react";
|
|
2439
|
+
import { CircleSlash2 } from "lucide-react";
|
|
2440
|
+
import { MetricCard } from "@elabs-ai/components-ui";
|
|
2441
|
+
import { useLocale as useLocale4 } from "@elabs-ai/components-ui";
|
|
2442
|
+
import { MetricGrid } from "@elabs-ai/components-charts";
|
|
2443
|
+
import { Sparkline } from "@elabs-ai/components-charts";
|
|
2444
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2445
|
+
function trendDirectionKey(first, last) {
|
|
2446
|
+
if (last > first) return "process.kpiStrip.trendRising";
|
|
2447
|
+
if (last < first) return "process.kpiStrip.trendFalling";
|
|
2448
|
+
return "process.kpiStrip.trendSteady";
|
|
2449
|
+
}
|
|
2450
|
+
function formatTrendValue(formatNumber, value, format) {
|
|
2451
|
+
switch (format) {
|
|
2452
|
+
case "duration":
|
|
2453
|
+
return formatDurationMs(value);
|
|
2454
|
+
case "percent":
|
|
2455
|
+
return formatNumber(value, { style: "percent", maximumFractionDigits: 1 });
|
|
2456
|
+
case "count":
|
|
2457
|
+
default:
|
|
2458
|
+
return formatNumber(value);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
function trendVisual(values, subject, format, t, formatNumber) {
|
|
2462
|
+
if (!values || values.length === 0) return void 0;
|
|
2463
|
+
const first = values[0];
|
|
2464
|
+
const last = values[values.length - 1];
|
|
2465
|
+
const label = t("process.kpiStrip.trendAlt", {
|
|
2466
|
+
subject,
|
|
2467
|
+
periods: values.length,
|
|
2468
|
+
direction: t(trendDirectionKey(first, last)),
|
|
2469
|
+
first: formatTrendValue(formatNumber, first, format),
|
|
2470
|
+
last: formatTrendValue(formatNumber, last, format)
|
|
2471
|
+
});
|
|
2472
|
+
return /* @__PURE__ */ jsx6(Sparkline, { values, label });
|
|
2473
|
+
}
|
|
2474
|
+
function ProcessKpiStrip({
|
|
2475
|
+
kpis,
|
|
2476
|
+
conformance,
|
|
2477
|
+
trends,
|
|
2478
|
+
loading = false,
|
|
2479
|
+
className,
|
|
2480
|
+
...props
|
|
2481
|
+
}) {
|
|
2482
|
+
const { t, formatNumber } = useLocale4();
|
|
2483
|
+
const hasConformance = conformance !== null && conformance !== void 0;
|
|
2484
|
+
const conformanceHint = t("process.kpiStrip.conformanceUnavailableHint");
|
|
2485
|
+
return /* @__PURE__ */ jsx6("div", { "data-slot": "process-kpi-strip", className, ...props, children: /* @__PURE__ */ jsxs5(MetricGrid, { columns: 3, loading, children: [
|
|
2486
|
+
/* @__PURE__ */ jsx6(
|
|
2487
|
+
MetricCard,
|
|
2488
|
+
{
|
|
2489
|
+
label: t("process.kpiStrip.cases"),
|
|
2490
|
+
value: kpis.cases,
|
|
2491
|
+
announceLoading: false,
|
|
2492
|
+
visual: trendVisual(trends?.cases, t("process.kpiStrip.cases"), "count", t, formatNumber)
|
|
2493
|
+
}
|
|
2494
|
+
),
|
|
2495
|
+
/* @__PURE__ */ jsx6(
|
|
2496
|
+
MetricCard,
|
|
2497
|
+
{
|
|
2498
|
+
label: t("process.kpiStrip.events"),
|
|
2499
|
+
value: kpis.events,
|
|
2500
|
+
announceLoading: false,
|
|
2501
|
+
visual: trendVisual(
|
|
2502
|
+
trends?.events,
|
|
2503
|
+
t("process.kpiStrip.events"),
|
|
2504
|
+
"count",
|
|
2505
|
+
t,
|
|
2506
|
+
formatNumber
|
|
2507
|
+
)
|
|
2508
|
+
}
|
|
2509
|
+
),
|
|
2510
|
+
/* @__PURE__ */ jsx6(
|
|
2511
|
+
MetricCard,
|
|
2512
|
+
{
|
|
2513
|
+
label: t("process.kpiStrip.variants"),
|
|
2514
|
+
value: kpis.variants,
|
|
2515
|
+
announceLoading: false,
|
|
2516
|
+
visual: trendVisual(
|
|
2517
|
+
trends?.variants,
|
|
2518
|
+
t("process.kpiStrip.variants"),
|
|
2519
|
+
"count",
|
|
2520
|
+
t,
|
|
2521
|
+
formatNumber
|
|
2522
|
+
)
|
|
2523
|
+
}
|
|
2524
|
+
),
|
|
2525
|
+
/* @__PURE__ */ jsx6(
|
|
2526
|
+
MetricCard,
|
|
2527
|
+
{
|
|
2528
|
+
label: t("process.kpiStrip.medianThroughput"),
|
|
2529
|
+
value: formatDurationMs(kpis.medianThroughput),
|
|
2530
|
+
announceLoading: false,
|
|
2531
|
+
visual: trendVisual(
|
|
2532
|
+
trends?.medianThroughput,
|
|
2533
|
+
t("process.kpiStrip.medianThroughput"),
|
|
2534
|
+
"duration",
|
|
2535
|
+
t,
|
|
2536
|
+
formatNumber
|
|
2537
|
+
)
|
|
2538
|
+
}
|
|
2539
|
+
),
|
|
2540
|
+
/* @__PURE__ */ jsx6(
|
|
2541
|
+
MetricCard,
|
|
2542
|
+
{
|
|
2543
|
+
label: t("process.kpiStrip.reworkRate"),
|
|
2544
|
+
value: kpis.reworkRate,
|
|
2545
|
+
valueFormat: "percent",
|
|
2546
|
+
announceLoading: false,
|
|
2547
|
+
visual: trendVisual(
|
|
2548
|
+
trends?.reworkRate,
|
|
2549
|
+
t("process.kpiStrip.reworkRate"),
|
|
2550
|
+
"percent",
|
|
2551
|
+
t,
|
|
2552
|
+
formatNumber
|
|
2553
|
+
)
|
|
2554
|
+
}
|
|
2555
|
+
),
|
|
2556
|
+
/* @__PURE__ */ jsx6(
|
|
2557
|
+
MetricCard,
|
|
2558
|
+
{
|
|
2559
|
+
label: t("process.kpiStrip.conformance"),
|
|
2560
|
+
value: hasConformance ? conformance : /* @__PURE__ */ jsxs5(
|
|
2561
|
+
"span",
|
|
2562
|
+
{
|
|
2563
|
+
"data-slot": "process-kpi-strip-conformance-unavailable",
|
|
2564
|
+
className: "text-title inline-flex items-center gap-1.5 text-muted-foreground",
|
|
2565
|
+
children: [
|
|
2566
|
+
/* @__PURE__ */ jsx6(CircleSlash2, { "aria-hidden": "true", className: "size-5" }),
|
|
2567
|
+
t("process.kpiStrip.conformanceUnavailable")
|
|
2568
|
+
]
|
|
2569
|
+
}
|
|
2570
|
+
),
|
|
2571
|
+
valueFormat: hasConformance ? "percent" : void 0,
|
|
2572
|
+
description: hasConformance ? (
|
|
2573
|
+
// Same slot, same height, every state (F3) — invisible rather than absent, so
|
|
2574
|
+
// the tile's box never reflows depending on whether conformance was measured.
|
|
2575
|
+
/* @__PURE__ */ jsx6("span", { "aria-hidden": "true", className: "invisible", children: conformanceHint })
|
|
2576
|
+
) : conformanceHint,
|
|
2577
|
+
announceLoading: false,
|
|
2578
|
+
visual: hasConformance ? trendVisual(
|
|
2579
|
+
trends?.conformance,
|
|
2580
|
+
t("process.kpiStrip.conformance"),
|
|
2581
|
+
"percent",
|
|
2582
|
+
t,
|
|
2583
|
+
formatNumber
|
|
2584
|
+
) : void 0
|
|
2585
|
+
}
|
|
2586
|
+
)
|
|
2587
|
+
] }) });
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
// src/use-process-explorer/use-process-explorer.ts
|
|
2591
|
+
import { useCallback as useCallback5, useEffect as useEffect3, useMemo as useMemo5, useRef as useRef3, useState as useState4 } from "react";
|
|
2592
|
+
|
|
2593
|
+
// src/core/filter-log.ts
|
|
2594
|
+
function sequenceOf2(kase) {
|
|
2595
|
+
const sequence = new Array(kase.events.length);
|
|
2596
|
+
for (let i = 0; i < kase.events.length; i += 1) {
|
|
2597
|
+
sequence[i] = kase.events[i].activity;
|
|
2598
|
+
}
|
|
2599
|
+
return sequence;
|
|
2600
|
+
}
|
|
2601
|
+
function valuesFor(kase, key) {
|
|
2602
|
+
const attributes = kase.attributes;
|
|
2603
|
+
if (attributes !== void 0 && Object.hasOwn(attributes, key)) return [attributes[key]];
|
|
2604
|
+
const values = [];
|
|
2605
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2606
|
+
for (const event of kase.events) {
|
|
2607
|
+
const value = key === "resource" ? event.resource : event.attributes?.[key];
|
|
2608
|
+
if (value === void 0) continue;
|
|
2609
|
+
if (seen.has(value)) continue;
|
|
2610
|
+
seen.add(value);
|
|
2611
|
+
values.push(value);
|
|
2612
|
+
}
|
|
2613
|
+
return values;
|
|
2614
|
+
}
|
|
2615
|
+
function ordered(left, right, wantGreater) {
|
|
2616
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
2617
|
+
return wantGreater ? left > right : left < right;
|
|
2618
|
+
}
|
|
2619
|
+
if (typeof left === "string" && typeof right === "string") {
|
|
2620
|
+
return wantGreater ? left > right : left < right;
|
|
2621
|
+
}
|
|
2622
|
+
return false;
|
|
2623
|
+
}
|
|
2624
|
+
function matchesAttribute(kase, spec) {
|
|
2625
|
+
const values = valuesFor(kase, spec.key);
|
|
2626
|
+
switch (spec.op) {
|
|
2627
|
+
case "eq":
|
|
2628
|
+
return values.some((value) => Object.is(value, spec.value));
|
|
2629
|
+
// `ne` is the negation of `eq`, not "some value differs" — otherwise a case offering
|
|
2630
|
+
// two resources would satisfy both `eq` and `ne` against the same value.
|
|
2631
|
+
case "ne":
|
|
2632
|
+
return !values.some((value) => Object.is(value, spec.value));
|
|
2633
|
+
case "gt":
|
|
2634
|
+
return values.some((value) => ordered(value, spec.value, true));
|
|
2635
|
+
case "lt":
|
|
2636
|
+
return values.some((value) => ordered(value, spec.value, false));
|
|
2637
|
+
case "in": {
|
|
2638
|
+
if (!Array.isArray(spec.value)) return false;
|
|
2639
|
+
const allowed = spec.value;
|
|
2640
|
+
return values.some((value) => allowed.some((candidate) => Object.is(value, candidate)));
|
|
2641
|
+
}
|
|
2642
|
+
default:
|
|
2643
|
+
return false;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
function matchesOne(kase, sequence, spec) {
|
|
2647
|
+
switch (spec.kind) {
|
|
2648
|
+
case "with":
|
|
2649
|
+
return sequence.includes(spec.activity);
|
|
2650
|
+
case "without":
|
|
2651
|
+
return !sequence.includes(spec.activity);
|
|
2652
|
+
case "startsWith":
|
|
2653
|
+
return sequence[0] === spec.activity;
|
|
2654
|
+
case "endsWith":
|
|
2655
|
+
return sequence[sequence.length - 1] === spec.activity;
|
|
2656
|
+
case "follower": {
|
|
2657
|
+
if (spec.direct === true) {
|
|
2658
|
+
for (let i = 0; i + 1 < sequence.length; i += 1) {
|
|
2659
|
+
if (sequence[i] === spec.a && sequence[i + 1] === spec.b) return true;
|
|
2660
|
+
}
|
|
2661
|
+
return false;
|
|
2662
|
+
}
|
|
2663
|
+
const first = sequence.indexOf(spec.a);
|
|
2664
|
+
if (first < 0) return false;
|
|
2665
|
+
return sequence.indexOf(spec.b, first + 1) >= 0;
|
|
2666
|
+
}
|
|
2667
|
+
case "attribute":
|
|
2668
|
+
return matchesAttribute(kase, spec);
|
|
2669
|
+
case "duration": {
|
|
2670
|
+
if (spec.min === void 0 && spec.max === void 0) return true;
|
|
2671
|
+
if (!Number.isFinite(kase.duration)) return false;
|
|
2672
|
+
if (spec.min !== void 0 && kase.duration < spec.min) return false;
|
|
2673
|
+
if (spec.max !== void 0 && kase.duration > spec.max) return false;
|
|
2674
|
+
return true;
|
|
2675
|
+
}
|
|
2676
|
+
case "variant":
|
|
2677
|
+
return spec.ids.includes(variantId(sequence));
|
|
2678
|
+
case "cases":
|
|
2679
|
+
return spec.ids.includes(kase.caseId);
|
|
2680
|
+
default:
|
|
2681
|
+
return true;
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
function caseMatchesFilters(kase, specs) {
|
|
2685
|
+
if (specs.length === 0) return true;
|
|
2686
|
+
const sequence = sequenceOf2(kase);
|
|
2687
|
+
for (const spec of specs) {
|
|
2688
|
+
if (!matchesOne(kase, sequence, spec)) return false;
|
|
2689
|
+
}
|
|
2690
|
+
return true;
|
|
2691
|
+
}
|
|
2692
|
+
function filterNormalizedLog(log, specs) {
|
|
2693
|
+
const normalized = asNormalizedLog(log);
|
|
2694
|
+
if (specs.length === 0) return normalized;
|
|
2695
|
+
const cases = [];
|
|
2696
|
+
let events = 0;
|
|
2697
|
+
for (const kase of normalized.cases) {
|
|
2698
|
+
if (!caseMatchesFilters(kase, specs)) continue;
|
|
2699
|
+
cases.push(kase);
|
|
2700
|
+
events += kase.events.length;
|
|
2701
|
+
}
|
|
2702
|
+
return { cases, totals: { cases: cases.length, events } };
|
|
2703
|
+
}
|
|
2704
|
+
function filterLog(log, specs) {
|
|
2705
|
+
if (specs.length === 0) return log;
|
|
2706
|
+
const kept = /* @__PURE__ */ new Set();
|
|
2707
|
+
for (const kase of filterNormalizedLog(log, specs).cases) kept.add(kase.caseId);
|
|
2708
|
+
const filtered = { events: log.events.filter((row) => kept.has(row?.caseId)) };
|
|
2709
|
+
if (log.caseAttributes !== void 0) {
|
|
2710
|
+
const caseAttributes = {};
|
|
2711
|
+
for (const caseId of Object.keys(log.caseAttributes)) {
|
|
2712
|
+
if (kept.has(caseId)) {
|
|
2713
|
+
caseAttributes[caseId] = log.caseAttributes[caseId];
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
filtered.caseAttributes = caseAttributes;
|
|
2717
|
+
}
|
|
2718
|
+
return filtered;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
// src/core/reconcile-graph.ts
|
|
2722
|
+
function transitionKey(transition) {
|
|
2723
|
+
return `${transition.source}${EDGE_KEY_SEPARATOR}${transition.target}`;
|
|
2724
|
+
}
|
|
2725
|
+
function reconcileGraph(presented, filtered) {
|
|
2726
|
+
const filteredActivities = new Map(
|
|
2727
|
+
filtered.activities.map((activity) => [activity.id, activity])
|
|
2728
|
+
);
|
|
2729
|
+
const filteredTransitions = new Map(
|
|
2730
|
+
filtered.transitions.map((transition) => [transitionKey(transition), transition])
|
|
2731
|
+
);
|
|
2732
|
+
const excludedActivities = [];
|
|
2733
|
+
const activities = presented.activities.map((activity) => {
|
|
2734
|
+
const survivor = filteredActivities.get(activity.id);
|
|
2735
|
+
if (survivor) return survivor;
|
|
2736
|
+
excludedActivities.push(activity.id);
|
|
2737
|
+
return {
|
|
2738
|
+
...activity,
|
|
2739
|
+
instances: 0,
|
|
2740
|
+
cases: 0,
|
|
2741
|
+
isStart: false,
|
|
2742
|
+
isEnd: false,
|
|
2743
|
+
duration: emptyDurationStats()
|
|
2744
|
+
};
|
|
2745
|
+
});
|
|
2746
|
+
const excludedTransitions = [];
|
|
2747
|
+
const transitions = presented.transitions.map((transition) => {
|
|
2748
|
+
const key = transitionKey(transition);
|
|
2749
|
+
const survivor = filteredTransitions.get(key);
|
|
2750
|
+
if (survivor) return survivor;
|
|
2751
|
+
excludedTransitions.push(key);
|
|
2752
|
+
return {
|
|
2753
|
+
...transition,
|
|
2754
|
+
count: 0,
|
|
2755
|
+
caseCount: 0,
|
|
2756
|
+
duration: emptyDurationStats()
|
|
2757
|
+
};
|
|
2758
|
+
});
|
|
2759
|
+
const graph = {
|
|
2760
|
+
...presented,
|
|
2761
|
+
activities,
|
|
2762
|
+
transitions,
|
|
2763
|
+
startActivities: filtered.startActivities,
|
|
2764
|
+
endActivities: filtered.endActivities,
|
|
2765
|
+
totals: filtered.totals
|
|
2766
|
+
};
|
|
2767
|
+
return { graph, excludedActivities, excludedTransitions };
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// src/core/worker/process-worker.ts
|
|
2771
|
+
function handleProcessRequest(request) {
|
|
2772
|
+
try {
|
|
2773
|
+
if (request.kind === "discover") {
|
|
2774
|
+
return {
|
|
2775
|
+
id: request.id,
|
|
2776
|
+
ok: true,
|
|
2777
|
+
kind: "discover",
|
|
2778
|
+
graph: discoverGraph(request.log, request.options ?? {})
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
return { id: request.id, ok: true, kind: "variants", variants: extractVariants(request.log) };
|
|
2782
|
+
} catch (error) {
|
|
2783
|
+
return { id: request.id, ok: false, error: describe(error) };
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
function describe(error) {
|
|
2787
|
+
if (error instanceof Error) return error.message;
|
|
2788
|
+
return String(error);
|
|
2789
|
+
}
|
|
2790
|
+
function inWorkerScope() {
|
|
2791
|
+
const scope = globalThis;
|
|
2792
|
+
const constructor = scope.WorkerGlobalScope;
|
|
2793
|
+
if (typeof constructor !== "function") return false;
|
|
2794
|
+
return scope.self instanceof constructor;
|
|
2795
|
+
}
|
|
2796
|
+
if (inWorkerScope()) {
|
|
2797
|
+
const scope = self;
|
|
2798
|
+
scope.addEventListener("message", (event) => {
|
|
2799
|
+
scope.postMessage(handleProcessRequest(event.data));
|
|
2800
|
+
});
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
// src/core/worker/create-process-worker.ts
|
|
2804
|
+
function workerConstructible() {
|
|
2805
|
+
return typeof Worker !== "undefined" && typeof URL !== "undefined";
|
|
2806
|
+
}
|
|
2807
|
+
function createProcessWorker(options = {}) {
|
|
2808
|
+
const construct = options.createWorker;
|
|
2809
|
+
let inline = options.forceInline === true || construct === void 0 && !workerConstructible();
|
|
2810
|
+
let terminated = false;
|
|
2811
|
+
let worker;
|
|
2812
|
+
let nextId = 0;
|
|
2813
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2814
|
+
function degrade() {
|
|
2815
|
+
inline = true;
|
|
2816
|
+
const stale = [...pending.values()];
|
|
2817
|
+
pending.clear();
|
|
2818
|
+
if (worker !== void 0) {
|
|
2819
|
+
const dying = worker;
|
|
2820
|
+
worker = void 0;
|
|
2821
|
+
try {
|
|
2822
|
+
dying.terminate();
|
|
2823
|
+
} catch {
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
for (const entry of stale) entry.settle(handleProcessRequest(entry.request));
|
|
2827
|
+
}
|
|
2828
|
+
function onMessage(event) {
|
|
2829
|
+
const response = event.data;
|
|
2830
|
+
if (response === void 0 || typeof response.id !== "number") return;
|
|
2831
|
+
const entry = pending.get(response.id);
|
|
2832
|
+
if (entry === void 0) return;
|
|
2833
|
+
pending.delete(response.id);
|
|
2834
|
+
entry.settle(response);
|
|
2835
|
+
}
|
|
2836
|
+
function ensureWorker() {
|
|
2837
|
+
if (inline || terminated) return void 0;
|
|
2838
|
+
if (worker !== void 0) return worker;
|
|
2839
|
+
try {
|
|
2840
|
+
worker = construct === void 0 ? new Worker(new URL("./core/process-worker.js", import.meta.url), {
|
|
2841
|
+
type: "module"
|
|
2842
|
+
}) : construct();
|
|
2843
|
+
} catch {
|
|
2844
|
+
degrade();
|
|
2845
|
+
return void 0;
|
|
2846
|
+
}
|
|
2847
|
+
worker.addEventListener("message", onMessage);
|
|
2848
|
+
worker.addEventListener("error", degrade);
|
|
2849
|
+
worker.addEventListener("messageerror", degrade);
|
|
2850
|
+
return worker;
|
|
2851
|
+
}
|
|
2852
|
+
function send(build) {
|
|
2853
|
+
if (terminated) return Promise.reject(new Error("process worker terminated"));
|
|
2854
|
+
nextId += 1;
|
|
2855
|
+
const request = build(nextId);
|
|
2856
|
+
const active = ensureWorker();
|
|
2857
|
+
if (active === void 0) {
|
|
2858
|
+
return Promise.resolve().then(() => handleProcessRequest(request));
|
|
2859
|
+
}
|
|
2860
|
+
return new Promise((resolve, reject) => {
|
|
2861
|
+
pending.set(request.id, { request, settle: resolve });
|
|
2862
|
+
try {
|
|
2863
|
+
active.postMessage(request);
|
|
2864
|
+
} catch {
|
|
2865
|
+
pending.delete(request.id);
|
|
2866
|
+
degrade();
|
|
2867
|
+
try {
|
|
2868
|
+
resolve(handleProcessRequest(request));
|
|
2869
|
+
} catch (error) {
|
|
2870
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
function unwrap(response, read) {
|
|
2876
|
+
if (!response.ok) throw new Error(response.error);
|
|
2877
|
+
return read(response);
|
|
2878
|
+
}
|
|
2879
|
+
return {
|
|
2880
|
+
async discover(log, discoverOptions) {
|
|
2881
|
+
const response = await send(
|
|
2882
|
+
(id) => discoverOptions === void 0 ? { id, kind: "discover", log } : { id, kind: "discover", log, options: discoverOptions }
|
|
2883
|
+
);
|
|
2884
|
+
return unwrap(response, (ok) => ok.graph);
|
|
2885
|
+
},
|
|
2886
|
+
async variants(log) {
|
|
2887
|
+
const response = await send((id) => ({ id, kind: "variants", log }));
|
|
2888
|
+
return unwrap(response, (ok) => ok.variants);
|
|
2889
|
+
},
|
|
2890
|
+
terminate() {
|
|
2891
|
+
terminated = true;
|
|
2892
|
+
const stale = [...pending.values()];
|
|
2893
|
+
pending.clear();
|
|
2894
|
+
for (const entry of stale) {
|
|
2895
|
+
entry.settle({ id: entry.request.id, ok: false, error: "process worker terminated" });
|
|
2896
|
+
}
|
|
2897
|
+
if (worker !== void 0) {
|
|
2898
|
+
const dying = worker;
|
|
2899
|
+
worker = void 0;
|
|
2900
|
+
try {
|
|
2901
|
+
dying.terminate();
|
|
2902
|
+
} catch {
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
},
|
|
2906
|
+
get inline() {
|
|
2907
|
+
return inline;
|
|
2908
|
+
}
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
// src/use-process-explorer/use-process-explorer.ts
|
|
2913
|
+
var DEFAULT_ABSTRACTION = {
|
|
2914
|
+
activities: 1,
|
|
2915
|
+
paths: 1,
|
|
2916
|
+
invert: false,
|
|
2917
|
+
keepConnected: true
|
|
2918
|
+
};
|
|
2919
|
+
var DEFAULT_METRIC = { node: "absolute", edge: "absolute" };
|
|
2920
|
+
var DEFAULT_WORKER_THRESHOLD = 5e4;
|
|
2921
|
+
var EMPTY_GRAPH = {
|
|
2922
|
+
activities: [],
|
|
2923
|
+
transitions: [],
|
|
2924
|
+
startActivities: {},
|
|
2925
|
+
endActivities: {},
|
|
2926
|
+
totals: { cases: 0, events: 0, variants: 0 }
|
|
2927
|
+
};
|
|
2928
|
+
function discoverInline(log) {
|
|
2929
|
+
return { graph: discoverGraph(log), variants: extractVariants(log) };
|
|
2930
|
+
}
|
|
2931
|
+
function countDistinctVariants(normalized) {
|
|
2932
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2933
|
+
for (const kase of normalized.cases) {
|
|
2934
|
+
keys.add(variantKey(kase.events.map((event) => event.activity)));
|
|
2935
|
+
}
|
|
2936
|
+
return keys.size;
|
|
2937
|
+
}
|
|
2938
|
+
function useLogDiscovery(targetLog, workerThreshold, getHandle) {
|
|
2939
|
+
const useWorkerPath = targetLog !== null && targetLog.events.length > workerThreshold;
|
|
2940
|
+
const syncResult = useMemo5(
|
|
2941
|
+
() => targetLog === null || useWorkerPath ? null : discoverInline(targetLog),
|
|
2942
|
+
[targetLog, useWorkerPath]
|
|
2943
|
+
);
|
|
2944
|
+
const [asyncState, setAsyncState] = useState4(
|
|
2945
|
+
null
|
|
2946
|
+
);
|
|
2947
|
+
const [loading, setLoading] = useState4(false);
|
|
2948
|
+
const requestIdRef = useRef3(0);
|
|
2949
|
+
useEffect3(() => {
|
|
2950
|
+
const requestId = requestIdRef.current += 1;
|
|
2951
|
+
if (targetLog === null || !useWorkerPath) {
|
|
2952
|
+
setLoading(false);
|
|
2953
|
+
return;
|
|
2954
|
+
}
|
|
2955
|
+
setLoading(true);
|
|
2956
|
+
const handle = getHandle();
|
|
2957
|
+
Promise.all([handle.discover(targetLog), handle.variants(targetLog)]).then(([graph, variants]) => {
|
|
2958
|
+
if (requestIdRef.current !== requestId) return;
|
|
2959
|
+
setAsyncState({ log: targetLog, result: { graph, variants } });
|
|
2960
|
+
setLoading(false);
|
|
2961
|
+
}).catch(() => {
|
|
2962
|
+
if (requestIdRef.current !== requestId) return;
|
|
2963
|
+
setAsyncState({ log: targetLog, result: discoverInline(targetLog) });
|
|
2964
|
+
setLoading(false);
|
|
2965
|
+
});
|
|
2966
|
+
}, [targetLog, useWorkerPath]);
|
|
2967
|
+
const asyncResult = asyncState !== null && asyncState.log === targetLog ? asyncState.result : null;
|
|
2968
|
+
const targetUnsettled = useWorkerPath && (asyncState === null || asyncState.log !== targetLog);
|
|
2969
|
+
return {
|
|
2970
|
+
result: syncResult ?? asyncResult ?? { graph: EMPTY_GRAPH, variants: [] },
|
|
2971
|
+
loading: loading || targetUnsettled,
|
|
2972
|
+
settled: syncResult !== null || asyncResult !== null
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2975
|
+
function useProcessExplorer(log, opts = {}) {
|
|
2976
|
+
const workerThreshold = opts.workerThreshold ?? DEFAULT_WORKER_THRESHOLD;
|
|
2977
|
+
const workerOptionsRef = useRef3(opts.worker);
|
|
2978
|
+
workerOptionsRef.current = opts.worker;
|
|
2979
|
+
const handleRef = useRef3(null);
|
|
2980
|
+
function getHandle() {
|
|
2981
|
+
if (handleRef.current === null) {
|
|
2982
|
+
handleRef.current = createProcessWorker(workerOptionsRef.current);
|
|
2983
|
+
}
|
|
2984
|
+
return handleRef.current;
|
|
2985
|
+
}
|
|
2986
|
+
useEffect3(
|
|
2987
|
+
() => () => {
|
|
2988
|
+
handleRef.current?.terminate();
|
|
2989
|
+
},
|
|
2990
|
+
[]
|
|
2991
|
+
);
|
|
2992
|
+
const [abstraction, setAbstractionState] = useState4(() => ({
|
|
2993
|
+
...DEFAULT_ABSTRACTION,
|
|
2994
|
+
...opts.abstraction
|
|
2995
|
+
}));
|
|
2996
|
+
const [metric, setMetricState] = useState4(() => ({
|
|
2997
|
+
...DEFAULT_METRIC,
|
|
2998
|
+
...opts.metric
|
|
2999
|
+
}));
|
|
3000
|
+
const [layer, setLayerState] = useState4(opts.layer ?? "frequency");
|
|
3001
|
+
const [selection, setSelection] = useState4(null);
|
|
3002
|
+
const [intents, setIntents] = useState4([]);
|
|
3003
|
+
const setAbstraction = useCallback5((next) => {
|
|
3004
|
+
setAbstractionState((prev) => ({ ...prev, ...next }));
|
|
3005
|
+
}, []);
|
|
3006
|
+
const setMetric = useCallback5((next) => {
|
|
3007
|
+
setMetricState((prev) => ({ ...prev, ...next }));
|
|
3008
|
+
}, []);
|
|
3009
|
+
const setLayer = useCallback5((next) => setLayerState(next), []);
|
|
3010
|
+
const onSelect = useCallback5((next) => setSelection(next), []);
|
|
3011
|
+
const applyIntent = useCallback5((intent) => {
|
|
3012
|
+
setIntents((prev) => [...prev, intent]);
|
|
3013
|
+
}, []);
|
|
3014
|
+
const clearIntent = useCallback5((index2) => {
|
|
3015
|
+
setIntents((prev) => prev.filter((_, i) => i !== index2));
|
|
3016
|
+
}, []);
|
|
3017
|
+
const filteredLog = useMemo5(
|
|
3018
|
+
() => intents.length === 0 ? log : filterLog(log, intents),
|
|
3019
|
+
[log, intents]
|
|
3020
|
+
);
|
|
3021
|
+
const sameLog = filteredLog === log;
|
|
3022
|
+
const fullDiscovery = useLogDiscovery(log, workerThreshold, getHandle);
|
|
3023
|
+
const filteredOwnDiscovery = useLogDiscovery(
|
|
3024
|
+
sameLog ? null : filteredLog,
|
|
3025
|
+
workerThreshold,
|
|
3026
|
+
getHandle
|
|
3027
|
+
);
|
|
3028
|
+
const filteredDiscovery = sameLog || !filteredOwnDiscovery.settled ? fullDiscovery : filteredOwnDiscovery;
|
|
3029
|
+
const loading = fullDiscovery.loading || filteredOwnDiscovery.loading;
|
|
3030
|
+
const presented = useMemo5(
|
|
3031
|
+
() => abstractGraph(fullDiscovery.result.graph, abstraction),
|
|
3032
|
+
[fullDiscovery.result.graph, abstraction]
|
|
3033
|
+
);
|
|
3034
|
+
const reconciled = useMemo5(
|
|
3035
|
+
() => reconcileGraph(presented, filteredDiscovery.result.graph),
|
|
3036
|
+
[presented, filteredDiscovery.result.graph]
|
|
3037
|
+
);
|
|
3038
|
+
const graph = reconciled.graph;
|
|
3039
|
+
const selectionStates = useMemo5(
|
|
3040
|
+
() => ({
|
|
3041
|
+
activities: Object.fromEntries(
|
|
3042
|
+
reconciled.excludedActivities.map((id) => [id, "excluded"])
|
|
3043
|
+
),
|
|
3044
|
+
transitions: Object.fromEntries(
|
|
3045
|
+
reconciled.excludedTransitions.map((key) => [key, "excluded"])
|
|
3046
|
+
),
|
|
3047
|
+
// Decision §1.4 step 5 (RM-052 round 3, #227, G2): there is no click channel for a
|
|
3048
|
+
// variant, so `"selected"` here is intent-derived — every id named by an active
|
|
3049
|
+
// `{ kind: "variant" }` intent, read by `VariantExplorer` (RM-054), not by
|
|
3050
|
+
// `ProcessMap`. This namespace was declared on `ProcessSelectionStates` from round 2
|
|
3051
|
+
// onward and never populated until this fix.
|
|
3052
|
+
variants: Object.fromEntries(
|
|
3053
|
+
intents.flatMap((intent) => intent.kind === "variant" ? intent.ids : []).map((id) => [id, "selected"])
|
|
3054
|
+
)
|
|
3055
|
+
}),
|
|
3056
|
+
[reconciled, intents]
|
|
3057
|
+
);
|
|
3058
|
+
const excludedCounts = useMemo5(
|
|
3059
|
+
() => ({
|
|
3060
|
+
activities: reconciled.excludedActivities.length,
|
|
3061
|
+
paths: reconciled.excludedTransitions.length
|
|
3062
|
+
}),
|
|
3063
|
+
[reconciled]
|
|
3064
|
+
);
|
|
3065
|
+
const rework = useMemo5(() => detectRework(filteredLog), [filteredLog]);
|
|
3066
|
+
const kpis = useMemo5(() => {
|
|
3067
|
+
const normalized = asNormalizedLog(filteredLog);
|
|
3068
|
+
const medianThroughput = durationStats(normalized.cases.map((kase) => kase.duration)).median;
|
|
3069
|
+
return {
|
|
3070
|
+
cases: normalized.totals.cases,
|
|
3071
|
+
events: normalized.totals.events,
|
|
3072
|
+
variants: countDistinctVariants(normalized),
|
|
3073
|
+
medianThroughput,
|
|
3074
|
+
reworkRate: rework.caseReworkRate
|
|
3075
|
+
};
|
|
3076
|
+
}, [filteredLog, rework]);
|
|
3077
|
+
return {
|
|
3078
|
+
graph,
|
|
3079
|
+
variants: filteredDiscovery.result.variants,
|
|
3080
|
+
abstraction,
|
|
3081
|
+
setAbstraction,
|
|
3082
|
+
metric,
|
|
3083
|
+
setMetric,
|
|
3084
|
+
layer,
|
|
3085
|
+
setLayer,
|
|
3086
|
+
selection,
|
|
3087
|
+
onSelect,
|
|
3088
|
+
applyIntent,
|
|
3089
|
+
clearIntent,
|
|
3090
|
+
intents,
|
|
3091
|
+
filteredLog,
|
|
3092
|
+
selectionStates,
|
|
3093
|
+
hiddenCounts: graph.hidden,
|
|
3094
|
+
excludedCounts,
|
|
3095
|
+
kpis,
|
|
3096
|
+
rework,
|
|
3097
|
+
loading
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
export {
|
|
3101
|
+
AbstractionControls,
|
|
3102
|
+
DEFAULT_LAYOUT_DEBOUNCE_MS,
|
|
3103
|
+
EMPTY_PROCESS_MAP_HOVER,
|
|
3104
|
+
GHOST_OPACITY,
|
|
3105
|
+
MetricLayerSwitch,
|
|
3106
|
+
PROCESS_FILTER_INTENT_KINDS,
|
|
3107
|
+
PROCESS_FILTER_INTENT_LABELS,
|
|
3108
|
+
PROCESS_FILTER_INTENT_MESSAGE_KEYS,
|
|
3109
|
+
PROCESS_MAP_EDGE_SCALE_GROUP,
|
|
3110
|
+
PROCESS_MAP_LEGIBLE_ZOOM,
|
|
3111
|
+
PROCESS_MAP_NODE_MOTION_CLASS,
|
|
3112
|
+
PROCESS_SELECTION_STATE_MESSAGE_KEYS,
|
|
3113
|
+
ProcessActivityNode,
|
|
3114
|
+
ProcessKpiStrip,
|
|
3115
|
+
ProcessMap,
|
|
3116
|
+
ProcessMapEdgeKeyContext,
|
|
3117
|
+
ProcessMapHoverContext,
|
|
3118
|
+
ProcessTransitionEdge,
|
|
3119
|
+
activityAriaLabel,
|
|
3120
|
+
activityMetricValue,
|
|
3121
|
+
activityRole,
|
|
3122
|
+
applyLayoutSnapshot,
|
|
3123
|
+
buildProcessMapModel,
|
|
3124
|
+
computeAutoAbstraction,
|
|
3125
|
+
edgeMetricLabel,
|
|
3126
|
+
formatDurationMs,
|
|
3127
|
+
formatMetricValue,
|
|
3128
|
+
isPerformanceMetric,
|
|
3129
|
+
nodeMetricLabel,
|
|
3130
|
+
processEdgeDenominators,
|
|
3131
|
+
processEdgeId,
|
|
3132
|
+
processGraphStructureKey,
|
|
3133
|
+
resolveActivityFrequencyMode,
|
|
3134
|
+
resolveSelectionState,
|
|
3135
|
+
resolveTransitionFrequencyMode,
|
|
3136
|
+
selectionNeighbourhood,
|
|
3137
|
+
selectionStateLabel,
|
|
3138
|
+
transitionAriaLabel,
|
|
3139
|
+
transitionMetricValue,
|
|
3140
|
+
transitionShape,
|
|
3141
|
+
useProcessExplorer,
|
|
3142
|
+
useProcessLayout,
|
|
3143
|
+
useProcessMapEdgeKeys,
|
|
3144
|
+
useProcessMapHover
|
|
3145
|
+
};
|
|
3146
|
+
//# sourceMappingURL=index.js.map
|