@ocelescope/filter 0.10.2 → 0.11.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +0 -2
  2. package/dist/index.js +1792 -647
  3. package/package.json +28 -21
package/dist/index.js CHANGED
@@ -1,22 +1,25 @@
1
- import { ActivityPicker, AttributesTable, ObjectTypePicker, RelationTable, defineModule, defineModuleRoute, useCurrentOcel, useInvalidate } from "@ocelescope/core";
2
- import { CheckIcon, FilterIcon, PlusIcon, RotateCcwIcon, Trash2Icon, XIcon } from "lucide-react";
3
- import { ActionIcon, ActionIconGroup, Box, Grid, Group, InlineStyles, LoadingOverlay, NumberInput, RangeSlider, Scroller, Stack, Switch, Tabs, Text, TextInput, useMantineTheme } from "@mantine/core";
1
+ import { ActivityPicker, AttributesTable, BarList, ObjectTypePicker, OptionPicker, defineModule, defineModuleRoute, useCurrentOcel, useInvalidate } from "@ocelescope/core";
2
+ import { ArrowLeftIcon, CheckIcon, FilterIcon, GripVerticalIcon, InfoIcon, PlusIcon, RotateCcwIcon, Trash2Icon, XIcon } from "lucide-react";
3
+ import { ActionIcon, Badge, Box, Button, Center, Divider, Grid, Group, InlineStyles, Loader, LoadingOverlay, Menu, NumberInput, Paper, Popover, RangeSlider, ScrollArea, SegmentedControl, Select, SimpleGrid, Slider, Stack, Switch, Text, TextInput, Title, Tooltip, UnstyledButton, useMantineTheme } from "@mantine/core";
4
4
  import hash from "object-hash";
5
- import { useMutation, useQuery } from "@tanstack/react-query";
5
+ import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query";
6
6
  import { customFetch } from "@ocelescope/api-client";
7
- import { Controller, Watch, useFieldArray, useForm } from "react-hook-form";
8
- import { useRouter } from "next/router.js";
9
- import { memo, useMemo, useState } from "react";
10
- import { DateTimePicker } from "@mantine/dates";
7
+ import { memo, useEffect, useMemo, useRef, useState } from "react";
8
+ import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form";
9
+ import { useAttributeDistribution, useAttributeValues, useE2o, useE2oCountDistribution, useEventAttributes, useEventCounts, useO2o, useO2oCountDistribution, useObjectAttributes, useObjectCounts, useTimeInfo } from "@ocelescope/api-base";
11
10
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
- import { useTimeInfo } from "@ocelescope/api-base";
13
- import EChartsReact from "echarts-for-react";
14
11
  import dayjs from "dayjs";
15
12
  import isBetween from "dayjs/plugin/isBetween.js";
16
- //#region src/lib/fetcher.ts
13
+ import { DateTimePicker } from "@mantine/dates";
14
+ import EChartsReact from "echarts-for-react";
15
+ import { useDebouncedValue } from "@mantine/hooks";
16
+ import dynamic from "next/dynamic.js";
17
+ import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
18
+ import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
19
+ import { CSS } from "@dnd-kit/utilities";
20
+ // src/lib/fetcher.ts
17
21
  const customFetch$1 = async (config, options) => customFetch(config, options);
18
- //#endregion
19
- //#region src/api/base.ts
22
+ // src/api/base.ts
20
23
  /**
21
24
  * Generated by orval 🍺
22
25
  * Do not edit manually.
@@ -36,6 +39,47 @@ const withQueryKey = (query, queryKey) => {
36
39
  return result;
37
40
  };
38
41
  /**
42
+ * @summary Register View
43
+ */
44
+ const registerView = (ocelId, timeFrameFilterActivityFilterObjectTypeFilterEventAttributeFilterObjectAttributeFilterE2OCountFilterO2OCountFilter, params, options, signal) => {
45
+ return customFetch$1({
46
+ url: `/api/external/modules/filter/v1/${ocelId}/views`,
47
+ method: "POST",
48
+ headers: { "Content-Type": "application/json" },
49
+ data: timeFrameFilterActivityFilterObjectTypeFilterEventAttributeFilterObjectAttributeFilterE2OCountFilterO2OCountFilter,
50
+ params,
51
+ signal
52
+ }, options);
53
+ };
54
+ const getRegisterViewQueryKey = (ocelId, timeFrameFilterActivityFilterObjectTypeFilterEventAttributeFilterObjectAttributeFilterE2OCountFilterO2OCountFilter, params) => {
55
+ return [
56
+ "POST",
57
+ `/api/external/modules/filter/v1/${ocelId}/views`,
58
+ ...params ? [params] : [],
59
+ timeFrameFilterActivityFilterObjectTypeFilterEventAttributeFilterObjectAttributeFilterE2OCountFilterO2OCountFilter
60
+ ];
61
+ };
62
+ const getRegisterViewQueryOptions = (ocelId, timeFrameFilterActivityFilterObjectTypeFilterEventAttributeFilterObjectAttributeFilterE2OCountFilterO2OCountFilter, params, options) => {
63
+ const { query: queryOptions, request: requestOptions } = options ?? {};
64
+ const queryKey = queryOptions?.queryKey ?? getRegisterViewQueryKey(ocelId, timeFrameFilterActivityFilterObjectTypeFilterEventAttributeFilterObjectAttributeFilterE2OCountFilterO2OCountFilter, params);
65
+ const queryFn = ({ signal }) => registerView(ocelId, timeFrameFilterActivityFilterObjectTypeFilterEventAttributeFilterObjectAttributeFilterE2OCountFilterO2OCountFilter, params, requestOptions, signal);
66
+ return {
67
+ queryKey,
68
+ queryFn,
69
+ enabled: ocelId !== null && ocelId !== void 0,
70
+ staleTime: 3e5,
71
+ ...queryOptions
72
+ };
73
+ };
74
+ /**
75
+ * @summary Register View
76
+ */
77
+ function useRegisterView(ocelId, timeFrameFilterActivityFilterObjectTypeFilterEventAttributeFilterObjectAttributeFilterE2OCountFilterO2OCountFilter, params, options, queryClient) {
78
+ const queryOptions = getRegisterViewQueryOptions(ocelId, timeFrameFilterActivityFilterObjectTypeFilterEventAttributeFilterObjectAttributeFilterE2OCountFilterO2OCountFilter, params, options);
79
+ const query = useQuery(queryOptions, queryClient);
80
+ return withQueryKey(query, queryOptions.queryKey);
81
+ }
82
+ /**
39
83
  * @summary Getfilter
40
84
  */
