@ocelescope/filter 0.10.1 → 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.
package/dist/index.js CHANGED
@@ -1,22 +1,25 @@
1
- import { AttributesTable, 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, LoadingOverlay, MultiSelect, NumberInput, Paper, RangeSlider, ScrollArea, Scroller, Stack, Switch, Tabs, Text, TextInput } 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 { BarChart } from "@mantine/charts";
13
- import { useEventCounts, useObjectCounts, useTimeInfo } from "@ocelescope/api-base";
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,508 +152,1363 @@ 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
+ ]
192
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
+ ]
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`
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
240
832
  });
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
247
- });
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")}`;
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` : ""}`;
275
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)}`;
280
892
  };
281
- const generateRecordId = (attribute) => {
282
- const entityName = getEntityType(attribute);
283
- return `${attribute.name}::${entityName ?? ""}`;
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 !== ""
903
+ };
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 !== ""
914
+ };
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
+ })]
1038
+ });
284
1039
  };
285
- const useAttributeFilter = ({ path, control }) => {
286
- const { fields, append, remove } = useFieldArray({
287
- name: path,
288
- control
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
289
1051
  });
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)
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
1239
+ const select = (relation) => onChange({
1240
+ ...filter,
1241
+ ...relation,
1242
+ direction: "source",
1243
+ range: [null, null]
382
1244
  });
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
404
- });
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
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
424
1277
  });
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"
449
- });
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, {
1392
+ ocelId,
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, {
547
1405
  ocelId,
548
- ocelVersion: "original",
549
- relationType,
550
- extraColumns: filterColumns,
551
- subTableExtraColumns: filterColumns,
552
- visibleRelations,
553
- hideRange: true,
554
- hideTotal: true
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
575
- const TimeGraph = memo(({ timeInfo, startDate, endDate }) => {
576
- const data = useMemo(() => {
577
- return timeInfo.date_distribution.map(({ start_timestamp, end_timestamp, entity_count }) => {
578
- const isInRange = (!startDate || dayjs_default(end_timestamp).isAfter(dayjs_default(startDate))) && (!endDate || dayjs_default(start_timestamp).isBefore(dayjs_default(endDate)));
1445
+ // src/components/FilterViews/TimeFrameFilter.tsx
1446
+ const DATE_FORMAT = "YYYY-MM-DD HH:mm";
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 }) => {
1455
+ const theme = useMantineTheme();
1456
+ const option = useMemo(() => {
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)));
579
1460
  return {
580
- date: `${dayjs_default(start_timestamp).format("YYYY-MM-DD HH:mm")}-${dayjs_default(end_timestamp).format("YYYY-MM-DD HH:mm")} `,
581
- ...isInRange ? { value: Object.values(entity_count).reduce((acc, curr) => acc + curr, 0) } : { disabledValue: Object.values(entity_count).reduce((acc, curr) => acc + curr, 0) }
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
582
1464
  };
583
1465
  });
1466
+ return {
1467
+ tooltip: {
1468
+ trigger: "axis",
1469
+ axisPointer: { type: "shadow" }
1470
+ },
1471
+ grid: {
1472
+ left: 0,
1473
+ right: 0,
1474
+ top: 8,
1475
+ bottom: 0
1476
+ },
1477
+ xAxis: {
1478
+ type: "category",
1479
+ data: buckets.map(({ label }) => label),
1480
+ show: false
1481
+ },
1482
+ yAxis: {
1483
+ type: "value",
1484
+ show: false
1485
+ },
1486
+ series: [{
1487
+ type: "bar",
1488
+ name: "count",
1489
+ barCategoryGap: 0,
1490
+ data: buckets.map(({ count, isKept }) => ({
1491
+ value: count,
1492
+ itemStyle: {
1493
+ color: theme.colors.blue[6],
1494
+ opacity: isKept ? 1 : .25
1495
+ }
1496
+ }))
1497
+ }]
1498
+ };
584
1499
  }, [
585
1500
  timeInfo,
586
1501
  startDate,
587
- endDate
1502
+ endDate,
1503
+ mode,
1504
+ theme
588
1505
  ]);
589
- return /* @__PURE__ */ jsx(BarChart, {
590
- h: 300,
591
- w: "100%",
592
- data,
593
- dataKey: "date",
594
- type: "stacked",
595
- series: [{
596
- name: "value",
597
- color: "blue",
598
- label: "count"
599
- }, {
600
- name: "disabledValue",
601
- color: "red",
602
- label: "count"
603
- }],
604
- withYAxis: false,
605
- withXAxis: false,
606
- barChartProps: {
607
- barCategoryGap: 0,
608
- barGap: 0
609
- }
1506
+ return /* @__PURE__ */ jsx(EChartsReact, {
1507
+ option,
1508
+ style: { height: 300 }
610
1509
  });
611
1510
  });
612
- const TimeFrameSlider = ({ timeInfo, startTime, endTime, onChange }) => {
1511
+ const TimeFrameSlider = ({ timeInfo, startTime, endTime, mode, onChange }) => {
613
1512
  const distribution = timeInfo.date_distribution;
614
1513
  const lastIndex = distribution.length - 1;
615
1514
  const startIndex = useMemo(() => {
@@ -629,281 +1528,213 @@ const TimeFrameSlider = ({ timeInfo, startTime, endTime, onChange }) => {
629
1528
  min: 0,
630
1529
  max: lastIndex,
631
1530
  minRange: 0,
1531
+ inverted: mode === "exclude",
632
1532
  value: [startIndex, endIndex],
633
1533
  label: (value) => {
634
1534
  const bucket = distribution[value];
635
- return bucket ? dayjs_default(bucket.start_timestamp).format("YYYY-MM-DD HH:mm") : null;
1535
+ return bucket ? dayjs_default(bucket.start_timestamp).format(DATE_FORMAT) : null;
636
1536
  },
637
1537
  onChange: ([start, end]) => {
638
1538
  onChange([start === startIndex ? startTime : distribution[start]?.start_timestamp ?? startTime, distribution[end]?.end_timestamp ?? endTime]);
639
1539
  }
640
1540
  });
641
1541
  };
642
- const TimeFrameFilter = {
643
- title: "Timeframe",
644
- 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.",
645
- ViewComponent: memo(({ ocelId, control }) => {
646
- const { data: timeInfo, isLoading } = useTimeInfo(ocelId, {
647
- periods: 100,
648
- ocel_version: "original"
649
- });
650
- return /* @__PURE__ */ jsxs(Box, {
651
- pos: "relative",
652
- w: "100%",
653
- h: "100%",
654
- children: [/* @__PURE__ */ jsx(LoadingOverlay, { visible: isLoading }), timeInfo && /* @__PURE__ */ jsxs(Grid, {
655
- justify: "center",
656
- align: "center",
657
- 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, {
658
1566
  span: 12,
659
- children: /* @__PURE__ */ jsx(Watch, {
660
- control,
661
- name: ["time_frame.0.time_range.0", "time_frame.0.time_range.1"],
662
- render: ([startTime, endTime]) => {
663
- return /* @__PURE__ */ jsx(TimeGraph, {
664
- timeInfo,
665
- startDate: startTime ?? void 0,
666
- endDate: endTime ?? void 0
667
- });
668
- }
669
- })
670
- }), /* @__PURE__ */ jsx(Controller, {
671
- control,
672
- name: "time_frame.0.time_range",
673
- defaultValue: [timeInfo.start_time, timeInfo.end_time],
674
- render: ({ field }) => /* @__PURE__ */ jsxs(Fragment, { children: [
675
- /* @__PURE__ */ jsx(Grid.Col, {
676
- span: 3,
677
- children: /* @__PURE__ */ jsx(DateTimePicker, {
678
- minDate: timeInfo.start_time,
679
- maxDate: field.value?.[1] ?? timeInfo.end_time,
680
- onChange: (newStart) => {
681
- field.onChange([newStart, field.value?.[1]]);
682
- },
683
- value: field.value?.[0] ?? timeInfo.start_time
684
- })
685
- }),
686
- /* @__PURE__ */ jsx(Grid.Col, {
687
- span: 6,
688
- children: /* @__PURE__ */ jsx(TimeFrameSlider, {
689
- onChange: field.onChange,
690
- timeInfo,
691
- startTime: field.value?.[0] ?? timeInfo.start_time,
692
- endTime: field.value?.[1] ?? timeInfo.end_time
693
- })
694
- }),
695
- /* @__PURE__ */ jsx(Grid.Col, {
696
- span: 3,
697
- children: /* @__PURE__ */ jsx(DateTimePicker, {
698
- minDate: field.value?.[0] ?? timeInfo.start_time,
699
- maxDate: timeInfo.end_time,
700
- onChange: (newEnd) => {
701
- field.onChange([field.value?.[0], newEnd]);
702
- },
703
- 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
704
1576
  })
705
1577
  })
706
- ] })
707
- })]
708
- })]
709
- });
710
- }),
711
- 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: () => ({
712
1627
  type: "time_frame",
713
- time_range: [null, null]
714
- }],
715
- cleanUpFilters: (currentFilter) => {
716
- if (!currentFilter[0] || currentFilter[0].time_range[0] == null && currentFilter[0].time_range[1] == null) return [];
717
- return [currentFilter[0]];
718
- }
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
719
1633
  };
720
- //#endregion
721
- //#region src/util/color.ts
722
- const assignUniqueColors = (strings) => {
723
- const numStrings = strings.length;
724
- const uniqueStrings = Array.from(new Set(strings));
725
- const colorMap = {};
726
- uniqueStrings.forEach((str, index) => {
727
- const hsl = `hsl(${index * 360 / numStrings}, 70%, 50%)`;
728
- colorMap[str] = hsl;
729
- });
730
- return colorMap;
1634
+ // src/components/FilterViews/TypeForm.tsx
1635
+ const PICKER_CLASS = "ocelescope-type-filter-picker";
1636
+ const FillHeightStyles = () => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(InlineStyles, {
1637
+ deduplicate: true,
1638
+ selector: `.${PICKER_CLASS} :has([role="listbox"])`,
1639
+ styles: {
1640
+ flex: 1,
1641
+ minHeight: 0
1642
+ }
1643
+ }), /* @__PURE__ */ jsx(InlineStyles, {
1644
+ deduplicate: true,
1645
+ selector: `.${PICKER_CLASS} [role="listbox"]`,
1646
+ styles: {
1647
+ flex: 1,
1648
+ minHeight: 0,
1649
+ maxHeight: "none !important"
1650
+ }
1651
+ })] });
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;
731
1656
  };
732
- const BarChartSelect = memo(({ values, selected, onSelect }) => {
733
- const colorMap = useMemo(() => {
734
- return assignUniqueColors(Array.from(new Set(values.map(({ key }) => key))));
735
- }, [values]);
736
- const data = useMemo(() => [...values].sort((a, b) => b.value - a.value).map(({ value, key }) => ({
737
- key,
738
- value,
739
- color: selected.includes(key) ? colorMap[key] : "rgba(128, 128, 128, 0.3)"
740
- })), [
741
- values,
742
- selected,
743
- colorMap
744
- ]);
745
- return /* @__PURE__ */ jsx(BarChart, {
746
- h: 30 * values.length,
747
- data,
748
- minBarSize: 30,
749
- tooltipProps: { content: ({ label }) => /* @__PURE__ */ jsx(Paper, {
750
- px: "md",
751
- py: "sm",
752
- withBorder: true,
753
- shadow: "md",
754
- radius: "md",
755
- children: /* @__PURE__ */ jsx(Text, { children: label })
756
- }) },
757
- dataKey: "key",
758
- orientation: "vertical",
759
- yAxisProps: { width: 130 },
760
- series: [{
761
- name: "value",
762
- color: "gray.6"
763
- }],
764
- gridAxis: "none",
765
- barChartProps: { onClick: ({ activeLabel }) => {
766
- if (!activeLabel) return;
767
- onSelect(String(activeLabel));
768
- } }
769
- });
770
- });
771
- //#endregion
772
- //#region src/components/inputs/EntityTypeFilter.tsx
773
- const EntityTypeFilterInput = ({ entityTypes, onChange, showGraph = false, selectedEntityTypes, label, mode, onModeChange }) => {
774
- const isExclude = mode === "exclude";
775
- const maxCount = useMemo(() => entityTypes.reduce((max, { value }) => Math.max(max, value), 0), [entityTypes]);
776
- const [range, setRange] = useState([0, 100]);
777
- const frequencyMarks = useMemo(() => {
778
- if (maxCount === 0) return [];
779
- const percentages = new Set(entityTypes.map(({ value }) => Math.round(value / maxCount * 100)));
780
- return Array.from(percentages).map((value) => ({ value }));
781
- }, [entityTypes, maxCount]);
782
- const applyRange = ([low, high]) => {
783
- setRange([low, high]);
784
- const minCount = low / 100 * maxCount;
785
- const maxCountInBand = high / 100 * maxCount;
786
- const inBand = (count) => count >= minCount && count <= maxCountInBand;
787
- onChange(entityTypes.filter(({ value: count }) => isExclude ? !inBand(count) : inBand(count)).map(({ key }) => key));
788
- };
789
- const highlighted = isExclude ? entityTypes.map(({ key }) => key).filter((key) => !selectedEntityTypes.includes(key)) : selectedEntityTypes;
790
- const toggle = (key) => onChange(selectedEntityTypes.includes(key) ? selectedEntityTypes.filter((v) => v !== key) : [...selectedEntityTypes, key]);
791
- return /* @__PURE__ */ jsxs(Stack, {
792
- gap: "md",
793
- children: [showGraph && /* @__PURE__ */ jsxs(Stack, {
794
- gap: "xs",
795
- style: { minHeight: 0 },
796
- children: [/* @__PURE__ */ jsx(ScrollArea.Autosize, {
797
- mah: "60vh",
798
- type: "auto",
799
- children: /* @__PURE__ */ jsx(BarChartSelect, {
800
- selected: highlighted,
801
- values: entityTypes ?? [],
802
- onSelect: toggle
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
803
1680
  })
804
- }), /* @__PURE__ */ jsxs(Stack, {
805
- gap: 2,
806
- children: [/* @__PURE__ */ jsx(Text, {
807
- size: "sm",
808
- fw: 500,
809
- children: "Frequency range (% of most frequent)"
810
- }), /* @__PURE__ */ jsx(RangeSlider, {
811
- min: 0,
812
- max: 100,
813
- value: range,
814
- onChange: applyRange,
815
- disabled: maxCount === 0,
816
- label: (value) => `${value}%`,
817
- minRange: 0,
818
- marks: frequencyMarks,
819
- inverted: mode === "include",
820
- mb: "sm"
821
- })]
822
1681
  })]
823
- }), /* @__PURE__ */ jsxs(Group, {
824
- align: "flex-end",
825
- gap: "sm",
826
- wrap: "nowrap",
827
- children: [/* @__PURE__ */ jsx(MultiSelect, {
828
- flex: 1,
829
- label: isExclude ? `Excluded ${label}` : label,
830
- data: entityTypes.map(({ key }) => key),
831
- value: selectedEntityTypes,
832
- searchable: true,
833
- hidePickedOptions: true,
834
- nothingFoundMessage: "No event type found",
835
- onChange: (newValues) => onChange(newValues),
836
- clearable: true,
837
- styles: { input: {
838
- flexWrap: "nowrap",
839
- overflowX: "auto",
840
- overflowY: "hidden"
841
- } }
842
- }), /* @__PURE__ */ jsx(Switch, {
843
- mb: 8,
844
- label: "Exclude",
845
- checked: isExclude,
846
- onChange: (event) => onModeChange(event.currentTarget.checked ? "exclude" : "include")
847
- })]
848
- })]
1682
+ })
849
1683
  });
850
1684
  };
851
- //#endregion
852
- //#region src/components/FilterViews/TypeForm.tsx
853
- const EntityFilter = (entityType) => ({ ocelId, control }) => {
854
- const isEvents = entityType === "events";
855
- const { data: entityCounts } = (isEvents ? useEventCounts : useObjectCounts)(ocelId, { ocel_version: "original" });
856
- return /* @__PURE__ */ jsx("form", { children: /* @__PURE__ */ jsx(Controller, {
857
- name: isEvents ? `activity.0.mode` : `object_type.0.mode`,
858
- control,
859
- render: ({ field: modeField }) => /* @__PURE__ */ jsx(Controller, {
860
- name: isEvents ? `activity.0.event_types` : `object_type.0.object_types`,
861
- control,
862
- render: ({ field }) => /* @__PURE__ */ jsx(EntityTypeFilterInput, {
863
- entityTypes: Object.entries(entityCounts ?? {}).map(([activity, count]) => ({
864
- key: activity,
865
- value: count
866
- })),
867
- selectedEntityTypes: field.value ?? [],
868
- mode: modeField.value ?? "exclude",
869
- onModeChange: modeField.onChange,
870
- showGraph: true,
871
- label: isEvents ? "Activities" : "Object Types",
872
- onChange: field.onChange
873
- })
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
+ })]
874
1710
  })
875
- }) });
1711
+ });
876
1712
  };
877
- //#endregion
878
- //#region src/components/FilterViews/index.tsx
879
- const FILTER_MAP = {
1713
+ // src/components/FilterViews/index.tsx
1714
+ const FILTER_KINDS = {
880
1715
  activity: {
881
1716
  title: "Activity",
882
- description: "Filters the event log by activity (the event type). In include mode only events of the selected activities are kept; in exclude mode the selected activities are dropped and everything else is kept. The bar chart shows how many events exist per activity to help you decide what to keep.",
883
- ViewComponent: EntityFilter("events"),
884
- 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: () => ({
885
1720
  type: "activity",
886
1721
  event_types: [],
887
- mode: "exclude"
888
- }],
889
- cleanUpFilters: (filter) => {
890
- if (!filter[0] || filter[0].event_types.length === 0) return [];
891
- return [filter[0]];
892
- }
1722
+ mode: "include"
1723
+ }),
1724
+ summary: (filter) => listed(filter.event_types, "activities"),
1725
+ isComplete: (filter) => filter.event_types.length > 0
893
1726
  },
894
1727
  object_type: {
895
1728
  title: "Object Type",
896
- description: "Filters the log by object type. In include mode only objects of the selected types are kept; in exclude mode the selected types are dropped and everything else is kept. The bar chart shows how many objects exist per type to help you decide what to keep.",
897
- ViewComponent: EntityFilter("objects"),
898
- 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: () => ({
899
1732
  type: "object_type",
900
1733
  object_types: [],
901
- mode: "exclude"
902
- }],
903
- cleanUpFilters: (filter) => {
904
- if (!filter[0] || filter[0].object_types.length === 0) return [];
905
- return [filter[0]];
906
- }
1734
+ mode: "include"
1735
+ }),
1736
+ summary: (filter) => listed(filter.object_types, "object types"),
1737
+ isComplete: (filter) => filter.object_types.length > 0
907
1738
  },
908
1739
  time_frame: TimeFrameFilter,
909
1740
  event_attribute: EventAttributeFilter,
@@ -911,103 +1742,327 @@ const FILTER_MAP = {
911
1742
  e2o_count: E2OCountFilter,
912
1743
  o2o_count: O2OCountFilter
913
1744
  };
914
- const generateDefaultFilter = (currentFilters) => {
915
- const groupedFilter = currentFilters.reduce((acc, curr) => {
916
- return {
917
- ...acc,
918
- [curr.type]: acc[curr.type] ? [...acc[curr.type] ?? [], curr] : [curr]
919
- };
920
- }, {});
921
- const missingFilterKeys = Object.keys(FILTER_MAP).filter((key) => !(key in groupedFilter));
922
- return {
923
- ...groupedFilter,
924
- ...Object.fromEntries(missingFilterKeys.map((key) => [key, FILTER_MAP[key].generateDefault()]))
925
- };
926
- };
927
- const defaultFilter = (filters) => {
928
- if (!filters || filters.length === 0) return [];
929
- return filters;
930
- };
931
- const cleanUpFilterByKey = (key, filters) => {
932
- return (FILTER_MAP[key].cleanUpFilters ?? defaultFilter)(filters);
933
- };
934
- const cleanUpFilters = (filter) => {
935
- return Object.keys(filter).flatMap((key) => {
936
- const filters = filter[key];
937
- if (!filters) return [];
938
- return cleanUpFilterByKey(key, filters);
939
- }).filter((filter) => !!filter);
940
- };
941
- //#endregion
942
- //#region src/components/FilterForm.tsx
943
- const FilterForm = ({ ocelId, currentFilter, onSubmit }) => {
944
- const { control, handleSubmit, reset, formState } = useForm({ defaultValues: generateDefaultFilter(currentFilter) });
945
- const { currentTab, setCurrentTab } = useCurrentFilterTab({
946
- tabs: Object.keys(FILTER_MAP),
947
- defaultTab: "activity",
948
- queryKey: "tab"
949
- });
950
- return /* @__PURE__ */ jsxs(Tabs, {
951
- value: currentTab,
952
- onChange: (newTab) => setCurrentTab(newTab),
953
- 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,
954
1757
  style: {
955
- display: "flex",
956
- 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"
957
1765
  },
958
- h: "100%",
959
- children: [/* @__PURE__ */ jsxs(Group, {
960
- py: "xs",
1766
+ children: /* @__PURE__ */ jsxs(Group, {
961
1767
  gap: 0,
962
1768
  wrap: "nowrap",
963
- 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",
964
1791
  flex: 1,
965
1792
  miw: 0,
966
- children: /* @__PURE__ */ jsx(Scroller, { children: Object.entries(FILTER_MAP).map(([key, { title }]) => /* @__PURE__ */ jsx(Tabs.Tab, {
967
- value: key,
968
- children: title
969
- }, key)) })
970
- }), /* @__PURE__ */ jsxs(ActionIconGroup, { children: [formState.isDirty && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ActionIcon, {
971
- onClick: handleSubmit((data) => {
972
- onSubmit?.(cleanUpFilters(data));
973
- }),
974
- size: "lg",
975
- color: "green",
976
- children: /* @__PURE__ */ jsx(CheckIcon, { size: 24 })
977
- }), /* @__PURE__ */ jsx(ActionIcon, {
978
- size: "lg",
979
- color: "yellow",
980
- onClick: () => reset(generateDefaultFilter(currentFilter)),
981
- children: /* @__PURE__ */ jsx(RotateCcwIcon, { size: 24 })
982
- })] }), currentFilter.length > 0 && /* @__PURE__ */ jsx(ActionIcon, {
983
- size: "lg",
984
- color: "red",
985
- onClick: () => onSubmit?.([]),
986
- children: /* @__PURE__ */ jsx(Trash2Icon, { size: 24 })
987
- })] })]
988
- }), Object.entries(FILTER_MAP).map(([key, { ViewComponent, description }]) => /* @__PURE__ */ jsx(Tabs.Panel, {
989
- value: key,
990
- 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, {
991
1855
  flex: 1,
992
1856
  mih: 0,
993
- style: { overflow: "auto" },
1857
+ type: "auto",
1858
+ scrollbarSize: 6,
994
1859
  children: /* @__PURE__ */ jsxs(Stack, {
995
- gap: "md",
996
- h: "100%",
997
- children: [/* @__PURE__ */ jsxs(Text, {
1860
+ gap: "xs",
1861
+ children: [ids.length === 0 && /* @__PURE__ */ jsx(Text, {
998
1862
  size: "sm",
999
1863
  c: "dimmed",
1000
- 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."]
1001
- }), /* @__PURE__ */ jsx(ViewComponent, {
1002
- control,
1003
- 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
+ })
1004
1884
  })]
1005
1885
  })
1006
- }, 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
+ })]
1007
2063
  });
1008
2064
  };
1009
- //#endregion
1010
- //#region src/components/FilterPage.tsx
2065
+ // src/components/FilterPage.tsx
1011
2066
  const FilterPage = () => {
1012
2067
  const { id: ocelId } = useCurrentOcel();
1013
2068
  const { data: filterPipeline, refetch } = useGetFilter(ocelId ?? "", { query: { enabled: !!ocelId } });
@@ -1031,8 +2086,7 @@ var FilterPage_default = defineModuleRoute({
1031
2086
  name: "filter",
1032
2087
  requiresOcel: true
1033
2088
  });
1034
- //#endregion
1035
- //#region src/index.ts
2089
+ // src/index.ts
1036
2090
  var src_default = defineModule({
1037
2091
  name: "filter",
1038
2092
  description: "Filter functionality provided by the ocelescope package",
@@ -1041,5 +2095,4 @@ var src_default = defineModule({
1041
2095
  routes: [FilterPage_default],
1042
2096
  icon: FilterIcon
1043
2097
  });
1044
- //#endregion
1045
2098
  export { src_default as default };