@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/LICENSE +661 -0
- package/README.md +47 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +995 -0
- package/dist/style.css +1468 -0
- package/package.json +68 -0
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 };
|