41
85
  const getFilter = (ocelId, options, signal) => {
@@ -108,479 +152,1317 @@ const getSetFilterMutationOptions = (options) => {
108
152
  const useSetFilter = (options, queryClient) => {
109
153
  return useMutation(getSetFilterMutationOptions(options), queryClient);
110
154
  };
111
- //#endregion
112
- //#region src/hooks/useSearchParams.tsx
113
- const useCurrentFilterTab = ({ defaultTab, queryKey = "tab", tabs }) => {
114
- const router = useRouter();
115
- const currentTab = useMemo(() => {
116
- const raw = router.query[queryKey];
117
- const tab = typeof raw === "string" ? raw : defaultTab;
118
- return tabs.includes(tab) ? tab : defaultTab;
119
- }, [
120
- router.query,
121
- queryKey,
122
- defaultTab
123
- ]);
124
- const setCurrentTab = (tab) => {
125
- router.replace({
126
- pathname: router.pathname,
127
- query: {
128
- ...router.query,
129
- [queryKey]: tab
155
+ // src/hooks/useAttributeFilterColumns.tsx
156
+ const getInitialCondition = (attribute) => {
157
+ switch (attribute.type) {
158
+ case "int": return { number_range: [Number.parseInt(`${attribute.min}`, 10), Number.parseInt(`${attribute.max}`, 10)] };
159
+ case "float": return { number_range: [Number.parseFloat(`${attribute.min}`), Number.parseFloat(`${attribute.max}`)] };
160
+ case "date": return { time_range: [String(attribute.min), String(attribute.max)] };
161
+ default: return {};
162
+ }
163
+ };
164
+ const getEntityType = (attribute) => {
165
+ if (!("entity_type_names" in attribute)) return attribute.entity_type;
166
+ return attribute.entity_type_names.length === 1 ? attribute.entity_type_names[0] : void 0;
167
+ };
168
+ const useAttributeFilter = ({ filter, onChange }) => {
169
+ return { filterColumns: useMemo(() => [{
170
+ accessor: "action",
171
+ title: "",
172
+ width: "100px",
173
+ render: (record) => /* @__PURE__ */ jsx(Button, {
174
+ size: "compact-sm",
175
+ variant: "default",
176
+ onClick: (event) => {
177
+ event.stopPropagation();
178
+ onChange({
179
+ type: filter.type,
180
+ attribute: record.name,
181
+ target_type: getEntityType(record),
182
+ ...getInitialCondition(record)
183
+ });
184
+ },
185
+ children: "Use"
186
+ })
187
+ }], [filter.type, onChange]) };
188
+ };
189
+ // src/hooks/useView.ts
190
+ const ORIGINAL = { ocel_version: "original" };
191
+ const useView = (ocelId, steps) => {
192
+ const { data } = useRegisterView(ocelId, steps, void 0, { query: {
193
+ placeholderData: keepPreviousData,
194
+ staleTime: Infinity
195
+ } });
196
+ if (data === void 0) return;
197
+ return data === null ? ORIGINAL : { view_id: data };
198
+ };
199
+ const viewProps = (view) => "view_id" in view ? { viewId: view.view_id } : { ocelVersion: "original" };
200
+ // src/util/dayjs.ts
201
+ dayjs.extend(isBetween);
202
+ var dayjs_default = dayjs;
203
+ // src/components/Previewed.tsx
204
+ /** Renders ``children`` with the preview data once it is there. */
205
+ const Previewed = ({ query, children }) => {
206
+ if (query.data !== void 0) return children(query.data);
207
+ if (query.error) return /* @__PURE__ */ jsx(Text, {
208
+ c: "red",
209
+ size: "sm",
210
+ children: "Could not load the log as the steps before leave it."
211
+ });
212
+ return /* @__PURE__ */ jsx(Center, {
213
+ flex: 1,
214
+ mih: 120,
215
+ children: /* @__PURE__ */ jsx(Loader, { size: "sm" })
216
+ });
217
+ };
218
+ // src/components/FilterViews/AttributeFilter/util.ts
219
+ const DATE_FORMAT$1 = "YYYY-MM-DD HH:mm";
220
+ const formatValue = (value, type) => {
221
+ if (type === "date") return dayjs_default(value).format(DATE_FORMAT$1);
222
+ if (type === "float" && typeof value === "number") return value.toLocaleString(void 0, { maximumFractionDigits: 2 });
223
+ return `${value}`;
224
+ };
225
+ // src/components/RangeView.tsx
226
+ const Histogram = ({ bins, value: [low, high], format, onSelect, exclude }) => {
227
+ const theme = useMantineTheme();
228
+ const option = useMemo(() => ({
229
+ tooltip: {
230
+ trigger: "axis",
231
+ axisPointer: { type: "shadow" },
232
+ formatter: (params) => {
233
+ const [param] = Array.isArray(params) ? params : [params];
234
+ const bin = bins[param?.dataIndex ?? 0];
235
+ if (!bin) return "";
236
+ return `${bin.start === bin.end ? format(bin.start) : `${format(bin.start)} – ${format(bin.end)}`}<br/><b>${bin.count.toLocaleString()}</b> values`;
130
237
  }
131
- }, void 0, { shallow: true });
132
- };
133
- return {
134
- currentTab,
135
- setCurrentTab
238
+ },
239
+ grid: {
240
+ left: 8,
241
+ right: 8,
242
+ top: 8,
243
+ bottom: 24,
244
+ containLabel: true
245
+ },
246
+ xAxis: {
247
+ type: "category",
248
+ data: bins.map(({ start }) => format(start)),
249
+ axisTick: { show: false },
250
+ axisLabel: { hideOverlap: true }
251
+ },
252
+ yAxis: {
253
+ type: "value",
254
+ splitNumber: 3
255
+ },
256
+ series: [{
257
+ type: "bar",
258
+ barCategoryGap: "8%",
259
+ data: bins.map(({ start, end, count }) => {
260
+ const isKept = exclude ? start < low || end > high : end >= low && start <= high;
261
+ return {
262
+ value: count,
263
+ itemStyle: {
264
+ color: theme.colors.blue[6],
265
+ opacity: isKept ? 1 : .25
266
+ }
267
+ };
268
+ })
269
+ }]
270
+ }), [
271
+ bins,
272
+ low,
273
+ high,
274
+ format,
275
+ exclude,
276
+ theme
277
+ ]);
278
+ return /* @__PURE__ */ jsx(EChartsReact, {
279
+ option,
280
+ style: { height: 260 },
281
+ onEvents: { click: ({ dataIndex }) => {
282
+ const bin = bins[dataIndex];
283
+ if (bin) onSelect([bin.start, bin.end]);
284
+ } }
285
+ });
286
+ };
287
+ const STEPS = 1e3;
288
+ const RangeSelector = ({ min, max, value: [low, high], onChange, format, snap = (value) => value, exclude }) => {
289
+ const span = max - min;
290
+ const toPosition = (value) => Math.round((value - min) / span * STEPS);
291
+ const toValue = (position) => {
292
+ if (position <= 0) return min;
293
+ if (position >= STEPS) return max;
294
+ return snap(min + position / STEPS * span);
136
295
  };
296
+ return /* @__PURE__ */ jsx(RangeSlider, {
297
+ px: "xs",
298
+ min: 0,
299
+ max: STEPS,
300
+ minRange: 0,
301
+ inverted: exclude,
302
+ value: [toPosition(low), toPosition(high)],
303
+ label: (position) => format(toValue(position)),
304
+ onChange: ([start, end]) => onChange([toValue(start), toValue(end)])
305
+ });
137
306
  };
138
- //#endregion
139
- //#region src/components/inputs/AttributeInputField.tsx
140
- const StringFilterField = memo(function StringFilterField({ control, name }) {
141
- return /* @__PURE__ */ jsx(Controller, {
142
- name,
143
- control,
144
- render: ({ field }) => /* @__PURE__ */ jsx(TextInput, {
145
- value: field.value ?? "",
146
- onChange: field.onChange,
147
- placeholder: "Enter regex"
307
+ const RangeView = ({ bins, min, max, value, onChange, format, snap, exclude = false, inputs }) => {
308
+ const [low, high] = value;
309
+ return /* @__PURE__ */ jsxs(Stack, {
310
+ gap: "sm",
311
+ children: [
312
+ /* @__PURE__ */ jsx(Histogram, {
313
+ bins,
314
+ value,
315
+ format,
316
+ onSelect: onChange,
317
+ exclude
318
+ }),
319
+ max > min && /* @__PURE__ */ jsx(RangeSelector, {
320
+ min,
321
+ max,
322
+ value,
323
+ onChange,
324
+ format,
325
+ snap,
326
+ exclude
327
+ }),
328
+ /* @__PURE__ */ jsxs(Group, {
329
+ align: "end",
330
+ wrap: "nowrap",
331
+ children: [/* @__PURE__ */ jsx(Box, {
332
+ flex: 1,
333
+ children: inputs
334
+ }), /* @__PURE__ */ jsx(Button, {
335
+ variant: "default",
336
+ onClick: () => onChange([min, max]),
337
+ disabled: low <= min && high >= max,
338
+ children: "Reset"
339
+ })]
340
+ }),
341
+ /* @__PURE__ */ jsx(Text, {
342
+ size: "xs",
343
+ c: "dimmed",
344
+ children: "Click a bar to set the range to it."
345
+ })
346
+ ]
347
+ });
348
+ };
349
+ // src/components/FilterViews/AttributeFilter/views/RangeView/DateRangeView.tsx
350
+ const PICKER_FORMAT = "YYYY-MM-DD HH:mm:ss";
351
+ const toTime = (value) => dayjs_default(value).valueOf();
352
+ const toDate = (value) => dayjs_default(value).format(PICKER_FORMAT);
353
+ const formatTime = (value) => dayjs_default(value).format("YYYY-MM-DD HH:mm");
354
+ const DateRangeView = ({ data, filter, onChange }) => {
355
+ const min = toTime(data.min);
356
+ const max = toTime(data.max);
357
+ const bins = useMemo(() => (data.bins ?? []).map(({ start, end, count }) => ({
358
+ start: toTime(start),
359
+ end: toTime(end),
360
+ count
361
+ })), [data.bins]);
362
+ const [start, end] = filter.time_range ?? [null, null];
363
+ const value = [start != null ? toTime(start) : min, end != null ? toTime(end) : max];
364
+ const setRange = ([low, high]) => onChange({
365
+ ...filter,
366
+ time_range: [toDate(low), toDate(high)]
367
+ });
368
+ return /* @__PURE__ */ jsx(RangeView, {
369
+ bins,
370
+ min,
371
+ max,
372
+ value,
373
+ onChange: setRange,
374
+ exclude: filter.mode === "exclude",
375
+ format: formatTime,
376
+ inputs: /* @__PURE__ */ jsxs(Group, {
377
+ wrap: "nowrap",
378
+ align: "end",
379
+ gap: "xs",
380
+ children: [
381
+ /* @__PURE__ */ jsx(DateTimePicker, {
382
+ flex: 1,
383
+ withSeconds: true,
384
+ value: toDate(value[0]),
385
+ minDate: toDate(min),
386
+ maxDate: toDate(value[1]),
387
+ onChange: (next) => setRange([next ? toTime(next) : min, value[1]])
388
+ }),
389
+ /* @__PURE__ */ jsx(Text, {
390
+ c: "dimmed",
391
+ size: "sm",
392
+ pb: 8,
393
+ children: "-"
394
+ }),
395
+ /* @__PURE__ */ jsx(DateTimePicker, {
396
+ flex: 1,
397
+ withSeconds: true,
398
+ value: toDate(value[1]),
399
+ minDate: toDate(value[0]),
400
+ maxDate: toDate(max),
401
+ onChange: (next) => setRange([value[0], next ? toTime(next) : max])
402
+ })
403
+ ]
148
404
  })
149
405
  });
150
- });
151
- const NumberFilterField = memo(function NumberFilterField({ control, name, min, max, allowDecimals }) {
152
- return /* @__PURE__ */ jsx(Controller, {
153
- name,
154
- control,
155
- render: ({ field }) => {
156
- const currentMin = typeof field.value?.[0] === "number" ? field.value[0] : void 0;
157
- const currentMax = typeof field.value?.[1] === "number" ? field.value[1] : void 0;
158
- return /* @__PURE__ */ jsxs(Group, {
159
- wrap: "nowrap",
160
- align: "end",
161
- gap: "xs",
406
+ };
407
+ // src/components/FilterViews/AttributeFilter/views/RangeView/NumberRangeView.tsx
408
+ const formatInt = (value) => value.toLocaleString();
409
+ const formatFloat = (value) => value.toLocaleString(void 0, { maximumFractionDigits: 2 });
410
+ const NumberRangeView = ({ data, filter, onChange }) => {
411
+ const isInt = data.type === "int";
412
+ const min = Number(data.min);
413
+ const max = Number(data.max);
414
+ const bins = useMemo(() => (data.bins ?? []).map(({ start, end, count }, index, all) => ({
415
+ start: Number(start),
416
+ end: isInt && index < all.length - 1 ? Number(end) - 1 : Number(end),
417
+ count
418
+ })), [data.bins, isInt]);
419
+ const [low, high] = filter.number_range ?? [null, null];
420
+ const value = [low ?? min, high ?? max];
421
+ const setRange = (number_range) => onChange({
422
+ ...filter,
423
+ number_range
424
+ });
425
+ const toNumber = (next) => typeof next === "number" ? next : void 0;
426
+ return /* @__PURE__ */ jsx(RangeView, {
427
+ bins,
428
+ min,
429
+ max,
430
+ value,
431
+ onChange: setRange,
432
+ exclude: filter.mode === "exclude",
433
+ format: isInt ? formatInt : formatFloat,
434
+ snap: isInt ? Math.round : void 0,
435
+ inputs: /* @__PURE__ */ jsxs(Group, {
436
+ wrap: "nowrap",
437
+ align: "end",
438
+ gap: "xs",
439
+ children: [
440
+ /* @__PURE__ */ jsx(NumberInput, {
441
+ flex: 1,
442
+ value: value[0],
443
+ min,
444
+ max: value[1],
445
+ allowDecimal: !isInt,
446
+ onChange: (next) => setRange([toNumber(next) ?? min, value[1]])
447
+ }),
448
+ /* @__PURE__ */ jsx(Text, {
449
+ c: "dimmed",
450
+ size: "sm",
451
+ pb: 8,
452
+ children: "-"
453
+ }),
454
+ /* @__PURE__ */ jsx(NumberInput, {
455
+ flex: 1,
456
+ value: value[1],
457
+ min: value[0],
458
+ max,
459
+ allowDecimal: !isInt,
460
+ onChange: (next) => setRange([value[0], toNumber(next) ?? max])
461
+ })
462
+ ]
463
+ })
464
+ });
465
+ };
466
+ // src/components/FilterViews/AttributeFilter/views/ValuesView/PatternView.tsx
467
+ const PatternView = ({ data, filter, onChange, ocelId, entityType, before }) => {
468
+ const [pattern] = useDebouncedValue(filter.regex ?? "", 300);
469
+ const exclude = filter.mode === "exclude";
470
+ const view = useView(ocelId, before);
471
+ const matches = useAttributeValues(ocelId, {
472
+ ...view,
473
+ attribute: filter.attribute,
474
+ regex: pattern,
475
+ entity_type: entityType,
476
+ entity_names: filter.target_type ? [filter.target_type] : void 0
477
+ }, { query: {
478
+ enabled: view !== void 0 && pattern !== "",
479
+ placeholderData: keepPreviousData,
480
+ retry: false
481
+ } });
482
+ const isEmpty = pattern === "";
483
+ const matched = isEmpty ? data.count : matches.data?.count ?? 0;
484
+ const values = isEmpty ? data.values ?? [] : matches.data?.values ?? [];
485
+ const distinct = isEmpty ? data.distinct_values : matches.data?.distinct_values ?? 0;
486
+ const kept = exclude ? data.count - matched : matched;
487
+ return /* @__PURE__ */ jsxs(Stack, {
488
+ gap: "sm",
489
+ flex: 1,
490
+ mih: 0,
491
+ children: [
492
+ /* @__PURE__ */ jsx(TextInput, {
493
+ label: "Regular expression",
494
+ placeholder: "e.g. ^Warehouse",
495
+ value: filter.regex ?? "",
496
+ error: !isEmpty && matches.error ? "Invalid pattern" : void 0,
497
+ rightSection: matches.isFetching ? /* @__PURE__ */ jsx(Loader, { size: "xs" }) : void 0,
498
+ onChange: (event) => onChange({
499
+ ...filter,
500
+ regex: event.currentTarget.value
501
+ })
502
+ }),
503
+ /* @__PURE__ */ jsxs(Group, {
504
+ justify: "space-between",
505
+ children: [/* @__PURE__ */ jsx(Text, {
506
+ size: "sm",
507
+ fw: 500,
508
+ children: isEmpty ? "Most frequent values" : `${matched.toLocaleString()} values match (${distinct.toLocaleString()} distinct)`
509
+ }), /* @__PURE__ */ jsxs(Text, {
510
+ size: "sm",
511
+ c: "dimmed",
512
+ children: [
513
+ "Keeping ",
514
+ kept.toLocaleString(),
515
+ " of ",
516
+ data.count.toLocaleString(),
517
+ " ",
518
+ "values"
519
+ ]
520
+ })]
521
+ }),
522
+ values.length > 0 ? /* @__PURE__ */ jsx(BarList, {
523
+ data: Object.fromEntries(values.map(({ value, count }) => [value, count])),
524
+ labelHeader: isEmpty ? "Value" : "Match",
525
+ maxVisibleItems: values.length,
526
+ style: {
527
+ flex: 1,
528
+ minHeight: 0
529
+ }
530
+ }) : !isEmpty && !matches.isFetching && /* @__PURE__ */ jsx(Text, {
531
+ size: "sm",
532
+ c: "dimmed",
533
+ ta: "center",
534
+ py: "md",
535
+ children: "No value matches the pattern."
536
+ }),
537
+ values.length < distinct && /* @__PURE__ */ jsxs(Text, {
538
+ size: "xs",
539
+ c: "dimmed",
162
540
  children: [
163
- /* @__PURE__ */ jsx(NumberInput, {
164
- value: currentMin,
165
- min,
166
- max: currentMax ?? max,
167
- onChange: (nextMin) => {
168
- field.onChange([nextMin, currentMax]);
169
- },
170
- allowDecimal: allowDecimals,
171
- style: { flex: 1 }
172
- }),
173
- /* @__PURE__ */ jsx(Text, {
174
- c: "dimmed",
175
- size: "sm",
176
- pb: 8,
177
- children: "-"
178
- }),
179
- /* @__PURE__ */ jsx(NumberInput, {
180
- value: currentMax,
181
- min: currentMin ?? min,
182
- max,
183
- allowDecimal: allowDecimals,
184
- onChange: (nextMax) => {
185
- field.onChange([currentMin, nextMax]);
186
- },
187
- style: { flex: 1 }
188
- })
541
+ "Showing the ",
542
+ values.length,
543
+ " most frequent of",
544
+ " ",
545
+ distinct.toLocaleString(),
546
+ " distinct values."
189
547
  ]
190
- });
191
- }
548
+ })
549
+ ]
550
+ });
551
+ };
552
+ // src/components/FilterViews/AttributeFilter/views/ValuesView/ValuePicker.tsx
553
+ const ValuePicker = ({ data, filter, onChange, ocelId, entityType, before }) => {
554
+ const theme = useMantineTheme();
555
+ const [search, setSearch] = useState("");
556
+ const [debounced] = useDebouncedValue(search.trim(), 300);
557
+ const view = useView(ocelId, before);
558
+ const found = useAttributeValues(ocelId, {
559
+ ...view,
560
+ attribute: filter.attribute,
561
+ search: debounced,
562
+ entity_type: entityType,
563
+ entity_names: filter.target_type ? [filter.target_type] : void 0
564
+ }, { query: {
565
+ enabled: view !== void 0 && debounced !== "",
566
+ placeholderData: keepPreviousData
567
+ } });
568
+ const isSearching = debounced !== "";
569
+ const values = isSearching ? found.data?.values ?? [] : data.values ?? [];
570
+ const total = isSearching ? found.data?.distinct_values ?? 0 : data.distinct_values;
571
+ const options = useMemo(() => values.map(({ value, count }) => ({
572
+ value,
573
+ label: value,
574
+ count,
575
+ color: theme.colors.blue[6]
576
+ })), [values, theme]);
577
+ const selected = (filter.values ?? []).map(String);
578
+ const exclude = filter.mode === "exclude";
579
+ const matched = useAttributeValues(ocelId, {
580
+ ...view,
581
+ attribute: filter.attribute,
582
+ values: selected,
583
+ entity_type: entityType,
584
+ entity_names: filter.target_type ? [filter.target_type] : void 0
585
+ }, { query: {
586
+ enabled: view !== void 0 && selected.length > 0,
587
+ placeholderData: keepPreviousData
588
+ } }).data?.count ?? 0;
589
+ const kept = selected.length === 0 ? exclude ? 0 : data.count : exclude ? data.count - matched : matched;
590
+ return /* @__PURE__ */ jsxs(Stack, {
591
+ gap: "sm",
592
+ children: [
593
+ /* @__PURE__ */ jsxs(Text, {
594
+ size: "sm",
595
+ c: "dimmed",
596
+ children: [
597
+ "Keeping ",
598
+ kept.toLocaleString(),
599
+ " of ",
600
+ data.count.toLocaleString(),
601
+ " values"
602
+ ]
603
+ }),
604
+ /* @__PURE__ */ jsx(OptionPicker, {
605
+ variant: "list",
606
+ multiple: true,
607
+ counts: true,
608
+ bars: true,
609
+ cutoff: true,
610
+ sort: "count",
611
+ options,
612
+ loading: found.isFetching,
613
+ search,
614
+ onSearch: setSearch,
615
+ total,
616
+ value: selected,
617
+ onChange: (next) => onChange({
618
+ ...filter,
619
+ values: next.length ? next : null
620
+ })
621
+ }),
622
+ values.length < total && /* @__PURE__ */ jsxs(Text, {
623
+ size: "xs",
624
+ c: "dimmed",
625
+ children: [
626
+ "Showing the ",
627
+ values.length,
628
+ " most frequent of ",
629
+ total.toLocaleString(),
630
+ " ",
631
+ "values. Search to find the others."
632
+ ]
633
+ }),
634
+ selected.length === 0 && /* @__PURE__ */ jsxs(Text, {
635
+ size: "xs",
636
+ c: "dimmed",
637
+ children: [
638
+ "No value picked: every value is ",
639
+ exclude ? "removed" : "kept",
640
+ "."
641
+ ]
642
+ })
643
+ ]
192
644
  });
645
+ };
646
+ // src/components/FilterViews/AttributeFilter/views/ValuesView/ValuesView.tsx
647
+ const PICKER_OPTIONS = [{
648
+ label: "Pick values",
649
+ value: "values"
650
+ }, {
651
+ label: "Pattern",
652
+ value: "pattern"
653
+ }];
654
+ const ValuesView = (props) => {
655
+ const { filter, onChange } = props;
656
+ const picker = filter.regex != null ? "pattern" : "values";
657
+ return /* @__PURE__ */ jsxs(Stack, {
658
+ gap: "sm",
659
+ flex: 1,
660
+ mih: 0,
661
+ children: [/* @__PURE__ */ jsx(SegmentedControl, {
662
+ size: "xs",
663
+ w: "fit-content",
664
+ value: picker,
665
+ data: PICKER_OPTIONS,
666
+ onChange: (next) => onChange({
667
+ ...filter,
668
+ values: null,
669
+ regex: next === "pattern" ? "" : null
670
+ })
671
+ }), picker === "pattern" ? /* @__PURE__ */ jsx(PatternView, { ...props }) : /* @__PURE__ */ jsx(ValuePicker, { ...props })]
672
+ });
673
+ };
674
+ // src/components/FilterViews/AttributeFilter/AttributeDetail.tsx
675
+ const ALL_TYPES = "__all__";
676
+ const MODE_OPTIONS$1 = [{
677
+ label: "Include",
678
+ value: "include"
679
+ }, {
680
+ label: "Exclude",
681
+ value: "exclude"
682
+ }];
683
+ const BINS$1 = 40;
684
+ const Stat$1 = ({ label, value }) => /* @__PURE__ */ jsxs(Stack, {
685
+ gap: 0,
686
+ miw: 0,
687
+ children: [/* @__PURE__ */ jsx(Text, {
688
+ size: "xs",
689
+ c: "dimmed",
690
+ tt: "uppercase",
691
+ fw: 600,
692
+ children: label
693
+ }), /* @__PURE__ */ jsx(Text, {
694
+ size: "sm",
695
+ fw: 500,
696
+ truncate: true,
697
+ children: typeof value === "number" ? value.toLocaleString() : value
698
+ })]
193
699
  });
194
- const DateFilterField = memo(function DateFilterField({ control, name, min, max }) {
195
- return /* @__PURE__ */ jsx(Controller, {
196
- name,
197
- control,
198
- render: ({ field }) => {
199
- return /* @__PURE__ */ jsxs(Group, {
700
+ const AttributeDetail = memo(({ ocelId, entityType, filter, onChange, before, scopes, onClear }) => {
701
+ const view = useView(ocelId, before);
702
+ const distribution = useAttributeDistribution(ocelId, {
703
+ ...view,
704
+ attribute: filter.attribute,
705
+ entity_type: entityType,
706
+ entity_names: filter.target_type ? [filter.target_type] : void 0,
707
+ bins: BINS$1
708
+ }, { query: {
709
+ enabled: view !== void 0,
710
+ placeholderData: keepPreviousData
711
+ } });
712
+ const typeNoun = entityType === "events" ? "activities" : "object types";
713
+ return /* @__PURE__ */ jsxs(Stack, {
714
+ gap: "md",
715
+ flex: 1,
716
+ mih: 0,
717
+ children: [/* @__PURE__ */ jsxs(Group, {
718
+ justify: "space-between",
719
+ wrap: "nowrap",
720
+ children: [/* @__PURE__ */ jsxs(Group, {
721
+ gap: "sm",
200
722
  wrap: "nowrap",
201
- align: "end",
202
- gap: "xs",
723
+ miw: 0,
203
724
  children: [
204
- /* @__PURE__ */ jsx(DateTimePicker, {
205
- value: field.value?.[0] ?? void 0,
206
- minDate: min,
207
- maxDate: field.value?.[1] ?? max,
208
- withSeconds: true,
209
- onChange: (nextMin) => {
210
- field.onChange([nextMin, field.value?.[1] ?? max]);
211
- },
212
- style: { flex: 1 }
725
+ /* @__PURE__ */ jsx(Button, {
726
+ variant: "subtle",
727
+ size: "compact-sm",
728
+ leftSection: /* @__PURE__ */ jsx(ArrowLeftIcon, { size: 14 }),
729
+ onClick: onClear,
730
+ children: "Change attribute"
213
731
  }),
214
- /* @__PURE__ */ jsx(Text, {
215
- c: "dimmed",
216
- size: "sm",
217
- pb: 8,
218
- children: "-"
732
+ /* @__PURE__ */ jsx(Title, {
733
+ order: 5,
734
+ style: { overflowWrap: "anywhere" },
735
+ children: filter.attribute
219
736
  }),
220
- /* @__PURE__ */ jsx(DateTimePicker, {
221
- value: field.value?.[1] ?? void 0,
222
- minDate: field.value?.[0] ?? min,
223
- maxDate: max,
224
- withSeconds: true,
225
- onChange: (nextMax) => {
226
- field.onChange([field.value?.[0] ?? min, nextMax]);
227
- },
228
- style: { flex: 1 }
737
+ distribution.data && /* @__PURE__ */ jsx(Badge, {
738
+ variant: "light",
739
+ children: distribution.data.type
229
740
  })
230
741
  ]
231
- });
232
- }
742
+ }), /* @__PURE__ */ jsxs(Group, {
743
+ gap: "sm",
744
+ wrap: "nowrap",
745
+ children: [/* @__PURE__ */ jsx(SegmentedControl, {
746
+ size: "xs",
747
+ data: MODE_OPTIONS$1,
748
+ value: filter.mode ?? "include",
749
+ onChange: (next) => onChange({
750
+ ...filter,
751
+ mode: next
752
+ })
753
+ }), scopes.length > 1 ? /* @__PURE__ */ jsx(Select, {
754
+ size: "xs",
755
+ w: 220,
756
+ allowDeselect: false,
757
+ value: filter.target_type ?? ALL_TYPES,
758
+ data: [{
759
+ value: ALL_TYPES,
760
+ label: `All ${typeNoun}`
761
+ }, ...scopes.map((scope) => ({
762
+ value: scope,
763
+ label: scope
764
+ }))],
765
+ onChange: (next) => onChange({
766
+ ...filter,
767
+ target_type: next === ALL_TYPES ? null : next
768
+ })
769
+ }) : scopes[0] && /* @__PURE__ */ jsx(Badge, {
770
+ variant: "outline",
771
+ color: "gray",
772
+ children: scopes[0]
773
+ })]
774
+ })]
775
+ }), /* @__PURE__ */ jsx(Previewed, {
776
+ query: distribution,
777
+ children: (data) => data ? /* @__PURE__ */ jsxs(Stack, {
778
+ gap: "md",
779
+ flex: 1,
780
+ mih: 0,
781
+ children: [/* @__PURE__ */ jsxs(SimpleGrid, {
782
+ cols: 4,
783
+ children: [
784
+ /* @__PURE__ */ jsx(Stat$1, {
785
+ label: "Values",
786
+ value: data.count
787
+ }),
788
+ /* @__PURE__ */ jsx(Stat$1, {
789
+ label: "Distinct",
790
+ value: data.distinct_values
791
+ }),
792
+ /* @__PURE__ */ jsx(Stat$1, {
793
+ label: "Min",
794
+ value: formatValue(data.min, data.type)
795
+ }),
796
+ /* @__PURE__ */ jsx(Stat$1, {
797
+ label: "Max",
798
+ value: formatValue(data.max, data.type)
799
+ })
800
+ ]
801
+ }), data.type === "int" || data.type === "float" ? /* @__PURE__ */ jsx(NumberRangeView, {
802
+ data,
803
+ filter,
804
+ onChange
805
+ }) : data.type === "date" ? /* @__PURE__ */ jsx(DateRangeView, {
806
+ data,
807
+ filter,
808
+ onChange
809
+ }) : /* @__PURE__ */ jsx(ValuesView, {
810
+ data,
811
+ filter,
812
+ onChange,
813
+ ocelId,
814
+ entityType,
815
+ before
816
+ })]
817
+ }) : /* @__PURE__ */ jsx(Text, {
818
+ size: "sm",
819
+ c: "dimmed",
820
+ children: "The attribute has no values in the log as the steps before leave it."
821
+ })
822
+ })]
233
823
  });
234
824
  });
235
- const AttributeInputField = memo(function AttributeInputField({ control, record, path }) {
236
- switch (record.type) {
237
- case "string": return /* @__PURE__ */ jsx(StringFilterField, {
238
- control,
239
- name: `${path}.regex`
240
- });
241
- case "int": return /* @__PURE__ */ jsx(NumberFilterField, {
242
- control,
243
- name: `${path}.number_range`,
244
- min: Number.parseInt(`${record.min}`, 10),
245
- max: Number.parseInt(`${record.max}`, 10),
246
- allowDecimals: false
825
+ // src/components/FilterViews/AttributeFilter/AttributeFilter.tsx
826
+ const AttributePanel = (entityType) => {
827
+ const useTypedAttributes = entityType === "events" ? useEventAttributes : useObjectAttributes;
828
+ return ({ ocelId, filter, onChange, before }) => {
829
+ const { filterColumns } = useAttributeFilter({
830
+ filter,
831
+ onChange
247
832
  });
248
- case "float": return /* @__PURE__ */ jsx(NumberFilterField, {
249
- control,
250
- name: `${path}.number_range`,
251
- min: Number.parseFloat(`${record.min}`),
252
- max: Number.parseFloat(`${record.max}`)
253
- });
254
- case "date": return /* @__PURE__ */ jsx(DateFilterField, {
255
- control,
256
- name: `${path}.time_range`,
257
- min: record.min.toString(),
258
- max: record.max.toString()
833
+ const view = useView(ocelId, before);
834
+ const typed = useTypedAttributes(ocelId, view, { query: {
835
+ enabled: view !== void 0,
836
+ placeholderData: keepPreviousData
837
+ } });
838
+ const scopes = useMemo(() => (typed.data ?? []).filter(({ name }) => name === filter.attribute).map(({ entity_type }) => entity_type), [typed.data, filter.attribute]);
839
+ if (filter.attribute) return /* @__PURE__ */ jsx(AttributeDetail, {
840
+ ocelId,
841
+ entityType,
842
+ filter,
843
+ onChange,
844
+ before,
845
+ scopes,
846
+ onClear: () => onChange({
847
+ type: filter.type,
848
+ attribute: ""
849
+ })
259
850
  });
260
- default: return /* @__PURE__ */ jsx(Text, {
261
- size: "sm",
262
- children: "Not Implemented"
851
+ return /* @__PURE__ */ jsx(Previewed, {
852
+ query: {
853
+ data: view,
854
+ isPending: true,
855
+ error: null
856
+ },
857
+ children: (view) => /* @__PURE__ */ jsx(AttributesTable, {
858
+ ocelId,
859
+ entityType,
860
+ ...viewProps(view),
861
+ extraColumns: filterColumns,
862
+ subTableExtraColumns: filterColumns,
863
+ hideRange: true,
864
+ hideValues: true
865
+ })
263
866
  });
867
+ };
868
+ };
869
+ const condition = (filter) => {
870
+ if (filter.regex) return `matches /${filter.regex}/`;
871
+ if (filter.number_range) {
872
+ const [low, high] = filter.number_range;
873
+ return `${low ?? "-∞"} – ${high ?? "∞"}`;
264
874
  }
265
- });
266
- //#endregion
267
- //#region src/hooks/useAttributeFilterColumns.tsx
268
- const getInitialFilter = (attribute) => {
269
- switch (attribute.type) {
270
- case "string": return { regex: "" };
271
- case "int": return { number_range: [Number.parseInt(`${attribute.min}`, 10), Number.parseInt(`${attribute.max}`, 10)] };
272
- case "float": return { number_range: [Number.parseFloat(`${attribute.min}`), Number.parseFloat(`${attribute.max}`)] };
273
- case "date": return { time_range: [String(attribute.min), String(attribute.max)] };
274
- default: return {};
875
+ if (filter.time_range) {
876
+ const [start, end] = filter.time_range;
877
+ const format = (value, open) => value ? dayjs_default(value).format("YYYY-MM-DD") : open;
878
+ return `${format(start, "start")} – ${format(end, "end")}`;
275
879
  }
880
+ if (filter.values) {
881
+ const shown = filter.values.slice(0, 3).join(", ");
882
+ const more = filter.values.length - 3;
883
+ return `in ${shown}${more > 0 ? ` +${more} more` : ""}`;
884
+ }
885
+ return "any value";
276
886
  };
277
- const getEntityType = (attribute) => {
278
- if (!("entity_type_names" in attribute)) return attribute.entity_type;
279
- return attribute.entity_type_names.length === 1 ? attribute.entity_type_names[0] : void 0;
887
+ const summary$1 = (filter) => {
888
+ if (!filter.attribute) return "No attribute selected";
889
+ const scope = filter.target_type ? ` (${filter.target_type})` : "";
890
+ const negation = filter.mode === "exclude" ? "not " : "";
891
+ return `${filter.attribute}${scope}: ${negation}${condition(filter)}`;
892
+ };
893
+ const EventAttributeFilter = {
894
+ title: "Event Attribute",
895
+ description: "Keeps the events whose value of one attribute satisfies a condition, such as a value range or a pattern. Pick the attribute with Use, then set the condition on its value distribution: a range for numbers and dates, chosen values or a pattern otherwise. With Exclude, the matching events are removed instead. Activities without the attribute are left untouched.",
896
+ Panel: AttributePanel("events"),
897
+ create: () => ({
898
+ type: "event_attribute",
899
+ attribute: ""
900
+ }),
901
+ summary: summary$1,
902
+ isComplete: (filter) => filter.attribute !== ""
280
903
  };
281
- const generateRecordId = (attribute) => {
282
- const entityName = getEntityType(attribute);
283
- return `${attribute.name}::${entityName ?? ""}`;
904
+ const ObjectAttributeFilter = {
905
+ title: "Object Attribute",
906
+ description: "Keeps the objects whose value of one attribute satisfies a condition, such as a value range or a pattern. Pick the attribute with Use, then set the condition on its value distribution: a range for numbers and dates, chosen values or a pattern otherwise. With Exclude, the matching objects are removed instead. Object types without the attribute are left untouched.",
907
+ Panel: AttributePanel("objects"),
908
+ create: () => ({
909
+ type: "object_attribute",
910
+ attribute: ""
911
+ }),
912
+ summary: summary$1,
913
+ isComplete: (filter) => filter.attribute !== ""
284
914
  };
285
- const useAttributeFilter = ({ path, control }) => {
286
- const { fields, append, remove } = useFieldArray({
287
- name: path,
288
- control
915
+ // src/components/FilterViews/RelationCountFilter/RelationDetail.tsx
916
+ const ANY_QUALIFIER = "__any__";
917
+ const BINS = 40;
918
+ const formatCount = (value) => value.toLocaleString();
919
+ const Stat = ({ label, value }) => /* @__PURE__ */ jsxs(Stack, {
920
+ gap: 0,
921
+ miw: 0,
922
+ children: [/* @__PURE__ */ jsx(Text, {
923
+ size: "xs",
924
+ c: "dimmed",
925
+ tt: "uppercase",
926
+ fw: 600,
927
+ children: label
928
+ }), /* @__PURE__ */ jsx(Text, {
929
+ size: "sm",
930
+ fw: 500,
931
+ children: value.toLocaleString()
932
+ })]
933
+ });
934
+ const RelationDetail = ({ ocelId, kind, hooks, filter, onChange, before, onClear }) => {
935
+ const direction = filter.direction ?? "source";
936
+ const view = useView(ocelId, before);
937
+ const options = { query: {
938
+ enabled: view !== void 0,
939
+ placeholderData: keepPreviousData
940
+ } };
941
+ const qualifiers = hooks.useRelations(ocelId, {
942
+ ...view,
943
+ with_qualifier: false,
944
+ source_types: [filter.source],
945
+ target_types: [filter.target]
946
+ }, options).data?.response[0]?.qualifiers ?? [];
947
+ const distribution = hooks.useDistribution(ocelId, {
948
+ ...view,
949
+ source: filter.source,
950
+ target: filter.target,
951
+ direction,
952
+ qualifier: filter.qualifier ?? void 0,
953
+ bins: BINS
954
+ }, options);
955
+ const sides = kind === "e2o" ? {
956
+ source: "event",
957
+ target: "object"
958
+ } : {
959
+ source: filter.source,
960
+ target: filter.target
961
+ };
962
+ const isSource = direction === "source";
963
+ const countedNoun = kind === "e2o" && isSource ? "event" : "object";
964
+ const relatedNoun = kind === "e2o" && !isSource ? "events" : "objects";
965
+ const caption = `How many ${isSource ? filter.target : filter.source} ${relatedNoun} each ${isSource ? filter.source : filter.target} ${countedNoun} is related to`;
966
+ return /* @__PURE__ */ jsxs(Stack, {
967
+ gap: "md",
968
+ flex: 1,
969
+ mih: 0,
970
+ children: [/* @__PURE__ */ jsxs(Group, {
971
+ justify: "space-between",
972
+ wrap: "nowrap",
973
+ children: [/* @__PURE__ */ jsxs(Group, {
974
+ gap: "sm",
975
+ wrap: "nowrap",
976
+ miw: 0,
977
+ children: [/* @__PURE__ */ jsx(Button, {
978
+ variant: "subtle",
979
+ size: "compact-sm",
980
+ leftSection: /* @__PURE__ */ jsx(ArrowLeftIcon, { size: 14 }),
981
+ onClick: onClear,
982
+ children: "Change relation"
983
+ }), /* @__PURE__ */ jsxs(Title, {
984
+ order: 5,
985
+ style: { overflowWrap: "anywhere" },
986
+ children: [
987
+ filter.source,
988
+ " → ",
989
+ filter.target
990
+ ]
991
+ })]
992
+ }), /* @__PURE__ */ jsxs(Group, {
993
+ gap: "sm",
994
+ wrap: "nowrap",
995
+ children: [/* @__PURE__ */ jsx(SegmentedControl, {
996
+ size: "xs",
997
+ value: direction,
998
+ data: [{
999
+ label: `Per ${sides.source}`,
1000
+ value: "source"
1001
+ }, {
1002
+ label: `Per ${sides.target}`,
1003
+ value: "target"
1004
+ }],
1005
+ onChange: (next) => onChange({
1006
+ ...filter,
1007
+ direction: next,
1008
+ range: [null, null]
1009
+ })
1010
+ }), qualifiers.length > 0 && /* @__PURE__ */ jsx(Select, {
1011
+ size: "xs",
1012
+ w: 200,
1013
+ allowDeselect: false,
1014
+ value: filter.qualifier ?? ANY_QUALIFIER,
1015
+ data: [{
1016
+ value: ANY_QUALIFIER,
1017
+ label: "Any qualifier"
1018
+ }, ...qualifiers.map((qualifier) => ({
1019
+ value: qualifier,
1020
+ label: qualifier
1021
+ }))],
1022
+ onChange: (next) => onChange({
1023
+ ...filter,
1024
+ qualifier: next === ANY_QUALIFIER ? null : next
1025
+ })
1026
+ })]
1027
+ })]
1028
+ }), /* @__PURE__ */ jsx(Previewed, {
1029
+ query: distribution,
1030
+ children: (data) => /* @__PURE__ */ jsx(CountRange, {
1031
+ data,
1032
+ filter,
1033
+ onChange,
1034
+ countedLabel: countedNoun === "event" ? "Events" : "Objects",
1035
+ caption
1036
+ })
1037
+ })]
289
1038
  });
290
- const filterIndexMap = useMemo(() => {
291
- const map = /* @__PURE__ */ new Map();
292
- fields.forEach(({ attribute, target_type }, index) => {
293
- map.set(`${attribute}::${target_type ?? ""}`, index);
294
- });
295
- return map;
296
- }, [fields]);
297
- const [showOnlyFiltered, setShowOnlyFiltered] = useState(false);
298
- const filteredAttributeNames = useMemo(() => Array.from(new Set(fields.map(({ attribute }) => attribute))), [fields]);
299
- return {
300
- filterColumns: useMemo(() => [{
301
- accessor: "filter",
302
- title: "Filter",
303
- filter: () => /* @__PURE__ */ jsx(Switch, {
304
- label: "Show only filtered attributes",
305
- checked: showOnlyFiltered,
306
- onChange: (event) => setShowOnlyFiltered(event.currentTarget.checked)
1039
+ };
1040
+ const CountRange = ({ data, filter, onChange, countedLabel, caption }) => {
1041
+ const bins = useMemo(() => data.bins.map(({ start, end, count }, index, all) => ({
1042
+ start: Number(start),
1043
+ end: index < all.length - 1 ? Number(end) - 1 : Number(end),
1044
+ count
1045
+ })), [data.bins]);
1046
+ const [low, high] = filter.range;
1047
+ const value = [low ?? data.min, high ?? data.max];
1048
+ const setRange = (range) => onChange({
1049
+ ...filter,
1050
+ range
1051
+ });
1052
+ return /* @__PURE__ */ jsxs(Stack, {
1053
+ gap: "md",
1054
+ children: [
1055
+ /* @__PURE__ */ jsxs(SimpleGrid, {
1056
+ cols: 4,
1057
+ children: [
1058
+ /* @__PURE__ */ jsx(Stat, {
1059
+ label: countedLabel,
1060
+ value: data.entities
1061
+ }),
1062
+ /* @__PURE__ */ jsx(Stat, {
1063
+ label: "Relations",
1064
+ value: data.relations
1065
+ }),
1066
+ /* @__PURE__ */ jsx(Stat, {
1067
+ label: "Min",
1068
+ value: data.min
1069
+ }),
1070
+ /* @__PURE__ */ jsx(Stat, {
1071
+ label: "Max",
1072
+ value: data.max
1073
+ })
1074
+ ]
307
1075
  }),
308
- filtering: showOnlyFiltered,
309
- render: (record) => {
310
- const filterIndex = filterIndexMap.get(generateRecordId(record)) ?? -1;
311
- return filterIndex < 0 ? /* @__PURE__ */ jsxs(Text, {
312
- size: "sm",
313
- c: "dimmed",
1076
+ /* @__PURE__ */ jsx(Text, {
1077
+ size: "sm",
1078
+ c: "dimmed",
1079
+ children: caption
1080
+ }),
1081
+ /* @__PURE__ */ jsx(RangeView, {
1082
+ bins,
1083
+ min: data.min,
1084
+ max: data.max,
1085
+ value,
1086
+ onChange: setRange,
1087
+ format: formatCount,
1088
+ snap: Math.round,
1089
+ inputs: /* @__PURE__ */ jsxs(Group, {
1090
+ wrap: "nowrap",
1091
+ align: "end",
1092
+ gap: "xs",
314
1093
  children: [
315
- record.min,
316
- " - ",
317
- record.max
1094
+ /* @__PURE__ */ jsx(NumberInput, {
1095
+ flex: 1,
1096
+ value: value[0],
1097
+ min: data.min,
1098
+ max: value[1],
1099
+ allowDecimal: false,
1100
+ onChange: (next) => setRange([typeof next === "number" ? next : data.min, value[1]])
1101
+ }),
1102
+ /* @__PURE__ */ jsx(Text, {
1103
+ c: "dimmed",
1104
+ size: "sm",
1105
+ pb: 8,
1106
+ children: "-"
1107
+ }),
1108
+ /* @__PURE__ */ jsx(NumberInput, {
1109
+ flex: 1,
1110
+ value: value[1],
1111
+ min: value[0],
1112
+ max: data.max,
1113
+ allowDecimal: false,
1114
+ onChange: (next) => setRange([value[0], typeof next === "number" ? next : data.max])
1115
+ })
318
1116
  ]
319
- }) : /* @__PURE__ */ jsx(AttributeInputField, {
320
- record,
321
- control,
322
- path: `${path}.${filterIndex}`
323
- });
324
- }
325
- }, {
326
- accessor: "action",
327
- title: "",
328
- width: "50px",
329
- render: (record) => {
330
- const key = generateRecordId(record);
331
- const filterIndex = filterIndexMap.get(key) ?? -1;
332
- return filterIndex < 0 ? /* @__PURE__ */ jsx(ActionIcon, {
333
- color: "green",
334
- variant: "light",
335
- onClick: (event) => {
336
- event.stopPropagation();
337
- append({
338
- attribute: record.name,
339
- type: path,
340
- target_type: getEntityType(record),
341
- ...getInitialFilter(record)
342
- });
343
- },
344
- children: /* @__PURE__ */ jsx(PlusIcon, { size: 16 })
345
- }) : /* @__PURE__ */ jsx(ActionIcon, {
346
- color: "red",
347
- variant: "light",
348
- onClick: (event) => {
349
- event.stopPropagation();
350
- remove(filterIndex);
1117
+ })
1118
+ })
1119
+ ]
1120
+ });
1121
+ };
1122
+ // src/components/FilterViews/RelationCountFilter/RelationGraph.tsx
1123
+ const OcelTypeGraph = dynamic(() => import("@r4pm/components").then((m) => m.OcelTypeGraph), { ssr: false });
1124
+ const eventId = (activity) => `event:${activity}`;
1125
+ const objectId = (objectType) => `object:${objectType}`;
1126
+ const EDGE_LIMIT = 10;
1127
+ const countOptions = (counts) => Object.entries(counts).map(([name, count]) => ({
1128
+ value: name,
1129
+ label: name,
1130
+ count
1131
+ }));
1132
+ const MIN_WIDTH = 1;
1133
+ const ScopePicker = ({ label, count, total, children }) => /* @__PURE__ */ jsxs(Group, {
1134
+ gap: 4,
1135
+ children: [/* @__PURE__ */ jsxs(Text, {
1136
+ size: "xs",
1137
+ c: "dimmed",
1138
+ children: [
1139
+ label,
1140
+ ": ",
1141
+ count,
1142
+ " / ",
1143
+ total
1144
+ ]
1145
+ }), /* @__PURE__ */ jsxs(Popover, {
1146
+ width: 260,
1147
+ position: "bottom",
1148
+ withArrow: true,
1149
+ shadow: "md",
1150
+ children: [/* @__PURE__ */ jsx(Popover.Target, { children: /* @__PURE__ */ jsx(ActionIcon, {
1151
+ size: "md",
1152
+ variant: "transparent",
1153
+ children: /* @__PURE__ */ jsx(FilterIcon, { size: 16 })
1154
+ }) }), /* @__PURE__ */ jsx(Popover.Dropdown, { children })]
1155
+ })]
1156
+ });
1157
+ const RelationGraph = ({ ocelId, kind, hooks, filter, onChange, before }) => {
1158
+ const theme = useMantineTheme();
1159
+ const [showQualifiers, setShowQualifiers] = useState(true);
1160
+ const [pickedActivities, setActivities] = useState(null);
1161
+ const [pickedObjectTypes, setObjectTypes] = useState(null);
1162
+ const [pending, setPending] = useState(null);
1163
+ const [edgeLimit, setEdgeLimit] = useState(EDGE_LIMIT);
1164
+ const view = useView(ocelId, before);
1165
+ const options = { query: {
1166
+ enabled: view !== void 0,
1167
+ placeholderData: keepPreviousData
1168
+ } };
1169
+ const relations = hooks.useRelations(ocelId, {
1170
+ ...view,
1171
+ with_qualifier: false
1172
+ }, options);
1173
+ const qualified = hooks.useRelations(ocelId, {
1174
+ ...view,
1175
+ with_qualifier: true
1176
+ }, options);
1177
+ const events = useEventCounts(ocelId, view, { query: {
1178
+ enabled: view !== void 0,
1179
+ placeholderData: keepPreviousData
1180
+ } });
1181
+ const objects = useObjectCounts(ocelId, view, { query: {
1182
+ enabled: view !== void 0,
1183
+ placeholderData: keepPreviousData
1184
+ } });
1185
+ const query = {
1186
+ data: useMemo(() => {
1187
+ if (!relations.data || !qualified.data || !events.data || !objects.data) return;
1188
+ const summaries = relations.data.response;
1189
+ const sourceId = kind === "e2o" ? eventId : objectId;
1190
+ const edges = summaries.flatMap(({ source, target, sum: pairSum, qualifiers }) => {
1191
+ return (showQualifiers && (qualifiers?.length ?? 0) > 0 ? qualified.data.response.filter((row) => row.source === source && row.target === target) : [{
1192
+ qualifier: null,
1193
+ sum: pairSum
1194
+ }]).map(({ qualifier, sum }) => ({
1195
+ id: `${kind}:${source}->${target}:${qualifier ?? ""}`,
1196
+ source: sourceId(source),
1197
+ target: objectId(target),
1198
+ qualifier: qualifier ?? void 0,
1199
+ kind,
1200
+ relation: {
1201
+ source,
1202
+ target,
1203
+ qualifier
351
1204
  },
352
- children: /* @__PURE__ */ jsx(XIcon, { size: 16 })
353
- });
354
- }
355
- }], [
356
- append,
357
- remove,
358
- filterIndexMap,
359
- path,
360
- control,
361
- showOnlyFiltered
1205
+ sum,
1206
+ pair: `${source}->${target}`,
1207
+ pairSum
1208
+ }));
1209
+ });
1210
+ const activityCounts = Object.fromEntries([...new Set(kind === "e2o" ? summaries.map(({ source }) => source) : [])].map((activity) => [activity, events.data[activity] ?? 0]));
1211
+ const objectTypeCounts = Object.fromEntries([...new Set(summaries.flatMap(({ source, target }) => kind === "e2o" ? [target] : [source, target]))].map((objectType) => [objectType, objects.data[objectType] ?? 0]));
1212
+ return {
1213
+ nodes: [...Object.entries(activityCounts).map(([activity, count]) => ({
1214
+ id: eventId(activity),
1215
+ label: activity,
1216
+ kind: "event",
1217
+ count
1218
+ })), ...Object.entries(objectTypeCounts).map(([objectType, count]) => ({
1219
+ id: objectId(objectType),
1220
+ label: objectType,
1221
+ kind: "object",
1222
+ count
1223
+ }))],
1224
+ edges,
1225
+ activityCounts,
1226
+ objectTypeCounts
1227
+ };
1228
+ }, [
1229
+ relations.data,
1230
+ qualified.data,
1231
+ events.data,
1232
+ objects.data,
1233
+ kind,
1234
+ showQualifiers
362
1235
  ]),
363
- visibleAttributes: showOnlyFiltered ? filteredAttributeNames : void 0
1236
+ isPending: relations.isPending || qualified.isPending || events.isPending || objects.isPending,
1237
+ error: relations.error ?? qualified.error ?? events.error ?? objects.error
364
1238
  };
365
- };
366
- //#endregion
367
- //#region src/components/FilterViews/AttributeFilter.tsx
368
- const AttributeFilter = (entityType) => ({ ocelId, control }) => {
369
- const { filterColumns, visibleAttributes } = useAttributeFilter({
370
- control,
371
- path: entityType === "events" ? "event_attribute" : "object_attribute"
372
- });
373
- return /* @__PURE__ */ jsx(AttributesTable, {
374
- ocelId,
375
- entityType,
376
- ocelVersion: "original",
377
- extraColumns: filterColumns,
378
- subTableExtraColumns: filterColumns,
379
- visibleAttributes,
380
- hideRange: true,
381
- hideValues: true
382
- });
383
- };
384
- const EventAttributeFilter = {
385
- title: "Event Attribute",
386
- description: "Filters events by the values of their attributes. For each attribute you can define a condition (e.g. a value range or an allowed set of values), and only events whose attribute values satisfy every condition are kept. Attributes without a condition are ignored.",
387
- ViewComponent: AttributeFilter("events"),
388
- generateDefault: () => []
389
- };
390
- const ObjectAttributeFilter = {
391
- title: "Object Attribute",
392
- description: "Filters objects by the values of their attributes. For each attribute you can define a condition (e.g. a value range or an allowed set of values), and only objects whose attribute values satisfy every condition are kept. Attributes without a condition are ignored.",
393
- ViewComponent: AttributeFilter("objects"),
394
- generateDefault: () => []
395
- };
396
- //#endregion
397
- //#region src/hooks/useRelationFilterColumns.tsx
398
- const filterKey = ({ source, target, qualifier }) => `${source}-${qualifier ?? ""}-${target}`;
399
- const resolveQualifier = (record) => record.qualifiers?.length === 1 ? record.qualifiers[0] : void 0;
400
- const useRelationCountFilter = ({ path, control }) => {
401
- const { fields, append, remove } = useFieldArray({
402
- name: path,
403
- control
1239
+ const select = (relation) => onChange({
1240
+ ...filter,
1241
+ ...relation,
1242
+ direction: "source",
1243
+ range: [null, null]
404
1244
  });
405
- const filterIndexMap = useMemo(() => {
406
- const map = /* @__PURE__ */ new Map();
407
- fields.forEach((field, index) => {
408
- map.set(filterKey(field), index);
409
- });
410
- return map;
411
- }, [fields]);
412
- const [showOnlyFiltered, setShowOnlyFiltered] = useState(false);
413
- const visibleRelations = useMemo(() => {
414
- const seen = /* @__PURE__ */ new Set();
415
- const relations = [];
416
- fields.forEach((field) => {
417
- const { source, target } = field;
418
- const key = `${source}-${target}`;
419
- if (!seen.has(key)) {
420
- seen.add(key);
421
- relations.push({
422
- source,
423
- target
424
- });
425
- }
426
- });
427
- return relations;
428
- }, [fields]);
429
- return {
430
- filterColumns: useMemo(() => [{
431
- accessor: "filter",
432
- title: "Filter",
433
- filter: () => /* @__PURE__ */ jsx(Switch, {
434
- label: "Show only filtered relations",
435
- checked: showOnlyFiltered,
436
- onChange: (event) => setShowOnlyFiltered(event.currentTarget.checked)
437
- }),
438
- filtering: showOnlyFiltered,
439
- render: (record) => {
440
- const qualifier = resolveQualifier(record);
441
- const filterIndex = filterIndexMap.get(filterKey({
442
- ...record,
443
- qualifier
444
- })) ?? -1;
445
- if (filterIndex < 0) return /* @__PURE__ */ jsx(Text, {
446
- size: "sm",
447
- c: "dimmed",
448
- children: "No filter applied"
1245
+ return /* @__PURE__ */ jsx(Previewed, {
1246
+ query,
1247
+ children: ({ nodes, edges: allEdges, activityCounts, objectTypeCounts }) => {
1248
+ const activities = pickedActivities ?? Object.keys(activityCounts);
1249
+ const objectTypes = pickedObjectTypes ?? Object.keys(objectTypeCounts);
1250
+ const picked = /* @__PURE__ */ new Set([...activities.map(eventId), ...objectTypes.map(objectId)]);
1251
+ const candidates = allEdges.filter(({ source, target }) => picked.has(source) && picked.has(target));
1252
+ const pairs = new Map(candidates.map(({ pair, pairSum }) => [pair, pairSum]));
1253
+ const shownPairs = new Set([...pairs].sort(([, a], [, b]) => b - a).slice(0, edgeLimit).map(([pair]) => pair));
1254
+ const edges = candidates.filter(({ pair }) => shownPairs.has(pair));
1255
+ const visibleNodeIds = new Set(edges.flatMap(({ source, target }) => [source, target]));
1256
+ const shownNodes = nodes.filter(({ id }) => visibleNodeIds.has(id));
1257
+ const structure = edges.map(({ id }) => id).sort().join("|");
1258
+ const largest = Math.max(1, ...edges.map(({ sum }) => sum));
1259
+ const byId = new Map(edges.map((edge) => [edge.id, edge]));
1260
+ const neighbours = (id) => new Set(edges.flatMap(({ source, target }) => source === id ? [target] : target === id ? [source] : []));
1261
+ const connected = pending ? neighbours(pending) : void 0;
1262
+ const clickNode = (id) => {
1263
+ if (!pending || pending === id) {
1264
+ setPending(pending === id ? null : id);
1265
+ return;
1266
+ }
1267
+ const candidates = edges.filter(({ source, target }) => source === pending && target === id || source === id && target === pending);
1268
+ const [first] = candidates;
1269
+ if (!first) {
1270
+ setPending(id);
1271
+ return;
1272
+ }
1273
+ setPending(null);
1274
+ select({
1275
+ ...first.relation,
1276
+ qualifier: candidates.length === 1 ? first.relation.qualifier : null
449
1277
  });
450
- return /* @__PURE__ */ jsx(Controller, {
451
- control,
452
- name: `${path}.${filterIndex}.range`,
453
- render: ({ field }) => {
454
- const currentMin = typeof field.value?.[0] === "number" ? field.value[0] : void 0;
455
- const currentMax = typeof field.value?.[1] === "number" ? field.value[1] : void 0;
456
- return /* @__PURE__ */ jsxs(Group, {
457
- wrap: "nowrap",
458
- align: "end",
459
- gap: "xs",
460
- children: [
461
- /* @__PURE__ */ jsx(NumberInput, {
462
- value: currentMin,
463
- min: record.min_count,
464
- max: currentMax ?? record.max_count,
465
- clampBehavior: "strict",
466
- allowDecimal: false,
467
- onChange: (nextMin) => {
468
- field.onChange([nextMin, currentMax]);
469
- },
470
- style: { flex: 1 }
471
- }),
472
- /* @__PURE__ */ jsx(Text, {
1278
+ };
1279
+ return /* @__PURE__ */ jsxs(Stack, {
1280
+ gap: "xs",
1281
+ flex: 1,
1282
+ mih: 0,
1283
+ children: [/* @__PURE__ */ jsxs(Group, {
1284
+ justify: "space-between",
1285
+ children: [/* @__PURE__ */ jsxs(Group, {
1286
+ gap: "md",
1287
+ children: [
1288
+ kind === "e2o" && /* @__PURE__ */ jsx(ScopePicker, {
1289
+ label: "Activities",
1290
+ count: activities.length,
1291
+ total: Object.keys(activityCounts).length,
1292
+ children: /* @__PURE__ */ jsx(OptionPicker, {
1293
+ options: countOptions(activityCounts),
1294
+ variant: "list",
1295
+ multiple: true,
1296
+ cutoff: true,
1297
+ value: activities,
1298
+ onChange: (next) => setActivities([...next])
1299
+ })
1300
+ }),
1301
+ /* @__PURE__ */ jsx(ScopePicker, {
1302
+ label: "Object types",
1303
+ count: objectTypes.length,
1304
+ total: Object.keys(objectTypeCounts).length,
1305
+ children: /* @__PURE__ */ jsx(OptionPicker, {
1306
+ options: countOptions(objectTypeCounts),
1307
+ variant: "list",
1308
+ multiple: true,
1309
+ cutoff: true,
1310
+ value: objectTypes,
1311
+ onChange: (next) => setObjectTypes([...next])
1312
+ })
1313
+ }),
1314
+ /* @__PURE__ */ jsxs(Group, {
1315
+ gap: "xs",
1316
+ children: [/* @__PURE__ */ jsxs(Text, {
1317
+ size: "xs",
473
1318
  c: "dimmed",
1319
+ children: [
1320
+ "Relations: top ",
1321
+ Math.min(edgeLimit, pairs.size),
1322
+ " of",
1323
+ " ",
1324
+ pairs.size
1325
+ ]
1326
+ }), /* @__PURE__ */ jsx(Slider, {
1327
+ w: 140,
474
1328
  size: "sm",
475
- pb: 8,
476
- children: "-"
477
- }),
478
- /* @__PURE__ */ jsx(NumberInput, {
479
- value: currentMax,
480
- min: currentMin ?? record.min_count,
481
- max: record.max_count,
482
- clampBehavior: "strict",
483
- allowDecimal: false,
484
- onChange: (nextMax) => {
485
- field.onChange([currentMin, nextMax]);
486
- },
487
- style: { flex: 1 }
488
- })
489
- ]
490
- });
491
- }
492
- });
493
- }
494
- }, {
495
- accessor: "action",
496
- title: "",
497
- width: "50px",
498
- render: (record) => {
499
- const qualifier = resolveQualifier(record);
500
- const filterIndex = filterIndexMap.get(filterKey({
501
- ...record,
502
- qualifier
503
- })) ?? -1;
504
- return filterIndex < 0 ? /* @__PURE__ */ jsx(ActionIcon, {
505
- color: "green",
506
- variant: "light",
507
- onClick: (event) => {
508
- event.stopPropagation();
509
- append({
510
- type: path,
511
- source: record.source,
512
- target: record.target,
513
- qualifier,
514
- range: [record.min_count, record.max_count]
515
- });
516
- },
517
- children: /* @__PURE__ */ jsx(PlusIcon, { size: 16 })
518
- }) : /* @__PURE__ */ jsx(ActionIcon, {
519
- color: "red",
520
- variant: "light",
521
- onClick: (event) => {
522
- event.stopPropagation();
523
- remove(filterIndex);
524
- },
525
- children: /* @__PURE__ */ jsx(XIcon, { size: 16 })
526
- });
527
- }
528
- }], [
529
- append,
530
- remove,
531
- filterIndexMap,
532
- path,
533
- control,
534
- showOnlyFiltered
535
- ]),
536
- visibleRelations: showOnlyFiltered ? visibleRelations : void 0
537
- };
538
- };
539
- //#endregion
540
- //#region src/components/FilterViews/RelationCountFilter.tsx
541
- const RelationCountFilter = (relationType) => ({ ocelId, control }) => {
542
- const { filterColumns, visibleRelations } = useRelationCountFilter({
543
- control,
544
- path: relationType === "e2o" ? "e2o_count" : "o2o_count"
1329
+ min: 1,
1330
+ max: Math.max(1, pairs.size),
1331
+ value: Math.min(edgeLimit, Math.max(1, pairs.size)),
1332
+ onChange: setEdgeLimit
1333
+ })]
1334
+ }),
1335
+ /* @__PURE__ */ jsx(Switch, {
1336
+ size: "xs",
1337
+ label: "Qualifiers",
1338
+ checked: showQualifiers,
1339
+ onChange: (event) => setShowQualifiers(event.currentTarget.checked)
1340
+ })
1341
+ ]
1342
+ }), pending ? /* @__PURE__ */ jsxs(Button, {
1343
+ size: "compact-xs",
1344
+ variant: "light",
1345
+ rightSection: /* @__PURE__ */ jsx(XIcon, { size: 12 }),
1346
+ onClick: () => setPending(null),
1347
+ children: [pending.slice(pending.indexOf(":") + 1), ": pick the second node"]
1348
+ }) : /* @__PURE__ */ jsx(Text, {
1349
+ size: "xs",
1350
+ c: "dimmed",
1351
+ children: "Click an edge, or two nodes, to pick a relation"
1352
+ })]
1353
+ }), /* @__PURE__ */ jsx(Box, {
1354
+ flex: 1,
1355
+ mih: 420,
1356
+ pos: "relative",
1357
+ children: /* @__PURE__ */ jsx(OcelTypeGraph, {
1358
+ nodes: shownNodes,
1359
+ edges,
1360
+ nodeRingColor: (id) => id === pending ? theme.colors.blue[6] : void 0,
1361
+ nodeDimmed: (id) => connected !== void 0 && id !== pending && !connected.has(id),
1362
+ edgeStyle: (id) => {
1363
+ const edge = byId.get(id);
1364
+ return edge ? { width: MIN_WIDTH + 5 * (edge.sum / largest) } : void 0;
1365
+ },
1366
+ onNodeClick: clickNode,
1367
+ onEdgeClick: (id) => {
1368
+ const edge = byId.get(id);
1369
+ if (edge) select(edge.relation);
1370
+ }
1371
+ }, structure)
1372
+ })]
1373
+ });
1374
+ }
545
1375
  });
546
- return /* @__PURE__ */ jsx(RelationTable, {
1376
+ };
1377
+ // src/components/FilterViews/RelationCountFilter/util.ts
1378
+ const RELATION_HOOKS = {
1379
+ e2o: {
1380
+ useRelations: useE2o,
1381
+ useDistribution: useE2oCountDistribution
1382
+ },
1383
+ o2o: {
1384
+ useRelations: useO2o,
1385
+ useDistribution: useO2oCountDistribution
1386
+ }
1387
+ };
1388
+ // src/components/FilterViews/RelationCountFilter/RelationCountFilter.tsx
1389
+ const RelationCountPanel = (kind) => {
1390
+ const hooks = RELATION_HOOKS[kind];
1391
+ return ({ ocelId, filter, onChange, before }) => filter.source ? /* @__PURE__ */ jsx(RelationDetail, {
547
1392
  ocelId,
548
- ocelVersion: "original",
549
- relationType,
550
- extraColumns: filterColumns,
551
- subTableExtraColumns: filterColumns,
552
- visibleRelations,
553
- hideRange: true,
554
- hideTotal: true
1393
+ kind,
1394
+ hooks,
1395
+ filter,
1396
+ onChange,
1397
+ before,
1398
+ onClear: () => onChange({
1399
+ type: filter.type,
1400
+ source: "",
1401
+ target: "",
1402
+ range: [null, null]
1403
+ })
1404
+ }) : /* @__PURE__ */ jsx(RelationGraph, {
1405
+ ocelId,
1406
+ kind,
1407
+ hooks,
1408
+ filter,
1409
+ onChange,
1410
+ before
555
1411
  });
556
1412
  };
1413
+ const summary = (filter) => {
1414
+ if (!filter.source) return "No relation selected";
1415
+ const [low, high] = filter.range;
1416
+ const qualifier = filter.qualifier ? ` (${filter.qualifier})` : "";
1417
+ return `${filter.source} → ${filter.target}${qualifier}: ${low ?? 0} – ${high ?? "∞"}`;
1418
+ };
557
1419
  const E2OCountFilter = {
558
1420
  title: "E2O Count",
559
- description: "Filters events of the source type by the number of objects they are related to (event-to-object relations). For each relation, you can set a minimum and/or maximum count. Events of the source type whose number of related objects does not fall within the specified range are filtered out. Relations without a range are ignored.",
560
- ViewComponent: RelationCountFilter("e2o"),
561
- generateDefault: () => []
1421
+ description: "Keeps the events of the source activity whose number of related objects of the target type (event-to-object relations) falls within a range. Pick the relation by clicking its edge in the graph, then set a minimum and/or maximum count on its distribution. Events of other activities are left untouched.",
1422
+ Panel: RelationCountPanel("e2o"),
1423
+ create: () => ({
1424
+ type: "e2o_count",
1425
+ source: "",
1426
+ target: "",
1427
+ range: [null, null]
1428
+ }),
1429
+ summary,
1430
+ isComplete: (filter) => filter.source !== "" && filter.target !== ""
562
1431
  };
563
1432
  const O2OCountFilter = {
564
1433
  title: "O2O Count",
565
- description: "Filters objects of the source type by the number of other objects they are related to (object-to-object relations). For each relation, you can set a minimum and/or maximum count. Objects of the source type whose number of related objects does not fall within the specified range are filtered out. Relations without a range are ignored.",
566
- ViewComponent: RelationCountFilter("o2o"),
567
- generateDefault: () => []
1434
+ description: "Keeps the objects of the source type whose number of related objects of the target type (object-to-object relations) falls within a range. Pick the relation by clicking its edge in the graph, then set a minimum and/or maximum count on its distribution. Objects of other types are left untouched.",
1435
+ Panel: RelationCountPanel("o2o"),
1436
+ create: () => ({
1437
+ type: "o2o_count",
1438
+ source: "",
1439
+ target: "",
1440
+ range: [null, null]
1441
+ }),
1442
+ summary,
1443
+ isComplete: (filter) => filter.source !== "" && filter.target !== ""
568
1444
  };
569
- //#endregion
570
- //#region src/util/dayjs.ts
571
- dayjs.extend(isBetween);
572
- var dayjs_default = dayjs;
573
- //#endregion
574
- //#region src/components/FilterViews/TimeFrameFilter.tsx
1445
+ // src/components/FilterViews/TimeFrameFilter.tsx
575
1446
  const DATE_FORMAT = "YYYY-MM-DD HH:mm";
576
- const TimeGraph = memo(({ timeInfo, startDate, endDate }) => {
1447
+ const MODE_OPTIONS = [{
1448
+ label: "Include",
1449
+ value: "include"
1450
+ }, {
1451
+ label: "Exclude",
1452
+ value: "exclude"
1453
+ }];
1454
+ const TimeGraph = memo(({ timeInfo, startDate, endDate, mode }) => {
577
1455
  const theme = useMantineTheme();
578
1456
  const option = useMemo(() => {
579
- const buckets = timeInfo.date_distribution.map(({ start_timestamp, end_timestamp, entity_count }) => ({
580
- label: `${dayjs_default(start_timestamp).format(DATE_FORMAT)} - ${dayjs_default(end_timestamp).format(DATE_FORMAT)}`,
581
- count: Object.values(entity_count).reduce((acc, curr) => acc + curr, 0),
582
- isInRange: (!startDate || dayjs_default(end_timestamp).isAfter(dayjs_default(startDate))) && (!endDate || dayjs_default(start_timestamp).isBefore(dayjs_default(endDate)))
583
- }));
1457
+ const buckets = timeInfo.date_distribution.map(({ start_timestamp, end_timestamp, entity_count }) => {
1458
+ const overlapsRange = (!startDate || dayjs_default(end_timestamp).isAfter(dayjs_default(startDate))) && (!endDate || dayjs_default(start_timestamp).isBefore(dayjs_default(endDate)));
1459
+ const insideRange = (!startDate || !dayjs_default(start_timestamp).isBefore(dayjs_default(startDate))) && (!endDate || !dayjs_default(end_timestamp).isAfter(dayjs_default(endDate)));
1460
+ return {
1461
+ label: `${dayjs_default(start_timestamp).format(DATE_FORMAT)} - ${dayjs_default(end_timestamp).format(DATE_FORMAT)}`,
1462
+ count: Object.values(entity_count).reduce((acc, curr) => acc + curr, 0),
1463
+ isKept: mode === "exclude" ? !insideRange : overlapsRange
1464
+ };
1465
+ });
584
1466
  return {
585
1467
  tooltip: {
586
1468
  trigger: "axis",
@@ -605,11 +1487,11 @@ const TimeGraph = memo(({ timeInfo, startDate, endDate }) => {
605
1487
  type: "bar",
606
1488
  name: "count",
607
1489
  barCategoryGap: 0,
608
- data: buckets.map(({ count, isInRange }) => ({
1490
+ data: buckets.map(({ count, isKept }) => ({
609
1491
  value: count,
610
1492
  itemStyle: {
611
1493
  color: theme.colors.blue[6],
612
- opacity: isInRange ? 1 : .25
1494
+ opacity: isKept ? 1 : .25
613
1495
  }
614
1496
  }))
615
1497
  }]
@@ -618,6 +1500,7 @@ const TimeGraph = memo(({ timeInfo, startDate, endDate }) => {
618
1500
  timeInfo,
619
1501
  startDate,
620
1502
  endDate,
1503
+ mode,
621
1504
  theme
622
1505
  ]);
623
1506
  return /* @__PURE__ */ jsx(EChartsReact, {
@@ -625,7 +1508,7 @@ const TimeGraph = memo(({ timeInfo, startDate, endDate }) => {
625
1508
  style: { height: 300 }
626
1509
  });
627
1510
  });
628
- const TimeFrameSlider = ({ timeInfo, startTime, endTime, onChange }) => {
1511
+ const TimeFrameSlider = ({ timeInfo, startTime, endTime, mode, onChange }) => {
629
1512
  const distribution = timeInfo.date_distribution;
630
1513
  const lastIndex = distribution.length - 1;
631
1514
  const startIndex = useMemo(() => {
@@ -645,6 +1528,7 @@ const TimeFrameSlider = ({ timeInfo, startTime, endTime, onChange }) => {
645
1528
  min: 0,
646
1529
  max: lastIndex,
647
1530
  minRange: 0,
1531
+ inverted: mode === "exclude",
648
1532
  value: [startIndex, endIndex],
649
1533
  label: (value) => {
650
1534
  const bucket = distribution[value];
@@ -655,84 +1539,99 @@ const TimeFrameSlider = ({ timeInfo, startTime, endTime, onChange }) => {
655
1539
  }
656
1540
  });
657
1541
  };
658
- const TimeFrameFilter = {
659
- title: "Timeframe",
660
- description: "Filters events by their timestamp, keeping only those that occur within the selected time range. Use the date pickers or drag the range slider to set the start and end; the bar chart shows the event distribution over time, with events inside the range highlighted and events outside it dimmed.",
661
- ViewComponent: memo(({ ocelId, control }) => {
662
- const { data: timeInfo, isLoading } = useTimeInfo(ocelId, {
663
- periods: 100,
664
- ocel_version: "original"
665
- });
666
- return /* @__PURE__ */ jsxs(Box, {
667
- pos: "relative",
668
- w: "100%",
669
- h: "100%",
670
- children: [/* @__PURE__ */ jsx(LoadingOverlay, { visible: isLoading }), timeInfo && /* @__PURE__ */ jsxs(Grid, {
671
- justify: "center",
672
- align: "center",
673
- children: [/* @__PURE__ */ jsx(Grid.Col, {
1542
+ const TimeFramePanel = memo(({ ocelId, filter, onChange, before }) => {
1543
+ const view = useView(ocelId, before);
1544
+ const { data: timeInfo, isLoading } = useTimeInfo(ocelId, {
1545
+ ...view,
1546
+ periods: 100
1547
+ }, { query: {
1548
+ enabled: view !== void 0,
1549
+ placeholderData: keepPreviousData
1550
+ } });
1551
+ const [start, end] = filter.time_range;
1552
+ const mode = filter.mode ?? "include";
1553
+ const setRange = (time_range) => onChange({
1554
+ ...filter,
1555
+ time_range
1556
+ });
1557
+ return /* @__PURE__ */ jsxs(Box, {
1558
+ pos: "relative",
1559
+ w: "100%",
1560
+ h: "100%",
1561
+ children: [/* @__PURE__ */ jsx(LoadingOverlay, { visible: isLoading }), timeInfo && /* @__PURE__ */ jsxs(Grid, {
1562
+ justify: "center",
1563
+ align: "center",
1564
+ children: [
1565
+ /* @__PURE__ */ jsx(Grid.Col, {
674
1566
  span: 12,
675
- children: /* @__PURE__ */ jsx(Watch, {
676
- control,
677
- name: ["time_frame.0.time_range.0", "time_frame.0.time_range.1"],
678
- render: ([startTime, endTime]) => /* @__PURE__ */ jsx(TimeGraph, {
679
- timeInfo,
680
- startDate: startTime ?? void 0,
681
- endDate: endTime ?? void 0
682
- })
683
- })
684
- }), /* @__PURE__ */ jsx(Controller, {
685
- control,
686
- name: "time_frame.0.time_range",
687
- defaultValue: [timeInfo.start_time, timeInfo.end_time],
688
- render: ({ field }) => /* @__PURE__ */ jsxs(Fragment, { children: [
689
- /* @__PURE__ */ jsx(Grid.Col, {
690
- span: 3,
691
- children: /* @__PURE__ */ jsx(DateTimePicker, {
692
- minDate: timeInfo.start_time,
693
- maxDate: field.value?.[1] ?? timeInfo.end_time,
694
- onChange: (newStart) => {
695
- field.onChange([newStart, field.value?.[1]]);
696
- },
697
- value: field.value?.[0] ?? timeInfo.start_time
698
- })
699
- }),
700
- /* @__PURE__ */ jsx(Grid.Col, {
701
- span: 6,
702
- children: /* @__PURE__ */ jsx(TimeFrameSlider, {
703
- onChange: field.onChange,
704
- timeInfo,
705
- startTime: field.value?.[0] ?? timeInfo.start_time,
706
- endTime: field.value?.[1] ?? timeInfo.end_time
707
- })
708
- }),
709
- /* @__PURE__ */ jsx(Grid.Col, {
710
- span: 3,
711
- children: /* @__PURE__ */ jsx(DateTimePicker, {
712
- minDate: field.value?.[0] ?? timeInfo.start_time,
713
- maxDate: timeInfo.end_time,
714
- onChange: (newEnd) => {
715
- field.onChange([field.value?.[0], newEnd]);
716
- },
717
- value: field.value?.[1] ?? timeInfo.end_time
1567
+ children: /* @__PURE__ */ jsx(Group, {
1568
+ justify: "flex-end",
1569
+ children: /* @__PURE__ */ jsx(SegmentedControl, {
1570
+ size: "xs",
1571
+ data: MODE_OPTIONS,
1572
+ value: mode,
1573
+ onChange: (next) => onChange({
1574
+ ...filter,
1575
+ mode: next
718
1576
  })
719
1577
  })
720
- ] })
721
- })]
722
- })]
723
- });
724
- }),
725
- generateDefault: () => [{
1578
+ })
1579
+ }),
1580
+ /* @__PURE__ */ jsx(Grid.Col, {
1581
+ span: 12,
1582
+ children: /* @__PURE__ */ jsx(TimeGraph, {
1583
+ timeInfo,
1584
+ startDate: start ?? void 0,
1585
+ endDate: end ?? void 0,
1586
+ mode
1587
+ })
1588
+ }),
1589
+ /* @__PURE__ */ jsx(Grid.Col, {
1590
+ span: 3,
1591
+ children: /* @__PURE__ */ jsx(DateTimePicker, {
1592
+ minDate: timeInfo.start_time,
1593
+ maxDate: end ?? timeInfo.end_time,
1594
+ onChange: (newStart) => setRange([newStart, end]),
1595
+ value: start ?? timeInfo.start_time
1596
+ })
1597
+ }),
1598
+ /* @__PURE__ */ jsx(Grid.Col, {
1599
+ span: 6,
1600
+ children: /* @__PURE__ */ jsx(TimeFrameSlider, {
1601
+ onChange: setRange,
1602
+ timeInfo,
1603
+ startTime: start ?? timeInfo.start_time,
1604
+ endTime: end ?? timeInfo.end_time,
1605
+ mode
1606
+ })
1607
+ }),
1608
+ /* @__PURE__ */ jsx(Grid.Col, {
1609
+ span: 3,
1610
+ children: /* @__PURE__ */ jsx(DateTimePicker, {
1611
+ minDate: start ?? timeInfo.start_time,
1612
+ maxDate: timeInfo.end_time,
1613
+ onChange: (newEnd) => setRange([start, newEnd]),
1614
+ value: end ?? timeInfo.end_time
1615
+ })
1616
+ })
1617
+ ]
1618
+ })]
1619
+ });
1620
+ });
1621
+ const bound = (value, open) => value ? dayjs_default(value).format(DATE_FORMAT) : open;
1622
+ const TimeFrameFilter = {
1623
+ title: "Timeframe",
1624
+ description: "Keeps only the events that occur within the selected time range, or with Exclude, removes them and keeps everything outside it. Use the date pickers or drag the range slider to set the start and end; the bar chart shows the event distribution over time, with kept events highlighted and removed events dimmed.",
1625
+ Panel: TimeFramePanel,
1626
+ create: () => ({
726
1627
  type: "time_frame",
727
- time_range: [null, null]
728
- }],
729
- cleanUpFilters: (currentFilter) => {
730
- if (!currentFilter[0] || currentFilter[0].time_range[0] == null && currentFilter[0].time_range[1] == null) return [];
731
- return [currentFilter[0]];
732
- }
1628
+ time_range: [null, null],
1629
+ mode: "include"
1630
+ }),
1631
+ summary: ({ time_range: [start, end], mode }) => start || end ? `${mode === "exclude" ? "Excluding " : ""}${bound(start, "start")} – ${bound(end, "end")}` : "No range set",
1632
+ isComplete: ({ time_range: [start, end] }) => start != null || end != null
733
1633
  };
734
- //#endregion
735
- //#region src/components/FilterViews/TypeForm.tsx
1634
+ // src/components/FilterViews/TypeForm.tsx
736
1635
  const PICKER_CLASS = "ocelescope-type-filter-picker";
737
1636
  const FillHeightStyles = () => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(InlineStyles, {
738
1637
  deduplicate: true,
@@ -750,71 +1649,92 @@ const FillHeightStyles = () => /* @__PURE__ */ jsxs(Fragment, { children: [/* @_
750
1649
  maxHeight: "none !important"
751
1650
  }
752
1651
  })] });
753
- const EntityFilter = (entityType) => ({ ocelId, control }) => {
754
- const isEvents = entityType === "events";
755
- const Picker = isEvents ? ActivityPicker : ObjectTypePicker;
756
- return /* @__PURE__ */ jsx(Controller, {
757
- name: isEvents ? `activity.0` : `object_type.0`,
758
- control,
759
- render: ({ field }) => {
760
- const filter = field.value;
761
- const selected = filter?.type === "activity" ? filter.event_types : filter?.type === "object_type" ? filter.object_types : [];
762
- return /* @__PURE__ */ jsxs(Stack, {
763
- gap: 4,
764
- flex: 1,
765
- mih: 0,
766
- className: PICKER_CLASS,
767
- children: [/* @__PURE__ */ jsx(FillHeightStyles, {}), /* @__PURE__ */ jsx(Picker, {
768
- variant: "list",
769
- multiple: true,
770
- cutoff: true,
771
- ocelId,
772
- ocelVersion: "original",
773
- value: selected,
774
- onChange: (next) => field.onChange(isEvents ? {
775
- type: "activity",
776
- event_types: next,
777
- mode: "include"
778
- } : {
779
- type: "object_type",
780
- object_types: next,
781
- mode: "include"
782
- })
783
- })]
784
- });
785
- }
1652
+ const listed = (names, noun) => {
1653
+ if (names.length === 0) return `No ${noun} selected`;
1654
+ const shown = names.slice(0, 2).join(", ");
1655
+ return names.length > 2 ? `${shown} +${names.length - 2} more` : shown;
1656
+ };
1657
+ const ActivityPanel = ({ ocelId, filter, onChange, before }) => {
1658
+ const view = useView(ocelId, before);
1659
+ return /* @__PURE__ */ jsx(Previewed, {
1660
+ query: {
1661
+ data: view,
1662
+ isPending: true,
1663
+ error: null
1664
+ },
1665
+ children: (view) => /* @__PURE__ */ jsxs(Stack, {
1666
+ gap: 4,
1667
+ flex: 1,
1668
+ mih: 0,
1669
+ className: PICKER_CLASS,
1670
+ children: [/* @__PURE__ */ jsx(FillHeightStyles, {}), /* @__PURE__ */ jsx(ActivityPicker, {
1671
+ variant: "list",
1672
+ multiple: true,
1673
+ cutoff: true,
1674
+ ocelId,
1675
+ ...viewProps(view),
1676
+ value: filter.event_types,
1677
+ onChange: (next) => onChange({
1678
+ ...filter,
1679
+ event_types: next
1680
+ })
1681
+ })]
1682
+ })
1683
+ });
1684
+ };
1685
+ const ObjectTypePanel = ({ ocelId, filter, onChange, before }) => {
1686
+ const view = useView(ocelId, before);
1687
+ return /* @__PURE__ */ jsx(Previewed, {
1688
+ query: {
1689
+ data: view,
1690
+ isPending: true,
1691
+ error: null
1692
+ },
1693
+ children: (view) => /* @__PURE__ */ jsxs(Stack, {
1694
+ gap: 4,
1695
+ flex: 1,
1696
+ mih: 0,
1697
+ className: PICKER_CLASS,
1698
+ children: [/* @__PURE__ */ jsx(FillHeightStyles, {}), /* @__PURE__ */ jsx(ObjectTypePicker, {
1699
+ variant: "list",
1700
+ multiple: true,
1701
+ cutoff: true,
1702
+ ocelId,
1703
+ ...viewProps(view),
1704
+ value: filter.object_types,
1705
+ onChange: (next) => onChange({
1706
+ ...filter,
1707
+ object_types: next
1708
+ })
1709
+ })]
1710
+ })
786
1711
  });
787
1712
  };
788
- //#endregion
789
- //#region src/components/FilterViews/index.tsx
790
- const FILTER_MAP = {
1713
+ // src/components/FilterViews/index.tsx
1714
+ const FILTER_KINDS = {
791
1715
  activity: {
792
1716
  title: "Activity",
793
- description: "Filters the event log by activity (the event type). Only events of the selected activities are kept. The list shows how many events exist per activity to help you decide what to keep.",
794
- ViewComponent: EntityFilter("events"),
795
- generateDefault: () => [{
1717
+ description: "Keeps only the events of the selected activities (the event types). The list shows how many events exist per activity to help you decide what to keep.",
1718
+ Panel: ActivityPanel,
1719
+ create: () => ({
796
1720
  type: "activity",
797
1721
  event_types: [],
798
1722
  mode: "include"
799
- }],
800
- cleanUpFilters: (filter) => {
801
- if (!filter[0] || filter[0].event_types.length === 0) return [];
802
- return [filter[0]];
803
- }
1723
+ }),
1724
+ summary: (filter) => listed(filter.event_types, "activities"),
1725
+ isComplete: (filter) => filter.event_types.length > 0
804
1726
  },
805
1727
  object_type: {
806
1728
  title: "Object Type",
807
- description: "Filters the log by object type. Only objects of the selected types are kept. The list shows how many objects exist per type to help you decide what to keep.",
808
- ViewComponent: EntityFilter("objects"),
809
- generateDefault: () => [{
1729
+ description: "Keeps only the objects of the selected types. The list shows how many objects exist per type to help you decide what to keep.",
1730
+ Panel: ObjectTypePanel,
1731
+ create: () => ({
810
1732
  type: "object_type",
811
1733
  object_types: [],
812
1734
  mode: "include"
813
- }],
814
- cleanUpFilters: (filter) => {
815
- if (!filter[0] || filter[0].object_types.length === 0) return [];
816
- return [filter[0]];
817
- }
1735
+ }),
1736
+ summary: (filter) => listed(filter.object_types, "object types"),
1737
+ isComplete: (filter) => filter.object_types.length > 0
818
1738
  },
819
1739
  time_frame: TimeFrameFilter,
820
1740
  event_attribute: EventAttributeFilter,
@@ -822,100 +1742,327 @@ const FILTER_MAP = {
822
1742
  e2o_count: E2OCountFilter,
823
1743
  o2o_count: O2OCountFilter
824
1744
  };
825
- const generateDefaultFilter = (currentFilters) => {
826
- const grouped = {};
827
- for (const filter of currentFilters) grouped[filter.type] = [...grouped[filter.type] ?? [], filter];
828
- const groupedFilter = grouped;
829
- const missingFilterKeys = Object.keys(FILTER_MAP).filter((key) => !(key in groupedFilter));
830
- return {
831
- ...groupedFilter,
832
- ...Object.fromEntries(missingFilterKeys.map((key) => [key, FILTER_MAP[key].generateDefault()]))
833
- };
834
- };
835
- const defaultFilter = (filters) => {
836
- if (!filters || filters.length === 0) return [];
837
- return filters;
838
- };
839
- const cleanUpFilterByKey = (key, filters) => {
840
- return (FILTER_MAP[key].cleanUpFilters ?? defaultFilter)(filters);
841
- };
842
- const cleanUpFilters = (filter) => {
843
- return Object.keys(filter).flatMap((key) => {
844
- const filters = filter[key];
845
- if (!filters) return [];
846
- return cleanUpFilterByKey(key, filters);
847
- }).filter((filter) => !!filter);
848
- };
849
- //#endregion
850
- //#region src/components/FilterForm.tsx
851
- const FilterForm = ({ ocelId, currentFilter, onSubmit }) => {
852
- const { control, handleSubmit, reset, formState } = useForm({ defaultValues: generateDefaultFilter(currentFilter) });
853
- const { currentTab, setCurrentTab } = useCurrentFilterTab({
854
- tabs: Object.keys(FILTER_MAP),
855
- defaultTab: "activity",
856
- queryKey: "tab"
857
- });
858
- return /* @__PURE__ */ jsxs(Tabs, {
859
- value: currentTab,
860
- onChange: (newTab) => setCurrentTab(newTab),
861
- keepMounted: false,
1745
+ const kindOf = (filter) => FILTER_KINDS[filter.type];
1746
+ const completeSteps = (steps) => steps.filter((step) => kindOf(step).isComplete(step));
1747
+ // src/components/Pipeline/PipelineStep.tsx
1748
+ const PipelineStep = ({ id, index, step, selected, onSelect, onRemove }) => {
1749
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
1750
+ const kind = kindOf(step);
1751
+ const complete = kind.isComplete(step);
1752
+ return /* @__PURE__ */ jsx(Paper, {
1753
+ ref: setNodeRef,
1754
+ withBorder: true,
1755
+ onClick: onSelect,
1756
+ bg: selected ? "var(--mantine-primary-color-light)" : void 0,
862
1757
  style: {
863
- display: "flex",
864
- flexDirection: "column"
1758
+ cursor: "pointer",
1759
+ overflow: "hidden",
1760
+ transform: CSS.Transform.toString(transform),
1761
+ transition,
1762
+ opacity: isDragging ? .6 : 1,
1763
+ zIndex: isDragging ? 1 : void 0,
1764
+ position: "relative"
865
1765
  },
866
- h: "100%",
867
- children: [/* @__PURE__ */ jsxs(Group, {
868
- py: "xs",
1766
+ children: /* @__PURE__ */ jsxs(Group, {
869
1767
  gap: 0,
870
1768
  wrap: "nowrap",
871
- children: [/* @__PURE__ */ jsx(Tabs.List, {
1769
+ align: "stretch",
1770
+ children: [/* @__PURE__ */ jsx(UnstyledButton, {
1771
+ "aria-label": "Drag to reorder",
1772
+ w: 24,
1773
+ display: "flex",
1774
+ bg: "var(--mantine-color-default-hover)",
1775
+ c: "dimmed",
1776
+ style: {
1777
+ alignItems: "center",
1778
+ justifyContent: "center",
1779
+ cursor: isDragging ? "grabbing" : "grab",
1780
+ touchAction: "none",
1781
+ borderRight: "1px solid var(--mantine-color-default-border)"
1782
+ },
1783
+ onClick: (event) => event.stopPropagation(),
1784
+ ...attributes,
1785
+ ...listeners,
1786
+ children: /* @__PURE__ */ jsx(GripVerticalIcon, { size: 14 })
1787
+ }), /* @__PURE__ */ jsxs(Group, {
1788
+ gap: "xs",
1789
+ wrap: "nowrap",
1790
+ align: "start",
872
1791
  flex: 1,
873
1792
  miw: 0,
874
- children: /* @__PURE__ */ jsx(Scroller, { children: Object.entries(FILTER_MAP).map(([key, { title }]) => /* @__PURE__ */ jsx(Tabs.Tab, {
875
- value: key,
876
- children: title
877
- }, key)) })
878
- }), /* @__PURE__ */ jsxs(ActionIconGroup, { children: [formState.isDirty && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ActionIcon, {
879
- onClick: handleSubmit((data) => {
880
- onSubmit?.(cleanUpFilters(data));
881
- }),
882
- size: "lg",
883
- color: "green",
884
- children: /* @__PURE__ */ jsx(CheckIcon, { size: 24 })
885
- }), /* @__PURE__ */ jsx(ActionIcon, {
886
- size: "lg",
887
- color: "yellow",
888
- onClick: () => reset(generateDefaultFilter(currentFilter)),
889
- children: /* @__PURE__ */ jsx(RotateCcwIcon, { size: 24 })
890
- })] }), currentFilter.length > 0 && /* @__PURE__ */ jsx(ActionIcon, {
891
- size: "lg",
892
- color: "red",
893
- onClick: () => onSubmit?.([]),
894
- children: /* @__PURE__ */ jsx(Trash2Icon, { size: 24 })
895
- })] })]
896
- }), Object.entries(FILTER_MAP).map(([key, { ViewComponent, description }]) => /* @__PURE__ */ jsx(Tabs.Panel, {
897
- value: key,
898
- p: "md",
1793
+ p: "xs",
1794
+ children: [/* @__PURE__ */ jsxs(Stack, {
1795
+ gap: 2,
1796
+ flex: 1,
1797
+ miw: 0,
1798
+ children: [/* @__PURE__ */ jsxs(Group, {
1799
+ gap: 6,
1800
+ wrap: "nowrap",
1801
+ children: [
1802
+ /* @__PURE__ */ jsx(Text, {
1803
+ size: "xs",
1804
+ c: "dimmed",
1805
+ children: index + 1
1806
+ }),
1807
+ /* @__PURE__ */ jsx(Text, {
1808
+ size: "sm",
1809
+ fw: 500,
1810
+ truncate: true,
1811
+ children: kind.title
1812
+ }),
1813
+ !complete && /* @__PURE__ */ jsx(Badge, {
1814
+ size: "xs",
1815
+ color: "yellow",
1816
+ variant: "light",
1817
+ children: "incomplete"
1818
+ })
1819
+ ]
1820
+ }), /* @__PURE__ */ jsx(Text, {
1821
+ size: "xs",
1822
+ c: "dimmed",
1823
+ truncate: true,
1824
+ children: kind.summary(step)
1825
+ })]
1826
+ }), /* @__PURE__ */ jsx(ActionIcon, {
1827
+ variant: "subtle",
1828
+ color: "red",
1829
+ size: "sm",
1830
+ "aria-label": "Remove step",
1831
+ onClick: (event) => {
1832
+ event.stopPropagation();
1833
+ onRemove();
1834
+ },
1835
+ children: /* @__PURE__ */ jsx(XIcon, { size: 14 })
1836
+ })]
1837
+ })]
1838
+ })
1839
+ });
1840
+ };
1841
+ // src/components/Pipeline/PipelineList.tsx
1842
+ const KEYS = Object.keys(FILTER_KINDS);
1843
+ const PipelineList = ({ ids, steps, selectedId, onSelect, onRemove, onMove, onAdd, ...stackProps }) => {
1844
+ const [search, setSearch] = useState("");
1845
+ const shown = KEYS.filter((key) => FILTER_KINDS[key].title.toLowerCase().includes(search.trim().toLowerCase()));
1846
+ const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
1847
+ const onDragEnd = ({ active, over }) => {
1848
+ if (!over || active.id === over.id) return;
1849
+ onMove(ids.indexOf(String(active.id)), ids.indexOf(String(over.id)));
1850
+ };
1851
+ return /* @__PURE__ */ jsxs(Stack, {
1852
+ gap: "xs",
1853
+ ...stackProps,
1854
+ children: [/* @__PURE__ */ jsx(ScrollArea, {
899
1855
  flex: 1,
900
1856
  mih: 0,
901
- style: { overflow: "auto" },
1857
+ type: "auto",
1858
+ scrollbarSize: 6,
902
1859
  children: /* @__PURE__ */ jsxs(Stack, {
903
- gap: "md",
904
- h: "100%",
905
- children: [/* @__PURE__ */ jsxs(Text, {
1860
+ gap: "xs",
1861
+ children: [ids.length === 0 && /* @__PURE__ */ jsx(Text, {
906
1862
  size: "sm",
907
1863
  c: "dimmed",
908
- children: [description, " Afterwards the log is cleaned up automatically: relations whose source or target is missing are removed, events without at least one E2O relation are filtered out, and objects without any E2O or O2O relation are filtered out too."]
909
- }), /* @__PURE__ */ jsx(ViewComponent, {
910
- control,
911
- ocelId
1864
+ children: "No filters yet. Add one to start the pipeline."
1865
+ }), /* @__PURE__ */ jsx(DndContext, {
1866
+ sensors,
1867
+ collisionDetection: closestCenter,
1868
+ onDragEnd,
1869
+ children: /* @__PURE__ */ jsx(SortableContext, {
1870
+ items: ids,
1871
+ strategy: verticalListSortingStrategy,
1872
+ children: ids.map((id, index) => {
1873
+ const step = steps[index];
1874
+ return step && /* @__PURE__ */ jsx(PipelineStep, {
1875
+ id,
1876
+ index,
1877
+ step,
1878
+ selected: id === selectedId,
1879
+ onSelect: () => onSelect(id),
1880
+ onRemove: () => onRemove(index)
1881
+ }, id);
1882
+ })
1883
+ })
912
1884
  })]
913
1885
  })
914
- }, key))]
1886
+ }), /* @__PURE__ */ jsxs(Menu, {
1887
+ position: "bottom-start",
1888
+ width: "target",
1889
+ children: [/* @__PURE__ */ jsx(Menu.Target, { children: /* @__PURE__ */ jsx(Button, {
1890
+ variant: "light",
1891
+ leftSection: /* @__PURE__ */ jsx(PlusIcon, { size: 16 }),
1892
+ fullWidth: true,
1893
+ children: "Add filter"
1894
+ }) }), /* @__PURE__ */ jsxs(Menu.Dropdown, { children: [
1895
+ /* @__PURE__ */ jsx(Menu.Search, {
1896
+ value: search,
1897
+ onChange: (event) => setSearch(event.currentTarget.value),
1898
+ placeholder: "Search filters"
1899
+ }),
1900
+ shown.map((key) => /* @__PURE__ */ jsx(Menu.Item, {
1901
+ onClick: () => onAdd(key),
1902
+ children: FILTER_KINDS[key].title
1903
+ }, key)),
1904
+ shown.length === 0 && /* @__PURE__ */ jsx(Menu.Label, { children: "No filter matches" })
1905
+ ] })]
1906
+ })]
1907
+ });
1908
+ };
1909
+ // src/components/FilterForm.tsx
1910
+ const CLEANUP_NOTE = "The steps run from top to bottom. After the last step the log is cleaned up: relations whose event or object is gone are removed, events without at least one E2O relation are filtered out, and objects without any E2O or O2O relation are filtered out too.";
1911
+ const FilterForm = ({ ocelId, currentFilter, onSubmit }) => {
1912
+ const { control, handleSubmit, reset, formState } = useForm({ defaultValues: { steps: currentFilter } });
1913
+ const { fields, append, remove, move } = useFieldArray({
1914
+ control,
1915
+ name: "steps"
1916
+ });
1917
+ const steps = useWatch({
1918
+ control,
1919
+ name: "steps"
1920
+ });
1921
+ const [selectedId, setSelectedId] = useState(fields[0]?.id);
1922
+ const ids = fields.map(({ id }) => id);
1923
+ const selectedIndex = fields.findIndex(({ id }) => id === selectedId);
1924
+ const selectedStep = steps[selectedIndex];
1925
+ const before = completeSteps(steps.slice(0, Math.max(selectedIndex, 0)));
1926
+ const selectAdded = useRef(false);
1927
+ useEffect(() => {
1928
+ if (selectAdded.current) {
1929
+ selectAdded.current = false;
1930
+ setSelectedId(fields[fields.length - 1]?.id);
1931
+ }
1932
+ }, [fields]);
1933
+ const addStep = (key) => {
1934
+ selectAdded.current = true;
1935
+ append(FILTER_KINDS[key].create());
1936
+ };
1937
+ return /* @__PURE__ */ jsxs(Group, {
1938
+ h: "100%",
1939
+ gap: 0,
1940
+ wrap: "nowrap",
1941
+ align: "stretch",
1942
+ children: [/* @__PURE__ */ jsx(Box, {
1943
+ flex: 1,
1944
+ miw: 0,
1945
+ p: "md",
1946
+ style: { overflow: "auto" },
1947
+ children: selectedStep ? /* @__PURE__ */ jsxs(Stack, {
1948
+ gap: "md",
1949
+ h: "100%",
1950
+ children: [/* @__PURE__ */ jsxs(Stack, {
1951
+ gap: 4,
1952
+ children: [/* @__PURE__ */ jsxs(Title, {
1953
+ order: 4,
1954
+ children: [
1955
+ selectedIndex + 1,
1956
+ ". ",
1957
+ kindOf(selectedStep).title
1958
+ ]
1959
+ }), /* @__PURE__ */ jsx(Text, {
1960
+ size: "sm",
1961
+ c: "dimmed",
1962
+ children: kindOf(selectedStep).description
1963
+ })]
1964
+ }), /* @__PURE__ */ jsx(Controller, {
1965
+ control,
1966
+ name: `steps.${selectedIndex}`,
1967
+ render: ({ field }) => {
1968
+ const { Panel } = kindOf(field.value);
1969
+ return /* @__PURE__ */ jsx(Panel, {
1970
+ ocelId,
1971
+ filter: field.value,
1972
+ before,
1973
+ onChange: (step) => field.onChange({ target: { value: step } })
1974
+ });
1975
+ }
1976
+ }, selectedId)]
1977
+ }) : /* @__PURE__ */ jsx(Center, {
1978
+ h: "100%",
1979
+ children: /* @__PURE__ */ jsx(Text, {
1980
+ c: "dimmed",
1981
+ size: "sm",
1982
+ maw: 360,
1983
+ ta: "center",
1984
+ children: fields.length === 0 ? "Add a filter to the pipeline on the right to start filtering." : "Select a step on the right to edit it."
1985
+ })
1986
+ })
1987
+ }), /* @__PURE__ */ jsxs(Stack, {
1988
+ w: 320,
1989
+ gap: "sm",
1990
+ py: "md",
1991
+ style: { borderLeft: "1px solid var(--mantine-color-default-border)" },
1992
+ children: [
1993
+ /* @__PURE__ */ jsxs(Group, {
1994
+ justify: "space-between",
1995
+ wrap: "nowrap",
1996
+ h: 28,
1997
+ px: "md",
1998
+ children: [/* @__PURE__ */ jsxs(Group, {
1999
+ gap: 6,
2000
+ wrap: "nowrap",
2001
+ children: [/* @__PURE__ */ jsx(Title, {
2002
+ order: 5,
2003
+ lh: 1,
2004
+ children: "Pipeline"
2005
+ }), /* @__PURE__ */ jsx(Tooltip, {
2006
+ label: CLEANUP_NOTE,
2007
+ multiline: true,
2008
+ w: 280,
2009
+ withArrow: true,
2010
+ children: /* @__PURE__ */ jsx(InfoIcon, {
2011
+ size: 16,
2012
+ color: "var(--mantine-color-dimmed)",
2013
+ "aria-label": "How the pipeline is applied"
2014
+ })
2015
+ })]
2016
+ }), /* @__PURE__ */ jsxs(Group, {
2017
+ justify: "flex-end",
2018
+ gap: 4,
2019
+ wrap: "nowrap",
2020
+ ml: "auto",
2021
+ children: [formState.isDirty && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Tooltip, {
2022
+ label: "Apply",
2023
+ children: /* @__PURE__ */ jsx(ActionIcon, {
2024
+ variant: "subtle",
2025
+ onClick: handleSubmit((data) => onSubmit?.(completeSteps(data.steps))),
2026
+ color: "green",
2027
+ children: /* @__PURE__ */ jsx(CheckIcon, { size: 20 })
2028
+ })
2029
+ }), /* @__PURE__ */ jsx(Tooltip, {
2030
+ label: "Discard changes",
2031
+ children: /* @__PURE__ */ jsx(ActionIcon, {
2032
+ variant: "subtle",
2033
+ color: "yellow",
2034
+ onClick: () => reset({ steps: currentFilter }),
2035
+ children: /* @__PURE__ */ jsx(RotateCcwIcon, { size: 20 })
2036
+ })
2037
+ })] }), currentFilter.length > 0 && /* @__PURE__ */ jsx(Tooltip, {
2038
+ label: "Remove all filters",
2039
+ children: /* @__PURE__ */ jsx(ActionIcon, {
2040
+ variant: "subtle",
2041
+ color: "red",
2042
+ onClick: () => onSubmit?.([]),
2043
+ children: /* @__PURE__ */ jsx(Trash2Icon, { size: 20 })
2044
+ })
2045
+ })]
2046
+ })]
2047
+ }),
2048
+ /* @__PURE__ */ jsx(Divider, {}),
2049
+ /* @__PURE__ */ jsx(PipelineList, {
2050
+ flex: 1,
2051
+ mih: 0,
2052
+ px: "md",
2053
+ ids,
2054
+ steps,
2055
+ selectedId,
2056
+ onSelect: setSelectedId,
2057
+ onRemove: remove,
2058
+ onMove: move,
2059
+ onAdd: addStep
2060
+ })
2061
+ ]
2062
+ })]
915
2063
  });
916
2064
  };
917
- //#endregion
918
- //#region src/components/FilterPage.tsx
2065
+ // src/components/FilterPage.tsx
919
2066
  const FilterPage = () => {
920
2067
  const { id: ocelId } = useCurrentOcel();
921
2068
  const { data: filterPipeline, refetch } = useGetFilter(ocelId ?? "", { query: { enabled: !!ocelId } });
@@ -939,8 +2086,7 @@ var FilterPage_default = defineModuleRoute({
939
2086
  name: "filter",
940
2087
  requiresOcel: true
941
2088
  });
942
- //#endregion
943
- //#region src/index.ts
2089
+ // src/index.ts
944
2090
  var src_default = defineModule({
945
2091
  name: "filter",
946
2092
  description: "Filter functionality provided by the ocelescope package",
@@ -949,5 +2095,4 @@ var src_default = defineModule({
949
2095
  routes: [FilterPage_default],
950
2096
  icon: FilterIcon
951
2097
  });
952
- //#endregion
953
2098
  export { src_default as default };