@ocelescope/filter 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,995 @@
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, Stack, Switch, Tabs, Text, TextInput } from "@mantine/core";
4
+ import hash from "object-hash";
5
+ import { useMutation, useQuery } from "@tanstack/react-query";
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";
11
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
+ import { BarChart } from "@mantine/charts";
13
+ import { useEventCounts, useObjectCounts, useTimeInfo } from "@ocelescope/api-base";
14
+ import dayjs from "dayjs";
15
+ import isBetween from "dayjs/plugin/isBetween.js";
16
+ //#region src/lib/fetcher.ts
17
+ const customFetch$1 = async (config, options) => customFetch(config, options);
18
+ //#endregion
19
+ //#region src/api/base.ts
20
+ /**
21
+ * Generated by orval v8.5.2 🍺
22
+ * Do not edit manually.
23
+ * Filter
24
+ * OpenAPI spec version: 1.0
25
+ */
26
+ /**
27
+ * @summary Getfilter
28
+ */
29
+ const getFilter = (ocelId, options, signal) => {
30
+ return customFetch$1({
31
+ url: `/api/external/modules/filter/v1/${ocelId}/filter`,
32
+ method: "GET",
33
+ signal
34
+ }, options);
35
+ };
36
+ const getGetFilterQueryKey = (ocelId) => {
37
+ return [`/api/external/modules/filter/v1/${ocelId}/filter`];
38
+ };
39
+ const getGetFilterQueryOptions = (ocelId, options) => {
40
+ const { query: queryOptions, request: requestOptions } = options ?? {};
41
+ const queryKey = queryOptions?.queryKey ?? getGetFilterQueryKey(ocelId);
42
+ const queryFn = ({ signal }) => getFilter(ocelId, requestOptions, signal);
43
+ return {
44
+ queryKey,
45
+ queryFn,
46
+ enabled: !!ocelId,
47
+ staleTime: 3e5,
48
+ ...queryOptions
49
+ };
50
+ };
51
+ /**
52
+ * @summary Getfilter
53
+ */
54
+ function useGetFilter(ocelId, options, queryClient) {
55
+ const queryOptions = getGetFilterQueryOptions(ocelId, options);
56
+ return {
57
+ ...useQuery(queryOptions, queryClient),
58
+ queryKey: queryOptions.queryKey
59
+ };
60
+ }
61
+ /**
62
+ * @summary Setfilter
63
+ */
64
+ const setFilter = (ocelId, nativeActivityFilterNativeObjectTypeFilterNativeTimeFrameFilterNativeEventAttributeFilterNativeObjectAttributeFilterNativeE2OCountFilterNativeO2OCountFilter, options, signal) => {
65
+ return customFetch$1({
66
+ url: `/api/external/modules/filter/v1/${ocelId}/filter`,
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ data: nativeActivityFilterNativeObjectTypeFilterNativeTimeFrameFilterNativeEventAttributeFilterNativeObjectAttributeFilterNativeE2OCountFilterNativeO2OCountFilter,
70
+ signal
71
+ }, options);
72
+ };
73
+ const getSetFilterMutationOptions = (options) => {
74
+ const mutationKey = ["setFilter"];
75
+ const { mutation: mutationOptions, request: requestOptions } = options ? options.mutation && "mutationKey" in options.mutation && options.mutation.mutationKey ? options : {
76
+ ...options,
77
+ mutation: {
78
+ ...options.mutation,
79
+ mutationKey
80
+ }
81
+ } : {
82
+ mutation: { mutationKey },
83
+ request: void 0
84
+ };
85
+ const mutationFn = (props) => {
86
+ const { ocelId, data } = props ?? {};
87
+ return setFilter(ocelId, data, requestOptions);
88
+ };
89
+ return {
90
+ mutationFn,
91
+ ...mutationOptions
92
+ };
93
+ };
94
+ /**
95
+ * @summary Setfilter
96
+ */
97
+ const useSetFilter = (options, queryClient) => {
98
+ return useMutation(getSetFilterMutationOptions(options), queryClient);
99
+ };
100
+ //#endregion
101
+ //#region src/hooks/useSearchParams.tsx
102
+ const useCurrentFilterTab = ({ defaultTab, queryKey = "tab", tabs }) => {
103
+ const router = useRouter();
104
+ const currentTab = useMemo(() => {
105
+ const raw = router.query[queryKey];
106
+ const tab = typeof raw === "string" ? raw : defaultTab;
107
+ return tabs.includes(tab) ? tab : defaultTab;
108
+ }, [
109
+ router.query,
110
+ queryKey,
111
+ defaultTab
112
+ ]);
113
+ const setCurrentTab = (tab) => {
114
+ router.replace({
115
+ pathname: router.pathname,
116
+ query: {
117
+ ...router.query,
118
+ [queryKey]: tab
119
+ }
120
+ }, void 0, { shallow: true });
121
+ };
122
+ return {
123
+ currentTab,
124
+ setCurrentTab
125
+ };
126
+ };
127
+ //#endregion
128
+ //#region src/components/inputs/AttributeInputField.tsx
129
+ const StringFilterField = memo(function StringFilterField({ control, name }) {
130
+ return /* @__PURE__ */ jsx(Controller, {
131
+ name,
132
+ control,
133
+ render: ({ field }) => /* @__PURE__ */ jsx(TextInput, {
134
+ value: field.value ?? "",
135
+ onChange: field.onChange,
136
+ placeholder: "Enter regex"
137
+ })
138
+ });
139
+ });
140
+ const NumberFilterField = memo(function NumberFilterField({ control, name, min, max, allowDecimals }) {
141
+ return /* @__PURE__ */ jsx(Controller, {
142
+ name,
143
+ control,
144
+ render: ({ field }) => {
145
+ const currentMin = typeof field.value?.[0] === "number" ? field.value[0] : void 0;
146
+ const currentMax = typeof field.value?.[1] === "number" ? field.value[1] : void 0;
147
+ return /* @__PURE__ */ jsxs(Group, {
148
+ wrap: "nowrap",
149
+ align: "end",
150
+ gap: "xs",
151
+ children: [
152
+ /* @__PURE__ */ jsx(NumberInput, {
153
+ value: currentMin,
154
+ min,
155
+ max: currentMax ?? max,
156
+ onChange: (nextMin) => {
157
+ field.onChange([nextMin, currentMax]);
158
+ },
159
+ allowDecimal: allowDecimals,
160
+ style: { flex: 1 }
161
+ }),
162
+ /* @__PURE__ */ jsx(Text, {
163
+ c: "dimmed",
164
+ size: "sm",
165
+ pb: 8,
166
+ children: "-"
167
+ }),
168
+ /* @__PURE__ */ jsx(NumberInput, {
169
+ value: currentMax,
170
+ min: currentMin ?? min,
171
+ max,
172
+ allowDecimal: allowDecimals,
173
+ onChange: (nextMax) => {
174
+ field.onChange([currentMin, nextMax]);
175
+ },
176
+ style: { flex: 1 }
177
+ })
178
+ ]
179
+ });
180
+ }
181
+ });
182
+ });
183
+ const DateFilterField = memo(function DateFilterField({ control, name, min, max }) {
184
+ return /* @__PURE__ */ jsx(Controller, {
185
+ name,
186
+ control,
187
+ render: ({ field }) => {
188
+ return /* @__PURE__ */ jsxs(Group, {
189
+ wrap: "nowrap",
190
+ align: "end",
191
+ gap: "xs",
192
+ children: [
193
+ /* @__PURE__ */ jsx(DateTimePicker, {
194
+ value: field.value?.[0] ?? void 0,
195
+ minDate: min,
196
+ maxDate: field.value?.[1] ?? max,
197
+ withSeconds: true,
198
+ onChange: (nextMin) => {
199
+ field.onChange([nextMin, field.value?.[1] ?? max]);
200
+ },
201
+ style: { flex: 1 }
202
+ }),
203
+ /* @__PURE__ */ jsx(Text, {
204
+ c: "dimmed",
205
+ size: "sm",
206
+ pb: 8,
207
+ children: "-"
208
+ }),
209
+ /* @__PURE__ */ jsx(DateTimePicker, {
210
+ value: field.value?.[1] ?? void 0,
211
+ minDate: field.value?.[0] ?? min,
212
+ maxDate: max,
213
+ withSeconds: true,
214
+ onChange: (nextMax) => {
215
+ field.onChange([field.value?.[0] ?? min, nextMax]);
216
+ },
217
+ style: { flex: 1 }
218
+ })
219
+ ]
220
+ });
221
+ }
222
+ });
223
+ });
224
+ const AttributeInputField = memo(function AttributeInputField({ control, record, path }) {
225
+ switch (record.type) {
226
+ case "string": return /* @__PURE__ */ jsx(StringFilterField, {
227
+ control,
228
+ name: `${path}.regex`
229
+ });
230
+ case "int": return /* @__PURE__ */ jsx(NumberFilterField, {
231
+ control,
232
+ name: `${path}.number_range`,
233
+ min: Number.parseInt(`${record.min}`, 10),
234
+ max: Number.parseInt(`${record.max}`, 10),
235
+ allowDecimals: false
236
+ });
237
+ case "float": return /* @__PURE__ */ jsx(NumberFilterField, {
238
+ control,
239
+ name: `${path}.number_range`,
240
+ min: Number.parseFloat(`${record.min}`),
241
+ max: Number.parseFloat(`${record.max}`)
242
+ });
243
+ case "date": return /* @__PURE__ */ jsx(DateFilterField, {
244
+ control,
245
+ name: `${path}.time_range`,
246
+ min: record.min.toString(),
247
+ max: record.max.toString()
248
+ });
249
+ default: return /* @__PURE__ */ jsx(Text, {
250
+ size: "sm",
251
+ children: "Not Implemented"
252
+ });
253
+ }
254
+ });
255
+ //#endregion
256
+ //#region src/hooks/useAttributeFilterColumns.tsx
257
+ const getInitialFilter = (attribute) => {
258
+ switch (attribute.type) {
259
+ case "string": return { regex: "" };
260
+ case "int": return { number_range: [Number.parseInt(`${attribute.min}`, 10), Number.parseInt(`${attribute.max}`, 10)] };
261
+ case "float": return { number_range: [Number.parseFloat(`${attribute.min}`), Number.parseFloat(`${attribute.max}`)] };
262
+ case "date": return { time_range: [String(attribute.min), String(attribute.max)] };
263
+ default: return {};
264
+ }
265
+ };
266
+ const getEntityType = (attribute) => {
267
+ if (!("entity_type_names" in attribute)) return attribute.entity_type;
268
+ return attribute.entity_type_names.length === 1 ? attribute.entity_type_names[0] : void 0;
269
+ };
270
+ const generateRecordId = (attribute) => {
271
+ const entityName = getEntityType(attribute);
272
+ return `${attribute.name}::${entityName ?? ""}`;
273
+ };
274
+ const useAttributeFilter = ({ path, control }) => {
275
+ const { fields, append, remove } = useFieldArray({
276
+ name: path,
277
+ control
278
+ });
279
+ const filterIndexMap = useMemo(() => {
280
+ const map = /* @__PURE__ */ new Map();
281
+ fields.forEach(({ attribute, target_type }, index) => {
282
+ map.set(`${attribute}::${target_type ?? ""}`, index);
283
+ });
284
+ return map;
285
+ }, [fields]);
286
+ const [showOnlyFiltered, setShowOnlyFiltered] = useState(false);
287
+ const filteredAttributeNames = useMemo(() => Array.from(new Set(fields.map(({ attribute }) => attribute))), [fields]);
288
+ return {
289
+ filterColumns: useMemo(() => [{
290
+ accessor: "filter",
291
+ title: "Filter",
292
+ filter: () => /* @__PURE__ */ jsx(Switch, {
293
+ label: "Show only filtered attributes",
294
+ checked: showOnlyFiltered,
295
+ onChange: (event) => setShowOnlyFiltered(event.currentTarget.checked)
296
+ }),
297
+ filtering: showOnlyFiltered,
298
+ render: (record) => {
299
+ const filterIndex = filterIndexMap.get(generateRecordId(record)) ?? -1;
300
+ return filterIndex < 0 ? /* @__PURE__ */ jsxs(Text, {
301
+ size: "sm",
302
+ c: "dimmed",
303
+ children: [
304
+ record.min,
305
+ " - ",
306
+ record.max
307
+ ]
308
+ }) : /* @__PURE__ */ jsx(AttributeInputField, {
309
+ record,
310
+ control,
311
+ path: `${path}.${filterIndex}`
312
+ });
313
+ }
314
+ }, {
315
+ accessor: "action",
316
+ title: "",
317
+ width: "50px",
318
+ render: (record) => {
319
+ const key = generateRecordId(record);
320
+ const filterIndex = filterIndexMap.get(key) ?? -1;
321
+ return filterIndex < 0 ? /* @__PURE__ */ jsx(ActionIcon, {
322
+ color: "green",
323
+ variant: "light",
324
+ onClick: (event) => {
325
+ event.stopPropagation();
326
+ append({
327
+ attribute: record.name,
328
+ type: path,
329
+ target_type: getEntityType(record),
330
+ ...getInitialFilter(record)
331
+ });
332
+ },
333
+ children: /* @__PURE__ */ jsx(PlusIcon, { size: 16 })
334
+ }) : /* @__PURE__ */ jsx(ActionIcon, {
335
+ color: "red",
336
+ variant: "light",
337
+ onClick: (event) => {
338
+ event.stopPropagation();
339
+ remove(filterIndex);
340
+ },
341
+ children: /* @__PURE__ */ jsx(XIcon, { size: 16 })
342
+ });
343
+ }
344
+ }], [
345
+ append,
346
+ remove,
347
+ filterIndexMap,
348
+ path,
349
+ control,
350
+ showOnlyFiltered
351
+ ]),
352
+ visibleAttributes: showOnlyFiltered ? filteredAttributeNames : void 0
353
+ };
354
+ };
355
+ //#endregion
356
+ //#region src/components/FilterViews/AttributeFilter.tsx
357
+ const AttributeFilter = (entityType) => ({ ocelId, control }) => {
358
+ const { filterColumns, visibleAttributes } = useAttributeFilter({
359
+ control,
360
+ path: entityType === "events" ? "event_attribute" : "object_attribute"
361
+ });
362
+ return /* @__PURE__ */ jsx(AttributesTable, {
363
+ ocelId,
364
+ entityType,
365
+ ocelVersion: "original",
366
+ extraColumns: filterColumns,
367
+ subTableExtraColumns: filterColumns,
368
+ visibleAttributes,
369
+ hideRange: true,
370
+ hideValues: true
371
+ });
372
+ };
373
+ const EventAttributeFilter = {
374
+ title: "Event Attribute",
375
+ 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.",
376
+ ViewComponent: AttributeFilter("events"),
377
+ generateDefault: () => []
378
+ };
379
+ const ObjectAttributeFilter = {
380
+ title: "Object Attribute",
381
+ 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.",
382
+ ViewComponent: AttributeFilter("objects"),
383
+ generateDefault: () => []
384
+ };
385
+ //#endregion
386
+ //#region src/hooks/useRelationFilterColumns.tsx
387
+ const filterKey = ({ source, target, qualifier }) => `${source}-${qualifier ?? ""}-${target}`;
388
+ const resolveQualifier = (record) => record.qualifiers?.length === 1 ? record.qualifiers[0] : void 0;
389
+ const useRelationCountFilter = ({ path, control }) => {
390
+ const { fields, append, remove } = useFieldArray({
391
+ name: path,
392
+ control
393
+ });
394
+ const filterIndexMap = useMemo(() => {
395
+ const map = /* @__PURE__ */ new Map();
396
+ fields.forEach((field, index) => {
397
+ map.set(filterKey(field), index);
398
+ });
399
+ return map;
400
+ }, [fields]);
401
+ const [showOnlyFiltered, setShowOnlyFiltered] = useState(false);
402
+ const visibleRelations = useMemo(() => {
403
+ const seen = /* @__PURE__ */ new Set();
404
+ const relations = [];
405
+ fields.forEach((field) => {
406
+ const { source, target } = field;
407
+ const key = `${source}-${target}`;
408
+ if (!seen.has(key)) {
409
+ seen.add(key);
410
+ relations.push({
411
+ source,
412
+ target
413
+ });
414
+ }
415
+ });
416
+ return relations;
417
+ }, [fields]);
418
+ return {
419
+ filterColumns: useMemo(() => [{
420
+ accessor: "filter",
421
+ title: "Filter",
422
+ filter: () => /* @__PURE__ */ jsx(Switch, {
423
+ label: "Show only filtered relations",
424
+ checked: showOnlyFiltered,
425
+ onChange: (event) => setShowOnlyFiltered(event.currentTarget.checked)
426
+ }),
427
+ filtering: showOnlyFiltered,
428
+ render: (record) => {
429
+ const qualifier = resolveQualifier(record);
430
+ const filterIndex = filterIndexMap.get(filterKey({
431
+ ...record,
432
+ qualifier
433
+ })) ?? -1;
434
+ if (filterIndex < 0) return /* @__PURE__ */ jsx(Text, {
435
+ size: "sm",
436
+ c: "dimmed",
437
+ children: "No filter applied"
438
+ });
439
+ return /* @__PURE__ */ jsx(Controller, {
440
+ control,
441
+ name: `${path}.${filterIndex}.range`,
442
+ render: ({ field }) => {
443
+ const currentMin = typeof field.value?.[0] === "number" ? field.value[0] : void 0;
444
+ const currentMax = typeof field.value?.[1] === "number" ? field.value[1] : void 0;
445
+ return /* @__PURE__ */ jsxs(Group, {
446
+ wrap: "nowrap",
447
+ align: "end",
448
+ gap: "xs",
449
+ children: [
450
+ /* @__PURE__ */ jsx(NumberInput, {
451
+ value: currentMin,
452
+ min: record.min_count,
453
+ max: currentMax ?? record.max_count,
454
+ clampBehavior: "strict",
455
+ allowDecimal: false,
456
+ onChange: (nextMin) => {
457
+ field.onChange([nextMin, currentMax]);
458
+ },
459
+ style: { flex: 1 }
460
+ }),
461
+ /* @__PURE__ */ jsx(Text, {
462
+ c: "dimmed",
463
+ size: "sm",
464
+ pb: 8,
465
+ children: "-"
466
+ }),
467
+ /* @__PURE__ */ jsx(NumberInput, {
468
+ value: currentMax,
469
+ min: currentMin ?? record.min_count,
470
+ max: record.max_count,
471
+ clampBehavior: "strict",
472
+ allowDecimal: false,
473
+ onChange: (nextMax) => {
474
+ field.onChange([currentMin, nextMax]);
475
+ },
476
+ style: { flex: 1 }
477
+ })
478
+ ]
479
+ });
480
+ }
481
+ });
482
+ }
483
+ }, {
484
+ accessor: "action",
485
+ title: "",
486
+ width: "50px",
487
+ render: (record) => {
488
+ const qualifier = resolveQualifier(record);
489
+ const filterIndex = filterIndexMap.get(filterKey({
490
+ ...record,
491
+ qualifier
492
+ })) ?? -1;
493
+ return filterIndex < 0 ? /* @__PURE__ */ jsx(ActionIcon, {
494
+ color: "green",
495
+ variant: "light",
496
+ onClick: (event) => {
497
+ event.stopPropagation();
498
+ append({
499
+ type: path,
500
+ source: record.source,
501
+ target: record.target,
502
+ qualifier,
503
+ range: [record.min_count, record.max_count]
504
+ });
505
+ },
506
+ children: /* @__PURE__ */ jsx(PlusIcon, { size: 16 })
507
+ }) : /* @__PURE__ */ jsx(ActionIcon, {
508
+ color: "red",
509
+ variant: "light",
510
+ onClick: (event) => {
511
+ event.stopPropagation();
512
+ remove(filterIndex);
513
+ },
514
+ children: /* @__PURE__ */ jsx(XIcon, { size: 16 })
515
+ });
516
+ }
517
+ }], [
518
+ append,
519
+ remove,
520
+ filterIndexMap,
521
+ path,
522
+ control,
523
+ showOnlyFiltered
524
+ ]),
525
+ visibleRelations: showOnlyFiltered ? visibleRelations : void 0
526
+ };
527
+ };
528
+ //#endregion
529
+ //#region src/components/FilterViews/RelationCountFilter.tsx
530
+ const RelationCountFilter = (relationType) => ({ ocelId, control }) => {
531
+ const { filterColumns, visibleRelations } = useRelationCountFilter({
532
+ control,
533
+ path: relationType === "e2o" ? "e2o_count" : "o2o_count"
534
+ });
535
+ return /* @__PURE__ */ jsx(RelationTable, {
536
+ ocelId,
537
+ ocelVersion: "original",
538
+ relationType,
539
+ extraColumns: filterColumns,
540
+ subTableExtraColumns: filterColumns,
541
+ visibleRelations,
542
+ hideRange: true,
543
+ hideTotal: true
544
+ });
545
+ };
546
+ const E2OCountFilter = {
547
+ title: "E2O Count",
548
+ 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.",
549
+ ViewComponent: RelationCountFilter("e2o"),
550
+ generateDefault: () => []
551
+ };
552
+ const O2OCountFilter = {
553
+ title: "O2O Count",
554
+ 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.",
555
+ ViewComponent: RelationCountFilter("o2o"),
556
+ generateDefault: () => []
557
+ };
558
+ //#endregion
559
+ //#region src/util/dayjs.ts
560
+ dayjs.extend(isBetween);
561
+ var dayjs_default = dayjs;
562
+ //#endregion
563
+ //#region src/components/FilterViews/TimeFrameFilter.tsx
564
+ const TimeGraph = memo(({ timeInfo, startDate, endDate }) => {
565
+ return /* @__PURE__ */ jsx(BarChart, {
566
+ h: 300,
567
+ w: "100%",
568
+ data: useMemo(() => {
569
+ return timeInfo.date_distribution.map(({ start_timestamp, end_timestamp, entity_count }) => {
570
+ const isInRange = (!startDate || dayjs_default(end_timestamp).isAfter(dayjs_default(startDate))) && (!endDate || dayjs_default(start_timestamp).isBefore(dayjs_default(endDate)));
571
+ return {
572
+ date: `${dayjs_default(start_timestamp).format("YYYY-MM-DD HH:mm")}-${dayjs_default(end_timestamp).format("YYYY-MM-DD HH:mm")} `,
573
+ ...isInRange ? { value: Object.values(entity_count).reduce((acc, curr) => acc + curr, 0) } : { disabledValue: Object.values(entity_count).reduce((acc, curr) => acc + curr, 0) }
574
+ };
575
+ });
576
+ }, [
577
+ timeInfo,
578
+ startDate,
579
+ endDate
580
+ ]),
581
+ dataKey: "date",
582
+ type: "stacked",
583
+ series: [{
584
+ name: "value",
585
+ color: "blue",
586
+ label: "count"
587
+ }, {
588
+ name: "disabledValue",
589
+ color: "red",
590
+ label: "count"
591
+ }],
592
+ withYAxis: false,
593
+ withXAxis: false,
594
+ barChartProps: {
595
+ barCategoryGap: 0,
596
+ barGap: 0
597
+ }
598
+ });
599
+ });
600
+ const TimeFrameSlider = ({ timeInfo, startTime, endTime, onChange }) => {
601
+ const startIndex = useMemo(() => timeInfo.date_distribution.findIndex(({ start_timestamp, end_timestamp }) => dayjs_default(startTime).isBetween(start_timestamp, end_timestamp, null, "[)")), [startTime, timeInfo.date_distribution]);
602
+ const endIndex = useMemo(() => timeInfo.date_distribution.findLastIndex(({ start_timestamp, end_timestamp }) => dayjs_default(endTime).isBetween(start_timestamp, end_timestamp, null, "(]")), [endTime, timeInfo.date_distribution]);
603
+ return /* @__PURE__ */ jsx(RangeSlider, {
604
+ min: 0,
605
+ max: timeInfo.date_distribution.length - 1,
606
+ minRange: 0,
607
+ value: [startIndex, endIndex],
608
+ label: (value) => dayjs_default(timeInfo.date_distribution[value].start_timestamp).format("YYYY-MM-DD HH:mm"),
609
+ onChange: ([start, end]) => {
610
+ onChange([start === startIndex ? startTime : timeInfo.date_distribution[start].start_timestamp, timeInfo.date_distribution[end].end_timestamp]);
611
+ }
612
+ });
613
+ };
614
+ const TimeFrameFilter = {
615
+ title: "Timeframe",
616
+ 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.",
617
+ ViewComponent: memo(({ ocelId, control }) => {
618
+ const { data: timeInfo, isLoading } = useTimeInfo(ocelId, {
619
+ periods: 100,
620
+ ocel_version: "original"
621
+ });
622
+ return /* @__PURE__ */ jsxs(Box, {
623
+ pos: "relative",
624
+ w: "100%",
625
+ h: "100%",
626
+ children: [/* @__PURE__ */ jsx(LoadingOverlay, { visible: isLoading }), timeInfo && /* @__PURE__ */ jsxs(Grid, {
627
+ justify: "center",
628
+ align: "center",
629
+ children: [/* @__PURE__ */ jsx(Grid.Col, {
630
+ span: 12,
631
+ children: /* @__PURE__ */ jsx(Watch, {
632
+ control,
633
+ name: ["time_frame.0.time_range.0", "time_frame.0.time_range.1"],
634
+ render: ([startTime, endTime]) => {
635
+ return /* @__PURE__ */ jsx(TimeGraph, {
636
+ timeInfo,
637
+ startDate: startTime ?? void 0,
638
+ endDate: endTime ?? void 0
639
+ });
640
+ }
641
+ })
642
+ }), /* @__PURE__ */ jsx(Controller, {
643
+ control,
644
+ name: "time_frame.0.time_range",
645
+ defaultValue: [timeInfo.start_time, timeInfo.end_time],
646
+ render: ({ field }) => /* @__PURE__ */ jsxs(Fragment, { children: [
647
+ /* @__PURE__ */ jsx(Grid.Col, {
648
+ span: 3,
649
+ children: /* @__PURE__ */ jsx(DateTimePicker, {
650
+ minDate: timeInfo.start_time,
651
+ maxDate: field.value?.[1] ?? timeInfo.end_time,
652
+ onChange: (newStart) => {
653
+ field.onChange([newStart, field.value?.[1]]);
654
+ },
655
+ value: field.value?.[0] ?? timeInfo.start_time
656
+ })
657
+ }),
658
+ /* @__PURE__ */ jsx(Grid.Col, {
659
+ span: 6,
660
+ children: /* @__PURE__ */ jsx(TimeFrameSlider, {
661
+ onChange: field.onChange,
662
+ timeInfo,
663
+ startTime: field.value?.[0] ?? timeInfo.start_time,
664
+ endTime: field.value?.[1] ?? timeInfo.end_time
665
+ })
666
+ }),
667
+ /* @__PURE__ */ jsx(Grid.Col, {
668
+ span: 3,
669
+ children: /* @__PURE__ */ jsx(DateTimePicker, {
670
+ minDate: field.value?.[0] ?? timeInfo.start_time,
671
+ maxDate: timeInfo.end_time,
672
+ onChange: (newEnd) => {
673
+ field.onChange([field.value?.[0], newEnd]);
674
+ },
675
+ value: field.value?.[1] ?? timeInfo.end_time
676
+ })
677
+ })
678
+ ] })
679
+ })]
680
+ })]
681
+ });
682
+ }),
683
+ generateDefault: () => [{
684
+ type: "time_frame",
685
+ time_range: [null, null]
686
+ }],
687
+ cleanUpFilters: (currentFilter) => {
688
+ if (!currentFilter[0] || currentFilter[0].time_range[0] == null && currentFilter[0].time_range[1] == null) return [];
689
+ return [currentFilter[0]];
690
+ }
691
+ };
692
+ //#endregion
693
+ //#region src/util/color.ts
694
+ const assignUniqueColors = (strings) => {
695
+ const numStrings = strings.length;
696
+ const uniqueStrings = Array.from(new Set(strings));
697
+ const colorMap = {};
698
+ uniqueStrings.forEach((str, index) => {
699
+ colorMap[str] = `hsl(${index * 360 / numStrings}, 70%, 50%)`;
700
+ });
701
+ return colorMap;
702
+ };
703
+ const BarChartSelect = memo(({ values, selected, onSelect }) => {
704
+ const colorMap = useMemo(() => {
705
+ return assignUniqueColors(Array.from(new Set(values.map(({ key }) => key))));
706
+ }, [values]);
707
+ return /* @__PURE__ */ jsx(BarChart, {
708
+ h: 30 * values.length,
709
+ data: values.map(({ value, key }) => ({
710
+ key,
711
+ value,
712
+ color: selected.includes(key) ? colorMap[key] : "rgba(128, 128, 128, 0.3)"
713
+ })),
714
+ minBarSize: 30,
715
+ tooltipProps: { content: ({ label }) => /* @__PURE__ */ jsx(Paper, {
716
+ px: "md",
717
+ py: "sm",
718
+ withBorder: true,
719
+ shadow: "md",
720
+ radius: "md",
721
+ children: /* @__PURE__ */ jsx(Text, { children: label })
722
+ }) },
723
+ dataKey: "key",
724
+ orientation: "vertical",
725
+ yAxisProps: { width: 130 },
726
+ series: [{
727
+ name: "value",
728
+ color: "gray.6"
729
+ }],
730
+ gridAxis: "none",
731
+ barChartProps: { onClick: ({ activeLabel }) => {
732
+ if (!activeLabel) return;
733
+ onSelect(String(activeLabel));
734
+ } }
735
+ });
736
+ });
737
+ //#endregion
738
+ //#region src/components/inputs/EntityTypeFilter.tsx
739
+ const EntityTypeFilterInput = ({ entityTypes, onChange, showGraph = false, selectedEntityTypes, label, mode, onModeChange }) => {
740
+ const isExclude = mode === "exclude";
741
+ const maxCount = useMemo(() => entityTypes.reduce((max, { value }) => Math.max(max, value), 0), [entityTypes]);
742
+ const [range, setRange] = useState([0, 100]);
743
+ const frequencyMarks = useMemo(() => {
744
+ if (maxCount === 0) return [];
745
+ const percentages = new Set(entityTypes.map(({ value }) => Math.round(value / maxCount * 100)));
746
+ return Array.from(percentages).map((value) => ({ value }));
747
+ }, [entityTypes, maxCount]);
748
+ const applyRange = ([low, high]) => {
749
+ setRange([low, high]);
750
+ const minCount = low / 100 * maxCount;
751
+ const maxCountInBand = high / 100 * maxCount;
752
+ const inBand = (count) => count >= minCount && count <= maxCountInBand;
753
+ onChange(entityTypes.filter(({ value: count }) => isExclude ? !inBand(count) : inBand(count)).map(({ key }) => key));
754
+ };
755
+ const highlighted = isExclude ? entityTypes.map(({ key }) => key).filter((key) => !selectedEntityTypes.includes(key)) : selectedEntityTypes;
756
+ const toggle = (key) => onChange(selectedEntityTypes.includes(key) ? selectedEntityTypes.filter((v) => v !== key) : [...selectedEntityTypes, key]);
757
+ return /* @__PURE__ */ jsxs(Stack, {
758
+ gap: "md",
759
+ children: [showGraph && /* @__PURE__ */ jsxs(Stack, {
760
+ gap: "xs",
761
+ style: { minHeight: 0 },
762
+ children: [/* @__PURE__ */ jsx(ScrollArea.Autosize, {
763
+ mah: "60vh",
764
+ type: "auto",
765
+ children: /* @__PURE__ */ jsx(BarChartSelect, {
766
+ selected: highlighted,
767
+ values: entityTypes ?? [],
768
+ onSelect: toggle
769
+ })
770
+ }), /* @__PURE__ */ jsxs(Stack, {
771
+ gap: 2,
772
+ children: [/* @__PURE__ */ jsx(Text, {
773
+ size: "sm",
774
+ fw: 500,
775
+ children: "Frequency range (% of most frequent)"
776
+ }), /* @__PURE__ */ jsx(RangeSlider, {
777
+ min: 0,
778
+ max: 100,
779
+ value: range,
780
+ onChange: applyRange,
781
+ disabled: maxCount === 0,
782
+ label: (value) => `${value}%`,
783
+ minRange: 0,
784
+ marks: frequencyMarks,
785
+ inverted: mode === "include",
786
+ mb: "sm"
787
+ })]
788
+ })]
789
+ }), /* @__PURE__ */ jsxs(Group, {
790
+ align: "flex-end",
791
+ gap: "sm",
792
+ wrap: "nowrap",
793
+ children: [/* @__PURE__ */ jsx(MultiSelect, {
794
+ flex: 1,
795
+ label: isExclude ? `Excluded ${label}` : label,
796
+ data: entityTypes.map(({ key }) => key),
797
+ value: selectedEntityTypes,
798
+ searchable: true,
799
+ hidePickedOptions: true,
800
+ nothingFoundMessage: "No event type found",
801
+ onChange: (newValues) => onChange(newValues),
802
+ clearable: true,
803
+ styles: { input: {
804
+ flexWrap: "nowrap",
805
+ overflowX: "auto",
806
+ overflowY: "hidden"
807
+ } }
808
+ }), /* @__PURE__ */ jsx(Switch, {
809
+ mb: 8,
810
+ label: "Exclude",
811
+ checked: isExclude,
812
+ onChange: (event) => onModeChange(event.currentTarget.checked ? "exclude" : "include")
813
+ })]
814
+ })]
815
+ });
816
+ };
817
+ //#endregion
818
+ //#region src/components/FilterViews/TypeForm.tsx
819
+ const EntityFilter = (entityType) => ({ ocelId, control }) => {
820
+ const isEvents = entityType === "events";
821
+ const { data: entityCounts } = (isEvents ? useEventCounts : useObjectCounts)(ocelId, { ocel_version: "original" });
822
+ return /* @__PURE__ */ jsx("form", { children: /* @__PURE__ */ jsx(Controller, {
823
+ name: isEvents ? `activity.0.mode` : `object_type.0.mode`,
824
+ control,
825
+ render: ({ field: modeField }) => /* @__PURE__ */ jsx(Controller, {
826
+ name: isEvents ? `activity.0.event_types` : `object_type.0.object_types`,
827
+ control,
828
+ render: ({ field }) => /* @__PURE__ */ jsx(EntityTypeFilterInput, {
829
+ entityTypes: Object.entries(entityCounts ?? {}).map(([activity, count]) => ({
830
+ key: activity,
831
+ value: count
832
+ })),
833
+ selectedEntityTypes: field.value ?? [],
834
+ mode: modeField.value ?? "exclude",
835
+ onModeChange: modeField.onChange,
836
+ showGraph: true,
837
+ label: isEvents ? "Activities" : "Object Types",
838
+ onChange: field.onChange
839
+ })
840
+ })
841
+ }) });
842
+ };
843
+ //#endregion
844
+ //#region src/components/FilterViews/index.tsx
845
+ const FILTER_MAP = {
846
+ activity: {
847
+ title: "Activity",
848
+ 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.",
849
+ ViewComponent: EntityFilter("events"),
850
+ generateDefault: () => [{
851
+ type: "activity",
852
+ event_types: [],
853
+ mode: "exclude"
854
+ }],
855
+ cleanUpFilters: (filter) => {
856
+ if (!filter[0] || filter[0].event_types.length === 0) return [];
857
+ return [filter[0]];
858
+ }
859
+ },
860
+ object_type: {
861
+ title: "Object Type",
862
+ 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.",
863
+ ViewComponent: EntityFilter("objects"),
864
+ generateDefault: () => [{
865
+ type: "object_type",
866
+ object_types: [],
867
+ mode: "exclude"
868
+ }],
869
+ cleanUpFilters: (filter) => {
870
+ if (!filter[0] || filter[0].object_types.length === 0) return [];
871
+ return [filter[0]];
872
+ }
873
+ },
874
+ time_frame: TimeFrameFilter,
875
+ event_attribute: EventAttributeFilter,
876
+ object_attribute: ObjectAttributeFilter,
877
+ e2o_count: E2OCountFilter,
878
+ o2o_count: O2OCountFilter
879
+ };
880
+ const generateDefaultFilter = (currentFilters) => {
881
+ const groupedFilter = currentFilters.reduce((acc, curr) => {
882
+ return {
883
+ ...acc,
884
+ [curr.type]: acc[curr.type] ? [...acc[curr.type] ?? [], curr] : [curr]
885
+ };
886
+ }, {});
887
+ const missingFilterKeys = Object.keys(FILTER_MAP).filter((key) => !(key in groupedFilter));
888
+ return {
889
+ ...groupedFilter,
890
+ ...Object.fromEntries(missingFilterKeys.map((key) => [key, FILTER_MAP[key].generateDefault()]))
891
+ };
892
+ };
893
+ const defaultFilter = (filters) => {
894
+ if (!filters || filters.length === 0) return [];
895
+ return filters;
896
+ };
897
+ const cleanUpFilterByKey = (key, filters) => {
898
+ return (FILTER_MAP[key].cleanUpFilters ?? defaultFilter)(filters);
899
+ };
900
+ const cleanUpFilters = (filter) => {
901
+ return Object.keys(filter).flatMap((key) => {
902
+ const filters = filter[key];
903
+ if (!filters) return [];
904
+ return cleanUpFilterByKey(key, filters);
905
+ }).filter((filter) => !!filter);
906
+ };
907
+ //#endregion
908
+ //#region src/components/FilterForm.tsx
909
+ const FilterForm = ({ ocelId, currentFilter, onSubmit }) => {
910
+ const { control, handleSubmit, reset, formState } = useForm({ defaultValues: generateDefaultFilter(currentFilter) });
911
+ const { currentTab, setCurrentTab } = useCurrentFilterTab({
912
+ tabs: Object.keys(FILTER_MAP),
913
+ defaultTab: "activity",
914
+ queryKey: "tab"
915
+ });
916
+ return /* @__PURE__ */ jsxs(Tabs, {
917
+ value: currentTab,
918
+ onChange: (newTab) => setCurrentTab(newTab),
919
+ keepMounted: false,
920
+ children: [/* @__PURE__ */ jsxs(Group, { children: [/* @__PURE__ */ jsx(Tabs.List, {
921
+ flex: 1,
922
+ children: Object.entries(FILTER_MAP).map(([key, { title }]) => /* @__PURE__ */ jsx(Tabs.Tab, {
923
+ value: key,
924
+ children: title
925
+ }, key))
926
+ }), /* @__PURE__ */ jsxs(ActionIconGroup, { children: [formState.isDirty && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ActionIcon, {
927
+ onClick: handleSubmit((data) => {
928
+ onSubmit?.(cleanUpFilters(data));
929
+ }),
930
+ size: "lg",
931
+ color: "green",
932
+ children: /* @__PURE__ */ jsx(CheckIcon, { size: 24 })
933
+ }), /* @__PURE__ */ jsx(ActionIcon, {
934
+ size: "lg",
935
+ color: "yellow",
936
+ onClick: () => reset(generateDefaultFilter(currentFilter)),
937
+ children: /* @__PURE__ */ jsx(RotateCcwIcon, { size: 24 })
938
+ })] }), currentFilter.length > 0 && /* @__PURE__ */ jsx(ActionIcon, {
939
+ size: "lg",
940
+ color: "red",
941
+ onClick: () => onSubmit?.([]),
942
+ children: /* @__PURE__ */ jsx(Trash2Icon, { size: 24 })
943
+ })] })] }), Object.entries(FILTER_MAP).map(([key, { ViewComponent, description }]) => /* @__PURE__ */ jsx(Tabs.Panel, {
944
+ value: key,
945
+ p: "md",
946
+ children: /* @__PURE__ */ jsxs(Stack, {
947
+ gap: "md",
948
+ children: [/* @__PURE__ */ jsxs(Text, {
949
+ size: "sm",
950
+ c: "dimmed",
951
+ 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."]
952
+ }), /* @__PURE__ */ jsx(ViewComponent, {
953
+ control,
954
+ ocelId
955
+ })]
956
+ })
957
+ }, key))]
958
+ });
959
+ };
960
+ //#endregion
961
+ //#region src/components/FilterPage.tsx
962
+ const FilterPage = () => {
963
+ const { id: ocelId } = useCurrentOcel();
964
+ const { data: filterPipeline, refetch } = useGetFilter(ocelId ?? "", { query: { enabled: !!ocelId } });
965
+ const invalidate = useInvalidate();
966
+ const { mutate, status } = useSetFilter({ mutation: { onSuccess: async () => {
967
+ refetch();
968
+ await invalidate(["ocels"]);
969
+ } } });
970
+ return /* @__PURE__ */ jsxs(Fragment, { children: [filterPipeline && ocelId && /* @__PURE__ */ jsx(FilterForm, {
971
+ ocelId,
972
+ currentFilter: filterPipeline,
973
+ onSubmit: (filter) => mutate({
974
+ ocelId,
975
+ data: filter
976
+ })
977
+ }, hash(filterPipeline)), /* @__PURE__ */ jsx(LoadingOverlay, { visible: status === "pending" })] });
978
+ };
979
+ //#endregion
980
+ //#region src/index.ts
981
+ var src_default = defineModule({
982
+ name: "filter",
983
+ description: "Filter functionality provided by the ocelescope package",
984
+ label: "Log Filter",
985
+ authors: [{ name: "Öztürk, Görkem-Emre" }],
986
+ routes: [defineModuleRoute({
987
+ component: FilterPage,
988
+ label: "Filter",
989
+ name: "filter",
990
+ requiresOcel: true
991
+ })],
992
+ icon: FilterIcon
993
+ });
994
+ //#endregion
995
+ export { src_default as default };