@ocelescope/core 0.0.0 → 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 CHANGED
@@ -1,48 +1,367 @@
1
- import { ActionIcon, AppShell, Badge, Burger, Button, ColorSchemeScript, Container, Group, LoadingOverlay, MantineProvider, Menu, MenuItem, Modal, Select, Stack, Text, TextInput, ThemeIcon, Title, mantineHtmlProps, useMantineTheme } from "@mantine/core";
2
- import { HydrationBoundary, QueryClient, QueryClientProvider, useQueryClient } from "@tanstack/react-query";
3
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
- import { useDisclosure } from "@mantine/hooks";
5
- import { CheckIcon, ContainerIcon, DownloadIcon, EllipsisVerticalIcon, EyeIcon, PencilIcon, TrashIcon, Upload, UploadIcon, X, XIcon } from "lucide-react";
1
+ import { AppShell, Box, Burger, Button, ColorSchemeScript, Combobox, CopyButton, Divider, Group, Input, InputBase, LoadingOverlay, MantineProvider, Modal, MultiSelect, NavLink, Stack, Table, Text, ThemeIcon, Tooltip, UnstyledButton, createTheme, mantineHtmlProps, useCombobox, useMantineTheme } from "@mantine/core";
2
+ import { getDownloadFlatLogQueryKey, getDownloadOCELQueryKey, getDownloadResourceAsPnmlQueryKey, getDownloadResourceQueryKey, useActivities, useAggregatedAttributes, useAttributeNames, useE2o, useE2oCombinations, useEventAttributes, useGetDefaultOcel, useGetOcels, useImportDefaultOcel, useLogout, useO2o, useO2oCombinations, useObjectAttributes, useObjectTypes, useUpload } from "@ocelescope/api-base";
3
+ import { HydrationBoundary, QueryClient, QueryClientProvider, keepPreviousData, useQueryClient } from "@tanstack/react-query";
4
+ import { ChevronDownIcon, ChevronRightIcon, ContainerIcon, DownloadIcon, FilterIcon, LogOutIcon, PackageIcon, UploadIcon, XIcon } from "lucide-react";
6
5
  import { DataTable } from "mantine-datatable";
6
+ import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
7
+ import dayjs from "dayjs";
8
+ import isBetween from "dayjs/plugin/isBetween.js";
9
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
10
  import { Dropzone } from "@mantine/dropzone";
8
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
- import { useDeleteOcel, useDeleteResource, useGetDefaultOcel, useGetOcels, useGetResourceMeta, useImportDefaultOcel, useRenameOcel, useRenameResource, useResources, useUpload } from "@ocelescope/api-base";
10
- import { env, useSessionStore } from "@ocelescope/api-client";
11
+ import { Readable } from "node:stream";
12
+ import { Notifications, showNotification } from "@mantine/notifications";
13
+ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
14
+ import { useDisclosure } from "@mantine/hooks";
15
+ import { env as env$1, useSessionStore } from "@ocelescope/api-client";
11
16
  import { create } from "zustand";
12
17
  import { persist } from "zustand/middleware";
13
- import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
14
- import { Head, Html, Main, NextScript } from "next/document";
15
- import { ResourceModal } from "@ocelescope/resources";
16
- import { saveAs } from "file-saver";
18
+ import Link from "next/link.js";
19
+ import { useRouter } from "next/router.js";
20
+ import z from "zod";
21
+ import { Head, Html, Main, NextScript } from "next/document.js";
17
22
  import { parse } from "content-disposition-attachment";
18
- import dayjs from "dayjs";
19
- import isBetween from "dayjs/plugin/isBetween";
20
- import { generateColor } from "@marko19907/string-to-color";
21
-
22
- import './index.css';
23
- //#region src/hooks/useInvalidate.ts
24
- const useInvalidate = () => {
25
- const queryClient = useQueryClient();
26
- return async (routeNames) => await queryClient.invalidateQueries({ predicate: (query) => typeof query.queryKey[0] === "string" && routeNames.some((route) => query.queryKey[0].includes(`/${route}`)) });
23
+ import { saveAs } from "file-saver";
24
+ import qs from "qs";
25
+ //#region src/lib/dayjs.ts
26
+ dayjs.extend(isBetween);
27
+ const formatDateTime = (dateString) => dayjs(dateString).format("YYYY-MM-DD HH:mm");
28
+ //#endregion
29
+ //#region src/lib/attributes.ts
30
+ const formatAttributeValue = (type, value) => {
31
+ switch (type) {
32
+ case "int": return value.toLocaleString("en-US");
33
+ case "float": return value.toLocaleString("en-US");
34
+ case "date": return formatDateTime(value);
35
+ default: return value;
36
+ }
37
+ };
38
+ //#endregion
39
+ //#region src/components/AttributeTable.tsx
40
+ const PAGE_SIZE$1 = 10;
41
+ const COLUMN_WIDTHS$1 = {
42
+ selector: 30,
43
+ name: 200,
44
+ entityType: 200,
45
+ type: 100,
46
+ range: 200,
47
+ distinctValues: 120
48
+ };
49
+ const SubAttributeTable = ({ ocelId, entityType = "objects", attributeName, entityNames, hideRange = false, hideValues = false, ocelVersion, extraColumns = [] }) => {
50
+ const isEvent = entityType === "events";
51
+ const { data, isFetching } = (isEvent ? useEventAttributes : useObjectAttributes)(ocelId, {
52
+ attribute_names: [attributeName],
53
+ names: entityNames,
54
+ ocel_version: ocelVersion
55
+ }, { query: { placeholderData: keepPreviousData } });
56
+ const columns = useMemo(() => [
57
+ {
58
+ accessor: "selector",
59
+ title: "",
60
+ width: COLUMN_WIDTHS$1.selector
61
+ },
62
+ {
63
+ accessor: "",
64
+ title: "Attribute Name",
65
+ width: COLUMN_WIDTHS$1.name
66
+ },
67
+ {
68
+ accessor: "entity_type",
69
+ title: isEvent ? "Activity" : "Object Type",
70
+ width: COLUMN_WIDTHS$1.entityType
71
+ },
72
+ {
73
+ accessor: "type",
74
+ title: "Data Type",
75
+ width: COLUMN_WIDTHS$1.type,
76
+ noWrap: true
77
+ },
78
+ {
79
+ accessor: "range",
80
+ width: COLUMN_WIDTHS$1.range,
81
+ render: ({ type, min, max }) => `${formatAttributeValue(type, min)} - ${formatAttributeValue(type, max)}`,
82
+ hidden: hideRange
83
+ },
84
+ {
85
+ accessor: "distinct_values",
86
+ title: "Values",
87
+ width: COLUMN_WIDTHS$1.distinctValues,
88
+ hidden: hideValues
89
+ },
90
+ ...extraColumns
91
+ ], [
92
+ isEvent,
93
+ hideRange,
94
+ hideValues,
95
+ extraColumns
96
+ ]);
97
+ return /* @__PURE__ */ jsx(DataTable, {
98
+ noHeader: true,
99
+ idAccessor: "entity_type",
100
+ records: data ?? [],
101
+ columns,
102
+ fetching: isFetching,
103
+ backgroundColor: {
104
+ light: "gray.0",
105
+ dark: "dark.6"
106
+ }
107
+ });
108
+ };
109
+ const AttributesTable = ({ ocelId, entityType = "objects", extraColumns = [], hideRange = false, hideValues = false, subTableExtraColumns, ocelVersion, alwaysExpandable = false, visibleAttributes }) => {
110
+ const isEvent = entityType === "events";
111
+ const [currentPage, setCurrentPage] = useState(1);
112
+ const { data: attributeNames } = useAttributeNames(ocelId, {
113
+ entity_type: entityType,
114
+ ocel_version: ocelVersion
115
+ });
116
+ const [filteredAttributes, setFilteredAttributes] = useState([]);
117
+ const { data: entityNames } = (isEvent ? useActivities : useObjectTypes)(ocelId, { ocel_version: ocelVersion });
118
+ const [filteredEntityNames, setFilteredEntityNames] = useState([]);
119
+ const isConstrained = visibleAttributes !== void 0;
120
+ const attributeNameOptions = useMemo(() => {
121
+ if (!isConstrained) return attributeNames ?? [];
122
+ return (attributeNames ?? []).filter((name) => visibleAttributes.includes(name));
123
+ }, [
124
+ attributeNames,
125
+ isConstrained,
126
+ visibleAttributes
127
+ ]);
128
+ const effectiveAttributeNames = useMemo(() => {
129
+ if (!isConstrained) return filteredAttributes;
130
+ return (filteredAttributes.length > 0 ? filteredAttributes : visibleAttributes).filter((name) => visibleAttributes.includes(name));
131
+ }, [
132
+ filteredAttributes,
133
+ isConstrained,
134
+ visibleAttributes
135
+ ]);
136
+ const showNoAttributes = isConstrained && effectiveAttributeNames.length === 0;
137
+ const { data, isFetching } = useAggregatedAttributes(ocelId, {
138
+ entity_type: entityType,
139
+ page: currentPage,
140
+ page_size: PAGE_SIZE$1,
141
+ ocel_version: ocelVersion,
142
+ ...effectiveAttributeNames.length > 0 && { attribute_names: effectiveAttributeNames },
143
+ ...filteredEntityNames.length > 0 && { entity_names: filteredEntityNames }
144
+ }, { query: {
145
+ placeholderData: keepPreviousData,
146
+ enabled: !showNoAttributes
147
+ } });
148
+ const [selectedAttributes, setSelectedAttributes] = useState([]);
149
+ const columns = useMemo(() => [
150
+ {
151
+ accessor: "selector",
152
+ title: "",
153
+ textAlign: "center",
154
+ render: ({ entity_type_names, name }) => {
155
+ if (!alwaysExpandable && entity_type_names.length === 1) return null;
156
+ return /* @__PURE__ */ jsx(ThemeIcon, {
157
+ size: "sm",
158
+ variant: "transparent",
159
+ style: { display: "flex" },
160
+ children: selectedAttributes.includes(name) ? /* @__PURE__ */ jsx(ChevronDownIcon, { size: 16 }) : /* @__PURE__ */ jsx(ChevronRightIcon, { size: 16 })
161
+ });
162
+ },
163
+ width: COLUMN_WIDTHS$1.selector,
164
+ cellsStyle: () => ({ padding: 5 })
165
+ },
166
+ {
167
+ accessor: "name",
168
+ title: "Attribute Name",
169
+ width: COLUMN_WIDTHS$1.name,
170
+ filter: () => /* @__PURE__ */ jsx(MultiSelect, {
171
+ data: attributeNameOptions,
172
+ value: filteredAttributes,
173
+ onChange: (newSelection) => setFilteredAttributes(newSelection),
174
+ comboboxProps: { withinPortal: false },
175
+ clearable: true,
176
+ searchable: true
177
+ }),
178
+ filtering: filteredAttributes.length > 0
179
+ },
180
+ {
181
+ accessor: "entityTypeField",
182
+ title: isEvent ? "Activity" : "Object Type",
183
+ width: COLUMN_WIDTHS$1.entityType,
184
+ render: ({ entity_type_names }) => `${entity_type_names.slice(0, 2).join(", ")}${entity_type_names.length > 3 ? `, ... (${entity_type_names.length} total)` : ""}`,
185
+ filter: () => /* @__PURE__ */ jsx(MultiSelect, {
186
+ data: entityNames,
187
+ value: filteredEntityNames,
188
+ onChange: (newSelection) => setFilteredEntityNames(newSelection),
189
+ comboboxProps: { withinPortal: false },
190
+ clearable: true,
191
+ searchable: true
192
+ }),
193
+ filtering: filteredEntityNames.length > 0
194
+ },
195
+ {
196
+ accessor: "type",
197
+ title: "Data Type",
198
+ width: COLUMN_WIDTHS$1.type
199
+ },
200
+ {
201
+ accessor: "range",
202
+ width: COLUMN_WIDTHS$1.range,
203
+ render: ({ type, min, max }) => `${formatAttributeValue(type, min)} - ${formatAttributeValue(type, max)}`,
204
+ hidden: hideRange
205
+ },
206
+ {
207
+ accessor: "distinct_values",
208
+ title: "Values",
209
+ width: COLUMN_WIDTHS$1.distinctValues,
210
+ hidden: hideValues
211
+ },
212
+ ...extraColumns
213
+ ], [
214
+ isEvent,
215
+ entityNames,
216
+ filteredEntityNames,
217
+ selectedAttributes,
218
+ filteredAttributes,
219
+ attributeNameOptions,
220
+ extraColumns,
221
+ hideRange,
222
+ hideValues,
223
+ alwaysExpandable
224
+ ]);
225
+ return /* @__PURE__ */ jsx(DataTable, {
226
+ idAccessor: "name",
227
+ records: showNoAttributes ? [] : data?.response,
228
+ columns,
229
+ withTableBorder: true,
230
+ fetching: isFetching,
231
+ totalRecords: showNoAttributes ? 0 : data?.total_items ?? 0,
232
+ page: currentPage,
233
+ recordsPerPage: PAGE_SIZE$1,
234
+ onPageChange: setCurrentPage,
235
+ noRecordsText: "No attributes found",
236
+ height: 500,
237
+ rowExpansion: {
238
+ allowMultiple: false,
239
+ expandable: ({ record: { entity_type_names } }) => alwaysExpandable || entity_type_names.length > 1,
240
+ expanded: {
241
+ recordIds: selectedAttributes,
242
+ onRecordIdsChange: setSelectedAttributes
243
+ },
244
+ content: ({ record }) => /* @__PURE__ */ jsx(SubAttributeTable, {
245
+ entityType,
246
+ attributeName: record.name,
247
+ ocelId,
248
+ extraColumns: subTableExtraColumns,
249
+ hideRange,
250
+ hideValues,
251
+ ocelVersion,
252
+ ...filteredEntityNames.length > 0 && { entityNames: filteredEntityNames }
253
+ })
254
+ }
255
+ });
27
256
  };
28
- var useInvalidate_default = useInvalidate;
29
-
30
257
  //#endregion
31
- //#region src/components/Dropzone/Dropzone.module.css?css_module
32
- const classes = {
33
- "control": "_0yGNhG_control",
34
- "icon": "_0yGNhG_icon",
35
- "description": "_0yGNhG_description",
36
- "wrapper": "_0yGNhG_wrapper",
37
- "dropzone": "_0yGNhG_dropzone"
38
- };
39
- var Dropzone_module_default = classes;
40
- const _control0 = classes["control"];
41
- const _icon0 = classes["icon"];
42
- const _description0 = classes["description"];
43
- const _wrapper0 = classes["wrapper"];
44
- const _dropzone0 = classes["dropzone"];
45
-
258
+ //#region src/components/BarList/BarList.module.css
259
+ var BarList_module_default = {
260
+ "bar": "uf8oba_bar",
261
+ "barCell": "uf8oba_barCell",
262
+ "container": "uf8oba_container",
263
+ "head": "uf8oba_head",
264
+ "headCount": "uf8oba_headCount",
265
+ "headLabel": "uf8oba_headLabel",
266
+ "labelInside": "uf8oba_labelInside",
267
+ "labelOutside": "uf8oba_labelOutside",
268
+ "scrollArea": "uf8oba_scrollArea",
269
+ "toggleRow": "uf8oba_toggleRow"
270
+ };
271
+ //#endregion
272
+ //#region src/components/BarList/BarList.tsx
273
+ const BarList = ({ data, labelHeader, valueHeader = "Frequency", maxVisibleItems = 8 }) => {
274
+ const [expanded, setExpanded] = useState(false);
275
+ const entries = useMemo(() => Object.entries(data).sort((a, b) => b[1] - a[1]), [data]);
276
+ const max = useMemo(() => Math.max(0, ...entries.map(([, c]) => c)), [entries]);
277
+ const typeCount = entries.length;
278
+ const totalFrequency = useMemo(() => entries.reduce((sum, [, count]) => sum + count, 0), [entries]);
279
+ const hasOverflow = typeCount > maxVisibleItems;
280
+ const visibleEntries = hasOverflow && !expanded ? entries.slice(0, maxVisibleItems) : entries;
281
+ const scrollRef = useRef(null);
282
+ const [collapsedHeight, setCollapsedHeight] = useState();
283
+ useLayoutEffect(() => {
284
+ if (!expanded && scrollRef.current) setCollapsedHeight(scrollRef.current.offsetHeight);
285
+ }, [
286
+ expanded,
287
+ typeCount,
288
+ maxVisibleItems
289
+ ]);
290
+ return /* @__PURE__ */ jsxs("div", {
291
+ className: BarList_module_default.container,
292
+ children: [/* @__PURE__ */ jsx("div", {
293
+ ref: scrollRef,
294
+ className: BarList_module_default.scrollArea,
295
+ style: expanded ? { maxHeight: collapsedHeight } : void 0,
296
+ children: /* @__PURE__ */ jsxs(Table, {
297
+ variant: "vertical",
298
+ layout: "fixed",
299
+ stickyHeader: true,
300
+ children: [/* @__PURE__ */ jsx(Table.Thead, {
301
+ className: BarList_module_default.head,
302
+ children: /* @__PURE__ */ jsxs(Table.Tr, { children: [/* @__PURE__ */ jsx(Table.Th, { children: /* @__PURE__ */ jsxs("span", {
303
+ className: BarList_module_default.headLabel,
304
+ children: [
305
+ labelHeader,
306
+ " ",
307
+ /* @__PURE__ */ jsxs("span", {
308
+ className: BarList_module_default.headCount,
309
+ children: [
310
+ "(",
311
+ typeCount.toLocaleString(),
312
+ ")"
313
+ ]
314
+ })
315
+ ]
316
+ }) }), /* @__PURE__ */ jsx(Table.Th, { children: /* @__PURE__ */ jsxs("span", {
317
+ className: BarList_module_default.headLabel,
318
+ children: [
319
+ valueHeader,
320
+ " ",
321
+ /* @__PURE__ */ jsxs("span", {
322
+ className: BarList_module_default.headCount,
323
+ children: [
324
+ "(",
325
+ totalFrequency.toLocaleString(),
326
+ ")"
327
+ ]
328
+ })
329
+ ]
330
+ }) })] })
331
+ }), /* @__PURE__ */ jsx(Table.Tbody, { children: visibleEntries.map(([name, count]) => {
332
+ const percentage = max > 0 ? Math.ceil(count / max * 100) : 0;
333
+ return /* @__PURE__ */ jsxs(Table.Tr, { children: [/* @__PURE__ */ jsx(Table.Td, { children: name }), /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs("div", {
334
+ className: BarList_module_default.barCell,
335
+ children: [/* @__PURE__ */ jsx("div", {
336
+ className: BarList_module_default.bar,
337
+ style: { width: `${percentage}%` },
338
+ children: percentage > 50 && /* @__PURE__ */ jsx("span", {
339
+ className: BarList_module_default.labelInside,
340
+ children: count
341
+ })
342
+ }), percentage <= 50 && /* @__PURE__ */ jsx("span", {
343
+ className: BarList_module_default.labelOutside,
344
+ children: count
345
+ })]
346
+ }) })] }, name);
347
+ }) })]
348
+ })
349
+ }), hasOverflow && /* @__PURE__ */ jsx(UnstyledButton, {
350
+ className: BarList_module_default.toggleRow,
351
+ onClick: () => setExpanded((prev) => !prev),
352
+ children: expanded ? "Show less" : `Show ${typeCount - maxVisibleItems} more`
353
+ })]
354
+ });
355
+ };
356
+ //#endregion
357
+ //#region src/components/Dropzone/Dropzone.module.css
358
+ var Dropzone_module_default = {
359
+ "control": "IgW5Wa_control",
360
+ "description": "IgW5Wa_description",
361
+ "dropzone": "IgW5Wa_dropzone",
362
+ "icon": "IgW5Wa_icon",
363
+ "wrapper": "IgW5Wa_wrapper"
364
+ };
46
365
  //#endregion
47
366
  //#region src/components/Dropzone/Dropzone.tsx
48
367
  const FileDropzone = ({ onUpload, accept, content }) => {
@@ -53,7 +372,7 @@ const FileDropzone = ({ onUpload, accept, content }) => {
53
372
  children: [/* @__PURE__ */ jsx(Dropzone, {
54
373
  openRef,
55
374
  onDrop: onUpload,
56
- className: Dropzone_module_default.dropzone,
375
+ className: Dropzone_module_default.dropzone ?? "",
57
376
  radius: "md",
58
377
  accept: accept ?? {},
59
378
  children: /* @__PURE__ */ jsxs("div", {
@@ -66,11 +385,11 @@ const FileDropzone = ({ onUpload, accept, content }) => {
66
385
  size: 50,
67
386
  color: theme.colors.blue[6]
68
387
  }) }),
69
- /* @__PURE__ */ jsx(Dropzone.Reject, { children: /* @__PURE__ */ jsx(X, {
388
+ /* @__PURE__ */ jsx(Dropzone.Reject, { children: /* @__PURE__ */ jsx(XIcon, {
70
389
  size: 50,
71
390
  color: theme.colors.red[6]
72
391
  }) }),
73
- /* @__PURE__ */ jsx(Dropzone.Idle, { children: /* @__PURE__ */ jsx(Upload, {
392
+ /* @__PURE__ */ jsx(Dropzone.Idle, { children: /* @__PURE__ */ jsx(UploadIcon, {
74
393
  size: 50,
75
394
  className: Dropzone_module_default.icon
76
395
  }) })
@@ -88,13 +407,13 @@ const FileDropzone = ({ onUpload, accept, content }) => {
88
407
  ]
89
408
  }),
90
409
  /* @__PURE__ */ jsx(Text, {
91
- className: Dropzone_module_default.description,
410
+ className: Dropzone_module_default.description ?? "",
92
411
  children: content.description || /* @__PURE__ */ jsx("span", { children: "Drag'n'drop files here to upload." })
93
412
  })
94
413
  ]
95
414
  })
96
415
  }), /* @__PURE__ */ jsx(Button, {
97
- className: Dropzone_module_default.control,
416
+ className: Dropzone_module_default.control ?? "",
98
417
  size: "sm",
99
418
  radius: "xl",
100
419
  onClick: () => openRef.current?.(),
@@ -102,13 +421,167 @@ const FileDropzone = ({ onUpload, accept, content }) => {
102
421
  })]
103
422
  });
104
423
  };
105
- var Dropzone_default = FileDropzone;
106
-
424
+ //#endregion
425
+ //#region src/components/EventHandler.ts
426
+ const apiBase = process.env.EXTERNAL_API_BASE_URL ?? "http://localhost:8000";
427
+ const sseHandler = async (req, res) => {
428
+ const session_id = req.query.sessionId;
429
+ const backendUrl = `${apiBase}/sse?session_id=${encodeURIComponent(session_id || "")}`;
430
+ try {
431
+ const controller = new AbortController();
432
+ const backendRes = await fetch(backendUrl, {
433
+ headers: { Accept: "text/event-stream" },
434
+ signal: controller.signal
435
+ });
436
+ if (!backendRes.body) {
437
+ res.status(502).end("No response body from backend");
438
+ return;
439
+ }
440
+ res.writeHead(200, {
441
+ "Content-Type": "text/event-stream",
442
+ "Cache-Control": "no-cache, no-transform",
443
+ Connection: "keep-alive"
444
+ });
445
+ const webStream = backendRes.body;
446
+ const nodeStream = Readable.fromWeb(webStream);
447
+ nodeStream.pipe(res);
448
+ res.on("close", () => {
449
+ console.log("Client disconnected");
450
+ controller.abort();
451
+ nodeStream.destroy();
452
+ res.end();
453
+ });
454
+ } catch (err) {
455
+ console.error("SSE proxy error:", err);
456
+ res.status(500).end("SSE proxy error");
457
+ }
458
+ };
459
+ //#endregion
460
+ //#region src/components/ModulePage.tsx
461
+ const createModulesPage = (config) => {
462
+ const { modules = [] } = config;
463
+ const getStaticPaths = async () => {
464
+ return {
465
+ paths: [...modules.flatMap(({ name: moduleName, routes }) => [...routes.map(({ name: routeName }) => ({ params: { slug: [moduleName, routeName] } })), { params: { slug: [moduleName] } }]), { params: { slug: [] } }],
466
+ fallback: false
467
+ };
468
+ };
469
+ const getStaticProps = async ({ params }) => {
470
+ const slugs = params?.slug ?? [];
471
+ const moduleDef = slugs[0] ? modules.find(({ name }) => name === slugs[0]) : modules[0];
472
+ const routeDef = slugs[1] ? moduleDef?.routes.find(({ name }) => name === slugs[1]) : moduleDef?.routes[0];
473
+ if (!moduleDef || !routeDef) return { notFound: true };
474
+ return { props: {
475
+ moduleName: moduleDef.name,
476
+ routeName: routeDef.name
477
+ } };
478
+ };
479
+ const ModulePage = ({ moduleName, routeName }) => {
480
+ const RouteComponent = modules.find(({ name }) => name === moduleName)?.routes.find(({ name }) => name === routeName)?.component;
481
+ return RouteComponent ? /* @__PURE__ */ jsx(RouteComponent, {}) : null;
482
+ };
483
+ return {
484
+ getStaticPaths,
485
+ getStaticProps,
486
+ ModulePage
487
+ };
488
+ };
489
+ //#endregion
490
+ //#region src/store/currentOcelStore.ts
491
+ const useCurrentOcelStore = create()(persist((set) => ({
492
+ ocelId: null,
493
+ setOcel: (id) => set({ ocelId: id }),
494
+ clearOcel: () => set({ ocelId: null })
495
+ }), { name: env$1.current_ocel_id }));
496
+ //#endregion
497
+ //#region src/hooks/useCurrentOCEL.ts
498
+ const useCurrentOcel = () => {
499
+ const { data: ocels = [] } = useGetOcels();
500
+ const { ocelId, setOcel, clearOcel } = useCurrentOcelStore();
501
+ useEffect(() => {
502
+ if (ocelId && !ocels?.some(({ id }) => id === ocelId)) clearOcel();
503
+ if (!ocelId && ocels[0]) setOcel(ocels[0].id);
504
+ }, [
505
+ clearOcel,
506
+ setOcel,
507
+ ocels,
508
+ ocelId
509
+ ]);
510
+ return {
511
+ id: ocelId,
512
+ setCurrentOcel: setOcel
513
+ };
514
+ };
515
+ //#endregion
516
+ //#region src/components/OcelSelect/OcelSelect.tsx
517
+ const FilteredIndicator = () => /* @__PURE__ */ jsx(Tooltip, {
518
+ label: "This log has been filtered",
519
+ children: /* @__PURE__ */ jsx(FilterIcon, {
520
+ size: 14,
521
+ color: "var(--mantine-color-blue-6)"
522
+ })
523
+ });
524
+ const OcelSelect = ({ extension, value, onChange, placeholder = "Select an OCEL", ...inputProps }) => {
525
+ const { data } = useGetOcels(extension ? { extension_name: extension } : {});
526
+ const ocels = useMemo(() => data ?? [], [data]);
527
+ const combobox = useCombobox({ onDropdownClose: () => combobox.resetSelectedOption() });
528
+ const selected = useMemo(() => ocels.find((ocel) => ocel.id === value), [ocels, value]);
529
+ return /* @__PURE__ */ jsxs(Combobox, {
530
+ store: combobox,
531
+ onOptionSubmit: (val) => {
532
+ onChange?.(val);
533
+ combobox.closeDropdown();
534
+ },
535
+ children: [/* @__PURE__ */ jsx(Combobox.Target, { children: /* @__PURE__ */ jsx(InputBase, {
536
+ ...inputProps,
537
+ component: "button",
538
+ type: "button",
539
+ pointer: true,
540
+ rightSection: /* @__PURE__ */ jsx(Combobox.Chevron, {}),
541
+ rightSectionPointerEvents: "none",
542
+ onClick: () => combobox.toggleDropdown(),
543
+ children: selected ? /* @__PURE__ */ jsxs(Group, {
544
+ gap: 6,
545
+ wrap: "nowrap",
546
+ style: { overflow: "hidden" },
547
+ children: [/* @__PURE__ */ jsx(Text, {
548
+ span: true,
549
+ size: "sm",
550
+ truncate: true,
551
+ children: selected.name
552
+ }), selected.filter_applied && /* @__PURE__ */ jsx(FilteredIndicator, {})]
553
+ }) : /* @__PURE__ */ jsx(Input.Placeholder, { children: placeholder })
554
+ }) }), /* @__PURE__ */ jsx(Combobox.Dropdown, { children: /* @__PURE__ */ jsx(Combobox.Options, { children: ocels.map((ocel) => /* @__PURE__ */ jsx(Combobox.Option, {
555
+ value: ocel.id,
556
+ children: /* @__PURE__ */ jsxs(Group, {
557
+ gap: 6,
558
+ wrap: "nowrap",
559
+ children: [ocel.name, ocel.filter_applied && /* @__PURE__ */ jsx(FilteredIndicator, {})]
560
+ })
561
+ }, ocel.id)) }) })]
562
+ });
563
+ };
564
+ const CurrentOcelSelect = () => {
565
+ const { id, setCurrentOcel } = useCurrentOcel();
566
+ return /* @__PURE__ */ jsx(OcelSelect, {
567
+ value: id,
568
+ w: 250,
569
+ onChange: (id) => {
570
+ if (id) setCurrentOcel(id);
571
+ }
572
+ });
573
+ };
574
+ //#endregion
575
+ //#region src/hooks/useInvalidate.ts
576
+ const useInvalidate = () => {
577
+ const queryClient = useQueryClient();
578
+ return async (routeNames) => await queryClient.invalidateQueries({ predicate: (query) => typeof query.queryKey[0] === "string" && routeNames.some((route) => query.queryKey[0].includes(`/${route}`)) });
579
+ };
107
580
  //#endregion
108
581
  //#region src/components/UploadSection/UploadSection.tsx
109
582
  const DefaultOcels = ({ onSuccess, setIsPending }) => {
110
583
  const { data: ocels = [] } = useGetDefaultOcel({});
111
- const invalidate = useInvalidate_default();
584
+ const invalidate = useInvalidate();
112
585
  const { mutate: uploadDefault } = useImportDefaultOcel({ mutation: {
113
586
  onSuccess: async () => {
114
587
  await invalidate(["ocels"]);
@@ -149,7 +622,7 @@ const FileUploadZone = ({ onSuccess, setIsPending }) => {
149
622
  onMutate: () => setIsPending(true),
150
623
  onSettled: () => setIsPending(false)
151
624
  } });
152
- return /* @__PURE__ */ jsx(Dropzone_default, {
625
+ return /* @__PURE__ */ jsx(FileDropzone, {
153
626
  content: { description: /* @__PURE__ */ jsx("span", { children: `Drag'n'drop your ${[
154
627
  "OCELs",
155
628
  "Plugins",
@@ -162,8 +635,10 @@ const FileUploadZone = ({ onSuccess, setIsPending }) => {
162
635
  };
163
636
  const UploadSection = ({ onSuccess }) => {
164
637
  const [isPending, setIsPending] = useState(false);
165
- const onUploadSuccess = useCallback(() => {
638
+ const invalidate = useInvalidate();
639
+ const onUploadSuccess = useCallback(async () => {
166
640
  setIsPending(false);
641
+ await invalidate(["tasks"]);
167
642
  onSuccess?.();
168
643
  }, [onSuccess]);
169
644
  return /* @__PURE__ */ jsxs(Stack, {
@@ -188,8 +663,6 @@ const UploadSection = ({ onSuccess }) => {
188
663
  ]
189
664
  });
190
665
  };
191
- var UploadSection_default = UploadSection;
192
-
193
666
  //#endregion
194
667
  //#region src/components/UploadModal/UploadModal.tsx
195
668
  const UploadModal = ({ visible, onClose }) => {
@@ -201,68 +674,164 @@ const UploadModal = ({ visible, onClose }) => {
201
674
  opened: visible,
202
675
  onClose,
203
676
  size: "xl",
204
- children: /* @__PURE__ */ jsx(UploadSection_default, { onSuccess: onClose })
677
+ children: /* @__PURE__ */ jsx(UploadSection, { onSuccess: onClose })
205
678
  });
206
679
  };
207
- var UploadModal_default = UploadModal;
208
-
209
680
  //#endregion
210
- //#region src/store/currentOcelStore.ts
211
- const useCurrentOcelStore = create()(persist((set) => ({
212
- ocelId: null,
213
- setOcel: (id) => set({ ocelId: id }),
214
- clearOcel: () => set({ ocelId: null })
215
- }), { name: env.current_ocel_id }));
216
- var currentOcelStore_default = useCurrentOcelStore;
217
-
681
+ //#region src/hooks/useModulePath.ts
682
+ const useModulePath = (config) => {
683
+ const { slug } = useRouter().query;
684
+ const [moduleName, routeName] = [slug?.[0], slug?.[1]];
685
+ return useMemo(() => {
686
+ const moduleConfig = config?.modules?.find(({ name }) => name === moduleName);
687
+ const routeConfig = moduleConfig?.routes.find(({ name }) => name === routeName);
688
+ return {
689
+ moduleName: moduleConfig?.name,
690
+ routeName: routeConfig?.name
691
+ };
692
+ }, [moduleName, routeName]);
693
+ };
218
694
  //#endregion
219
- //#region src/hooks/useCurrentOCEL.ts
220
- const useCurrentOcel = () => {
221
- const { data: ocels = [] } = useGetOcels();
222
- const { ocelId, setOcel, clearOcel } = currentOcelStore_default();
223
- useEffect(() => {
224
- if (ocelId && !ocels?.some(({ id }) => id === ocelId)) clearOcel();
225
- if (!ocelId && ocels[0]) setOcel(ocels[0].id);
226
- }, [
227
- clearOcel,
228
- setOcel,
229
- ocels,
230
- ocelId
231
- ]);
695
+ //#region src/lib/getModuleRoute.ts
696
+ const getModuleRoute = ({ moduleName, routeName, query }) => {
232
697
  return {
233
- id: ocelId,
234
- setCurrentOcel: setOcel
698
+ pathname: `/${moduleName}${routeName ? `/${routeName}` : ""}`,
699
+ ...query && { query: { ...query } }
235
700
  };
236
701
  };
237
- var useCurrentOCEL_default = useCurrentOcel;
238
-
239
702
  //#endregion
240
- //#region src/components/OcelSelect/OcelSelect.tsx
241
- const OcelSelect = ({ extension, ...props }) => {
242
- const { data } = useGetOcels(extension ? { extension_name: extension } : {});
243
- const ocels = useMemo(() => data ?? [], [data]);
244
- return /* @__PURE__ */ jsx(Select, {
245
- ...props,
246
- data: ocels.map(({ name, id }) => ({
247
- value: id,
248
- label: name
249
- }))
703
+ //#region src/components/AppShell/AppShell.module.css
704
+ var AppShell_module_default = {
705
+ "button": "rZS5FG_button",
706
+ "buttonIcon": "rZS5FG_buttonIcon",
707
+ "chevron": "rZS5FG_chevron",
708
+ "control": "rZS5FG_control",
709
+ "footer": "rZS5FG_footer",
710
+ "header": "rZS5FG_header",
711
+ "link": "rZS5FG_link",
712
+ "linkIcon": "rZS5FG_linkIcon",
713
+ "links": "rZS5FG_links",
714
+ "navbar": "rZS5FG_navbar"
715
+ };
716
+ //#endregion
717
+ //#region src/components/AppShell/components/NavBar.tsx
718
+ const LogoutButton = () => {
719
+ const [isModalOpen, setIsModalOpen] = useState(false);
720
+ const queryClient = useQueryClient();
721
+ const { push } = useRouter();
722
+ const { mutate: logout } = useLogout({ mutation: { onSuccess: () => {
723
+ queryClient.clear();
724
+ setIsModalOpen(false);
725
+ push("/");
726
+ } } });
727
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Modal, {
728
+ opened: isModalOpen,
729
+ onClose: () => setIsModalOpen(false),
730
+ title: "Are you sure?",
731
+ children: [/* @__PURE__ */ jsxs(Text, { children: [
732
+ "If you leave now, all your data and progress will be",
733
+ " ",
734
+ /* @__PURE__ */ jsx("strong", { children: "deleted permanently" }),
735
+ ". This action cannot be undone."
736
+ ] }), /* @__PURE__ */ jsx(Button, {
737
+ color: "red",
738
+ mt: "md",
739
+ onClick: () => {
740
+ logout();
741
+ },
742
+ fullWidth: true,
743
+ children: "Accept"
744
+ })]
745
+ }), /* @__PURE__ */ jsx(UnstyledButton, {
746
+ className: AppShell_module_default.button ?? "",
747
+ onClick: () => setIsModalOpen(true),
748
+ children: /* @__PURE__ */ jsxs(Group, {
749
+ justify: "space-between",
750
+ w: "100%",
751
+ children: [/* @__PURE__ */ jsx(Text, {
752
+ style: { lineHeight: 1 },
753
+ children: "Logout"
754
+ }), /* @__PURE__ */ jsx(LogOutIcon, { className: AppShell_module_default.buttonIcon })]
755
+ })
756
+ })] });
757
+ };
758
+ const NavbarGroup = ({ modules, modulePath, isOcelAvailable }) => {
759
+ return modules.map(({ label, name, icon: Icon = PackageIcon, routes }) => {
760
+ const isModuleDisabled = !Object.values(routes).some(({ requiresOcel }) => !requiresOcel) && !isOcelAvailable;
761
+ return /* @__PURE__ */ jsx(NavLink, {
762
+ leftSection: /* @__PURE__ */ jsx(Icon, {
763
+ width: 18,
764
+ height: 18,
765
+ size: 18
766
+ }),
767
+ label,
768
+ defaultOpened: name === modulePath?.moduleName,
769
+ component: Link,
770
+ href: getModuleRoute({ moduleName: name }),
771
+ disabled: isModuleDisabled,
772
+ ...isModuleDisabled ? { opened: false } : {},
773
+ active: Object.keys(routes).length === 1 && name === modulePath?.moduleName,
774
+ children: Object.keys(routes).length > 1 && Object.values(routes).map(({ label: routeLabel, name: routeName, requiresOcel }) => /* @__PURE__ */ jsx(NavLink, {
775
+ label: routeLabel,
776
+ href: getModuleRoute({
777
+ moduleName: name,
778
+ routeName
779
+ }),
780
+ disabled: !!requiresOcel && !isOcelAvailable,
781
+ component: Link,
782
+ active: name === modulePath?.moduleName && routeName === modulePath.routeName
783
+ }, routeName))
784
+ }, name);
250
785
  });
251
786
  };
252
- const CurrentOcelSelect = () => {
253
- const { id, setCurrentOcel } = useCurrentOCEL_default();
254
- return /* @__PURE__ */ jsx(OcelSelect, {
255
- value: id,
256
- unselectable: "off",
257
- onChange: (id$1) => {
258
- if (id$1) setCurrentOcel(id$1);
259
- }
787
+ const NavBar = ({ config }) => {
788
+ const { modules = [], navbarGroups } = config;
789
+ const modulePath = useModulePath(config);
790
+ const { data: ocels } = useGetOcels();
791
+ const isOcelAvailable = ocels?.length !== 0;
792
+ const moduleGroups = useMemo(() => {
793
+ if (!navbarGroups) return [{
794
+ title: void 0,
795
+ modules
796
+ }];
797
+ const groupedModuleNames = navbarGroups.flatMap(({ modulesNames }) => modulesNames);
798
+ const ungroupedModules = modules.filter(({ name }) => !groupedModuleNames.includes(name));
799
+ return [...navbarGroups.map(({ modulesNames, title }) => ({
800
+ title,
801
+ modules: modulesNames.map((moduleName) => modules.find(({ name }) => name === moduleName)).filter((module) => !!module)
802
+ })), ...ungroupedModules.length > 0 ? [{
803
+ title: void 0,
804
+ modules: ungroupedModules
805
+ }] : []];
806
+ }, [modules, navbarGroups]);
807
+ return /* @__PURE__ */ jsx(AppShell.Navbar, {
808
+ className: AppShell_module_default.navbar ?? "",
809
+ children: /* @__PURE__ */ jsxs(Stack, {
810
+ justify: "space-between",
811
+ h: "100%",
812
+ gap: 0,
813
+ children: [
814
+ /* @__PURE__ */ jsx(Stack, {
815
+ gap: 0,
816
+ flex: 1,
817
+ children: moduleGroups.map(({ modules, title }, index) => /* @__PURE__ */ jsxs(Fragment, { children: [index > 0 && /* @__PURE__ */ jsx(Divider, {
818
+ label: title,
819
+ my: !title ? 9 : void 0
820
+ }), /* @__PURE__ */ jsx(NavbarGroup, {
821
+ modules,
822
+ isOcelAvailable,
823
+ modulePath
824
+ })] }, index))
825
+ }),
826
+ /* @__PURE__ */ jsx(Divider, {}),
827
+ /* @__PURE__ */ jsx(LogoutButton, {})
828
+ ]
829
+ })
260
830
  });
261
831
  };
262
-
263
832
  //#endregion
264
833
  //#region src/components/AppShell/AppShell.tsx
265
- const AppShell$1 = ({ children }) => {
834
+ const AppShell$1 = ({ config, children }) => {
266
835
  const [mobileOpened, { toggle: toggleMobile }] = useDisclosure(true);
267
836
  const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
268
837
  const [isUploadModalVisible, setIsUploadModalVisible] = useState(false);
@@ -302,31 +871,168 @@ const AppShell$1 = ({ children }) => {
302
871
  onClick: () => setIsUploadModalVisible(true),
303
872
  children: "Upload"
304
873
  }),
305
- /* @__PURE__ */ jsx(UploadModal_default, {
874
+ /* @__PURE__ */ jsx(UploadModal, {
306
875
  visible: isUploadModalVisible,
307
876
  onClose: () => setIsUploadModalVisible(false)
308
877
  })
309
878
  ] })]
310
879
  }) }),
311
- /* @__PURE__ */ jsx(AppShell.Navbar, {}),
312
- /* @__PURE__ */ jsx(AppShell.Main, { children })
880
+ /* @__PURE__ */ jsx(NavBar, { config }),
881
+ /* @__PURE__ */ jsx(AppShell.Main, {
882
+ style: { overflow: "hidden" },
883
+ h: "calc(100dvh - var(--app-shell-header-offset, 0rem) - var(--app-shell-footer-height, 0px) + var(--app-shell-padding, 0))",
884
+ children: /* @__PURE__ */ jsx(Box, {
885
+ h: "100%",
886
+ style: {
887
+ overflow: "scroll",
888
+ position: "relative"
889
+ },
890
+ children
891
+ })
892
+ })
313
893
  ]
314
894
  });
315
895
  };
316
-
896
+ //#endregion
897
+ //#region src/lib/sse.ts
898
+ const OCELLink = z.object({
899
+ type: z.literal("ocel"),
900
+ ocel_id: z.string()
901
+ });
902
+ const ResourceLink = z.object({
903
+ type: z.literal("resource"),
904
+ resource_id: z.string()
905
+ });
906
+ const PluginLink = z.object({
907
+ type: z.literal("plugin"),
908
+ id: z.string(),
909
+ method: z.string(),
910
+ task_id: z.string()
911
+ });
912
+ const SystemLink = z.discriminatedUnion("type", [
913
+ OCELLink,
914
+ ResourceLink,
915
+ PluginLink
916
+ ]);
917
+ const SystemNotification = z.object({
918
+ type: z.literal("notification"),
919
+ title: z.string(),
920
+ message: z.string(),
921
+ notification_type: z.enum([
922
+ "warning",
923
+ "info",
924
+ "error"
925
+ ]),
926
+ link: SystemLink.nullish()
927
+ });
928
+ const InvalidationRequest = z.object({
929
+ type: z.literal("invalidation"),
930
+ routes: z.array(z.enum([
931
+ "ocels",
932
+ "resources",
933
+ "tasks",
934
+ "plugins"
935
+ ]))
936
+ });
937
+ const ErrorNotification = z.object({
938
+ type: z.literal("error"),
939
+ title: z.string(),
940
+ message: z.string(),
941
+ trace: z.string()
942
+ });
943
+ const ServerEventMessage = z.discriminatedUnion("type", [
944
+ SystemNotification,
945
+ InvalidationRequest,
946
+ ErrorNotification
947
+ ]);
948
+ //#endregion
949
+ //#region src/components/SSEWrapper.tsx
950
+ const SSEWrapper = () => {
951
+ const { sessionId } = useSessionStore();
952
+ const invalidate = useInvalidate();
953
+ useEffect(() => {
954
+ if (!sessionId) return;
955
+ const es = new EventSource(`${env$1.event_url}?sessionId=${sessionId}`);
956
+ es.onmessage = async (event) => {
957
+ const result = ServerEventMessage.safeParse(JSON.parse(event.data));
958
+ if (!result.success) {
959
+ console.warn("Invalid SSE message", event.data, result.error);
960
+ return;
961
+ }
962
+ const message = result.data;
963
+ switch (message.type) {
964
+ case "notification":
965
+ showNotification({
966
+ title: message.title,
967
+ message: message.message,
968
+ color: "green"
969
+ });
970
+ break;
971
+ case "error":
972
+ showNotification({
973
+ title: message.title,
974
+ color: "red",
975
+ autoClose: 6e4,
976
+ message: /* @__PURE__ */ jsxs(Stack, {
977
+ gap: 6,
978
+ align: "flex-start",
979
+ children: [/* @__PURE__ */ jsx(Text, {
980
+ size: "sm",
981
+ style: { flex: 1 },
982
+ lineClamp: 2,
983
+ children: message.message
984
+ }), /* @__PURE__ */ jsx(CopyButton, {
985
+ value: message.trace ?? "",
986
+ timeout: 1500,
987
+ children: ({ copied, copy }) => /* @__PURE__ */ jsx(Text, {
988
+ size: "xs",
989
+ c: copied ? "teal" : "red",
990
+ td: "underline",
991
+ style: {
992
+ cursor: "pointer",
993
+ whiteSpace: "nowrap",
994
+ marginTop: 2
995
+ },
996
+ onClick: copy,
997
+ children: copied ? "Copied" : "Copy trace"
998
+ })
999
+ })]
1000
+ })
1001
+ });
1002
+ break;
1003
+ case "invalidation": await invalidate(message.routes);
1004
+ }
1005
+ };
1006
+ es.onerror = (err) => {
1007
+ console.error("SSE error:", err);
1008
+ };
1009
+ return () => es.close();
1010
+ }, [sessionId, invalidate]);
1011
+ return null;
1012
+ };
317
1013
  //#endregion
318
1014
  //#region src/components/OcelescopeApp.tsx
319
- const OcelescopeApp = ({ Component, pageProps }) => {
1015
+ const theme = createTheme({ fontWeights: { medium: "500" } });
1016
+ const OcelescopeApp = (config) => ({ Component, pageProps }) => {
320
1017
  const [client] = useState(() => new QueryClient());
321
1018
  return /* @__PURE__ */ jsxs(QueryClientProvider, {
322
1019
  client,
323
1020
  children: [/* @__PURE__ */ jsx(HydrationBoundary, {
324
1021
  state: pageProps.dehydratedState,
325
- children: /* @__PURE__ */ jsx(MantineProvider, { children: /* @__PURE__ */ jsx(AppShell$1, { children: /* @__PURE__ */ jsx(Component, {}) }) })
1022
+ children: /* @__PURE__ */ jsxs(MantineProvider, {
1023
+ theme,
1024
+ children: [
1025
+ /* @__PURE__ */ jsx(SSEWrapper, {}),
1026
+ /* @__PURE__ */ jsx(Notifications, {}),
1027
+ /* @__PURE__ */ jsx(AppShell$1, {
1028
+ config,
1029
+ children: /* @__PURE__ */ jsx(Component, { ...pageProps })
1030
+ })
1031
+ ]
1032
+ })
326
1033
  }), /* @__PURE__ */ jsx(ReactQueryDevtools, {})]
327
1034
  });
328
1035
  };
329
-
330
1036
  //#endregion
331
1037
  //#region src/components/OcelescopeDocument.tsx
332
1038
  const OcelescopeDocument = () => {
@@ -336,16 +1042,266 @@ const OcelescopeDocument = () => {
336
1042
  children: [/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx(ColorSchemeScript, { defaultColorScheme: "auto" }) }), /* @__PURE__ */ jsxs("body", { children: [/* @__PURE__ */ jsx(Main, {}), /* @__PURE__ */ jsx(NextScript, {})] })]
337
1043
  });
338
1044
  };
339
-
1045
+ //#endregion
1046
+ //#region src/components/RelationTable.tsx
1047
+ const PAGE_SIZE = 10;
1048
+ const COLUMN_WIDTHS = {
1049
+ selector: 30,
1050
+ source: 220,
1051
+ target: 220,
1052
+ qualifier: 220,
1053
+ range: 160,
1054
+ total: 120
1055
+ };
1056
+ const relationId = ({ source, target }) => `${source} ${target}`;
1057
+ const SubRelationTable = ({ ocelId, relationType, direction, source, target, hideRange = false, hideTotal = false, ocelVersion, extraColumns = [] }) => {
1058
+ const { data, isFetching } = (relationType === "e2o" ? useE2o : useO2o)(ocelId, {
1059
+ direction,
1060
+ source_types: [source],
1061
+ target_types: [target],
1062
+ with_qualifier: true,
1063
+ ocel_version: ocelVersion
1064
+ }, { query: { placeholderData: keepPreviousData } });
1065
+ const columns = useMemo(() => [
1066
+ {
1067
+ accessor: "selector",
1068
+ title: "",
1069
+ width: COLUMN_WIDTHS.selector
1070
+ },
1071
+ {
1072
+ accessor: "source",
1073
+ title: "",
1074
+ width: COLUMN_WIDTHS.source,
1075
+ render: () => null
1076
+ },
1077
+ {
1078
+ accessor: "target",
1079
+ title: "",
1080
+ width: COLUMN_WIDTHS.target,
1081
+ render: () => null
1082
+ },
1083
+ {
1084
+ accessor: "qualifier",
1085
+ title: "Qualifier",
1086
+ width: COLUMN_WIDTHS.qualifier,
1087
+ render: ({ qualifier }) => qualifier || /* @__PURE__ */ jsx("i", { children: "No qualifier" })
1088
+ },
1089
+ {
1090
+ accessor: "range",
1091
+ title: "Range",
1092
+ width: COLUMN_WIDTHS.range,
1093
+ render: ({ min_count, max_count }) => `${min_count} - ${max_count}`,
1094
+ hidden: hideRange
1095
+ },
1096
+ {
1097
+ accessor: "sum",
1098
+ title: "Total",
1099
+ width: COLUMN_WIDTHS.total,
1100
+ hidden: hideTotal
1101
+ },
1102
+ ...extraColumns
1103
+ ], [
1104
+ hideRange,
1105
+ hideTotal,
1106
+ extraColumns
1107
+ ]);
1108
+ return /* @__PURE__ */ jsx(DataTable, {
1109
+ noHeader: true,
1110
+ idAccessor: "qualifier",
1111
+ records: data?.response ?? [],
1112
+ columns,
1113
+ fetching: isFetching,
1114
+ backgroundColor: {
1115
+ light: "gray.0",
1116
+ dark: "dark.6"
1117
+ }
1118
+ });
1119
+ };
1120
+ const RelationTable = ({ ocelId, relationType = "o2o", direction = "source", extraColumns = [], subTableExtraColumns, hideRange = false, hideTotal = false, ocelVersion, visibleRelations }) => {
1121
+ const isE2O = relationType === "e2o";
1122
+ const isSource = direction === "source";
1123
+ const [currentPage, setCurrentPage] = useState(1);
1124
+ const { data: combinations = [] } = (isE2O ? useE2oCombinations : useO2oCombinations)(ocelId, {
1125
+ direction,
1126
+ ocel_version: ocelVersion
1127
+ });
1128
+ const sourceOptions = useMemo(() => Array.from(new Set(combinations.map(({ source }) => source))), [combinations]);
1129
+ const targetOptions = useMemo(() => Array.from(new Set(combinations.map(({ target }) => target))), [combinations]);
1130
+ const qualifierOptions = useMemo(() => Array.from(new Set(combinations.map(({ qualifier }) => qualifier))), [combinations]);
1131
+ const sourceTitle = isE2O ? isSource ? "Activity" : "Object Type" : "Source";
1132
+ const targetTitle = isE2O ? isSource ? "Object Type" : "Activity" : "Target";
1133
+ const [filteredSources, setFilteredSources] = useState([]);
1134
+ const [filteredTargets, setFilteredTargets] = useState([]);
1135
+ const [filteredQualifiers, setFilteredQualifiers] = useState([]);
1136
+ const isConstrained = visibleRelations !== void 0;
1137
+ const visibleSources = useMemo(() => isConstrained ? Array.from(new Set(visibleRelations.map(({ source }) => source))) : [], [isConstrained, visibleRelations]);
1138
+ const visibleTargets = useMemo(() => isConstrained ? Array.from(new Set(visibleRelations.map(({ target }) => target))) : [], [isConstrained, visibleRelations]);
1139
+ const sourceSelectOptions = isConstrained ? sourceOptions.filter((option) => visibleSources.includes(option)) : sourceOptions;
1140
+ const targetSelectOptions = isConstrained ? targetOptions.filter((option) => visibleTargets.includes(option)) : targetOptions;
1141
+ const effectiveSources = useMemo(() => {
1142
+ if (!isConstrained) return filteredSources;
1143
+ return filteredSources.length > 0 ? filteredSources.filter((source) => visibleSources.includes(source)) : visibleSources;
1144
+ }, [
1145
+ isConstrained,
1146
+ filteredSources,
1147
+ visibleSources
1148
+ ]);
1149
+ const effectiveTargets = useMemo(() => {
1150
+ if (!isConstrained) return filteredTargets;
1151
+ return filteredTargets.length > 0 ? filteredTargets.filter((target) => visibleTargets.includes(target)) : visibleTargets;
1152
+ }, [
1153
+ isConstrained,
1154
+ filteredTargets,
1155
+ visibleTargets
1156
+ ]);
1157
+ const showNoRelations = isConstrained && (effectiveSources.length === 0 || effectiveTargets.length === 0);
1158
+ const { data, isFetching } = (isE2O ? useE2o : useO2o)(ocelId, {
1159
+ direction,
1160
+ page: currentPage,
1161
+ page_size: PAGE_SIZE,
1162
+ with_qualifier: false,
1163
+ ocel_version: ocelVersion,
1164
+ ...effectiveSources.length > 0 && { source_types: effectiveSources },
1165
+ ...effectiveTargets.length > 0 && { target_types: effectiveTargets },
1166
+ ...filteredQualifiers.length > 0 && { qualifiers: filteredQualifiers }
1167
+ }, { query: {
1168
+ placeholderData: keepPreviousData,
1169
+ enabled: !showNoRelations
1170
+ } });
1171
+ const [selectedRelations, setSelectedRelations] = useState([]);
1172
+ const columns = useMemo(() => [
1173
+ {
1174
+ accessor: "selector",
1175
+ title: "",
1176
+ textAlign: "center",
1177
+ width: COLUMN_WIDTHS.selector,
1178
+ cellsStyle: () => ({ padding: 5 }),
1179
+ render: (record) => {
1180
+ if ((record.qualifiers?.length ?? 0) <= 1) return null;
1181
+ return /* @__PURE__ */ jsx(ThemeIcon, {
1182
+ size: "sm",
1183
+ variant: "transparent",
1184
+ style: { display: "flex" },
1185
+ children: selectedRelations.includes(relationId(record)) ? /* @__PURE__ */ jsx(ChevronDownIcon, { size: 16 }) : /* @__PURE__ */ jsx(ChevronRightIcon, { size: 16 })
1186
+ });
1187
+ }
1188
+ },
1189
+ {
1190
+ accessor: "source",
1191
+ title: sourceTitle,
1192
+ width: COLUMN_WIDTHS.source,
1193
+ filter: () => /* @__PURE__ */ jsx(MultiSelect, {
1194
+ data: sourceSelectOptions,
1195
+ value: filteredSources,
1196
+ onChange: setFilteredSources,
1197
+ comboboxProps: { withinPortal: false },
1198
+ clearable: true,
1199
+ searchable: true
1200
+ }),
1201
+ filtering: filteredSources.length > 0
1202
+ },
1203
+ {
1204
+ accessor: "target",
1205
+ title: targetTitle,
1206
+ width: COLUMN_WIDTHS.target,
1207
+ filter: () => /* @__PURE__ */ jsx(MultiSelect, {
1208
+ data: targetSelectOptions,
1209
+ value: filteredTargets,
1210
+ onChange: setFilteredTargets,
1211
+ comboboxProps: { withinPortal: false },
1212
+ clearable: true,
1213
+ searchable: true
1214
+ }),
1215
+ filtering: filteredTargets.length > 0
1216
+ },
1217
+ {
1218
+ accessor: "qualifiers",
1219
+ title: "Qualifiers",
1220
+ width: COLUMN_WIDTHS.qualifier,
1221
+ render: ({ qualifiers = [] }) => {
1222
+ const labels = qualifiers.map((qualifier) => qualifier || "No qualifier");
1223
+ return `${labels.slice(0, 2).join(", ")}${labels.length > 2 ? `, ... (${labels.length})` : ""}`;
1224
+ },
1225
+ filter: () => /* @__PURE__ */ jsx(MultiSelect, {
1226
+ data: qualifierOptions,
1227
+ value: filteredQualifiers,
1228
+ onChange: setFilteredQualifiers,
1229
+ comboboxProps: { withinPortal: false },
1230
+ clearable: true,
1231
+ searchable: true
1232
+ }),
1233
+ filtering: filteredQualifiers.length > 0
1234
+ },
1235
+ {
1236
+ accessor: "range",
1237
+ title: "Range",
1238
+ width: COLUMN_WIDTHS.range,
1239
+ render: ({ min_count, max_count }) => `${min_count} - ${max_count}`,
1240
+ hidden: hideRange
1241
+ },
1242
+ {
1243
+ accessor: "sum",
1244
+ title: "Total",
1245
+ width: COLUMN_WIDTHS.total,
1246
+ hidden: hideTotal
1247
+ },
1248
+ ...extraColumns
1249
+ ], [
1250
+ sourceTitle,
1251
+ targetTitle,
1252
+ sourceSelectOptions,
1253
+ targetSelectOptions,
1254
+ qualifierOptions,
1255
+ filteredSources,
1256
+ filteredTargets,
1257
+ filteredQualifiers,
1258
+ selectedRelations,
1259
+ hideRange,
1260
+ hideTotal,
1261
+ extraColumns
1262
+ ]);
1263
+ return /* @__PURE__ */ jsx(DataTable, {
1264
+ idAccessor: relationId,
1265
+ records: showNoRelations ? [] : data?.response,
1266
+ columns,
1267
+ withTableBorder: true,
1268
+ fetching: isFetching,
1269
+ totalRecords: showNoRelations ? 0 : data?.total_items ?? 0,
1270
+ page: currentPage,
1271
+ recordsPerPage: PAGE_SIZE,
1272
+ onPageChange: setCurrentPage,
1273
+ noRecordsText: "No relations found",
1274
+ height: 500,
1275
+ rowExpansion: {
1276
+ allowMultiple: false,
1277
+ expandable: ({ record }) => (record.qualifiers?.length ?? 0) > 1,
1278
+ expanded: {
1279
+ recordIds: selectedRelations,
1280
+ onRecordIdsChange: setSelectedRelations
1281
+ },
1282
+ content: ({ record }) => /* @__PURE__ */ jsx(SubRelationTable, {
1283
+ ocelId,
1284
+ relationType,
1285
+ direction,
1286
+ source: record.source,
1287
+ target: record.target,
1288
+ hideRange,
1289
+ hideTotal,
1290
+ ocelVersion,
1291
+ extraColumns: subTableExtraColumns
1292
+ })
1293
+ }
1294
+ });
1295
+ };
340
1296
  //#endregion
341
1297
  //#region src/hooks/useDownload.ts
342
1298
  const useDownloadFile = () => {
343
1299
  const { sessionId } = useSessionStore();
344
1300
  const downloadFile = async (path) => {
345
1301
  try {
346
- const response = await fetch(`${env.backend_url}${path}`, {
1302
+ const response = await fetch(path, {
347
1303
  method: "GET",
348
- headers: { [env.session_id]: sessionId ?? "" }
1304
+ headers: { [env$1.session_id]: sessionId ?? "" }
349
1305
  });
350
1306
  if (!response.ok) throw new Error("Failed to download file");
351
1307
  const blob = await response.blob();
@@ -357,249 +1313,27 @@ const useDownloadFile = () => {
357
1313
  };
358
1314
  return downloadFile;
359
1315
  };
360
-
361
- //#endregion
362
- //#region src/lib/dayjs.ts
363
- dayjs.extend(isBetween);
364
- var dayjs_default = dayjs;
365
- const formatDateTime = (dateString) => dayjs(dateString).format("YYYY-MM-DD HH:mm");
366
-
367
- //#endregion
368
- //#region src/components/EntityTable/EntityTable.tsx
369
- const ocelExtenisions = [
370
- ".sqlite",
371
- ".xmlocel",
372
- ".jsonocel"
373
- ];
374
- const EntityTable = () => {
375
- const { data: ocels = [] } = useGetOcels();
376
- const { data: resources = [] } = useResources();
377
- const invalidate = useInvalidate_default();
1316
+ const useDownloadFromServer = (getQueryKey) => () => {
378
1317
  const downloadFile = useDownloadFile();
379
- const { data: resourceMeta = {} } = useGetResourceMeta();
380
- const { mutate: deleteResource } = useDeleteResource({ mutation: { onSuccess: () => invalidate(["resources"]) } });
381
- const { mutate: deleteOcel } = useDeleteOcel({ mutation: { onSuccess: () => invalidate(["ocels"]) } });
382
- const { mutate: renameOcel } = useRenameOcel({ mutation: { onSuccess: () => invalidate(["ocels"]) } });
383
- const { mutate: renameResource } = useRenameResource({ mutation: { onSuccess: () => invalidate(["resources"]) } });
384
- const deleteEntity = useCallback((id, entityType) => {
385
- switch (entityType) {
386
- case "ocel":
387
- deleteOcel({ params: { ocel_id: id } });
388
- break;
389
- case "resource":
390
- deleteResource({ resourceId: id });
391
- break;
392
- }
393
- }, [deleteResource, deleteOcel]);
394
- const renameEntitiy = useCallback((id, entityType, newName) => {
395
- switch (entityType) {
396
- case "ocel":
397
- renameOcel({ params: {
398
- ocel_id: id,
399
- new_name: newName
400
- } });
401
- break;
402
- case "resource": renameResource({
403
- resourceId: id,
404
- params: { new_name: newName }
405
- });
406
- }
407
- }, [renameOcel, renameResource]);
408
- const [renamedEntitiy, setRenamedEntitiy] = useState(void 0);
409
- const [viewedResouce, setViewedResource] = useState();
410
- const entities = useMemo(() => {
411
- const ocelEntities = ocels.map(({ name, created_at, id, extensions }) => ({
412
- id,
413
- name,
414
- type: "ocel",
415
- entityTypes: extensions.map(({ label }) => label),
416
- createdAt: formatDateTime(created_at),
417
- downloadFormats: [
418
- ".xml",
419
- ".json",
420
- ".sqlite"
421
- ]
422
- }));
423
- const resourceEntities = resources.map(({ id, name, type, created_at }) => ({
424
- id,
425
- name,
426
- entityTypes: [resourceMeta[type]?.label ?? type],
427
- type: "resource",
428
- createdAt: formatDateTime(dayjs_default(created_at).toISOString())
429
- }));
430
- return [...ocelEntities, ...resourceEntities];
431
- }, [
432
- ocels,
433
- resources,
434
- resourceMeta
435
- ]);
436
- return /* @__PURE__ */ jsxs(Fragment, { children: [viewedResouce && /* @__PURE__ */ jsx(ResourceModal, {
437
- id: viewedResouce,
438
- onClose: () => setViewedResource(void 0)
439
- }), entities.length ? /* @__PURE__ */ jsx(DataTable, {
440
- withTableBorder: true,
441
- borderRadius: "md",
442
- idAccessor: "id",
443
- columns: [
444
- {
445
- accessor: "name",
446
- render: ({ id, type, name }) => /* @__PURE__ */ jsx(Fragment, { children: renamedEntitiy?.id === id ? /* @__PURE__ */ jsxs(Group, { children: [/* @__PURE__ */ jsx(TextInput, {
447
- variant: "unstyled",
448
- value: renamedEntitiy.value,
449
- style: { borderBottom: "1px solid #9ca3af" },
450
- onChange: (e) => {
451
- setRenamedEntitiy({
452
- id,
453
- value: e.target.value
454
- });
455
- }
456
- }), /* @__PURE__ */ jsxs(Group, {
457
- gap: "xs",
458
- children: [/* @__PURE__ */ jsx(ActionIcon, {
459
- color: "green",
460
- m: 0,
461
- onClick: () => {
462
- renameEntitiy(id, type, renamedEntitiy.value);
463
- setRenamedEntitiy(void 0);
464
- },
465
- children: /* @__PURE__ */ jsx(CheckIcon, { size: 16 })
466
- }), /* @__PURE__ */ jsx(ActionIcon, {
467
- color: "red",
468
- m: 0,
469
- onClick: () => {
470
- setRenamedEntitiy(void 0);
471
- },
472
- children: /* @__PURE__ */ jsx(XIcon, { size: 16 })
473
- })]
474
- })] }) : name })
475
- },
476
- { accessor: "createdAt" },
477
- {
478
- accessor: "entityTypes",
479
- title: "Type",
480
- render: ({ type, entityTypes }) => /* @__PURE__ */ jsx(Group, {
481
- gap: "xs",
482
- children: [...type === "ocel" ? ["OCEL"] : [], ...entityTypes].map((entityType) => /* @__PURE__ */ jsx(Badge, {
483
- color: generateColor(entityType),
484
- children: entityType
485
- }, entityType))
486
- })
487
- },
488
- {
489
- accessor: "",
490
- textAlign: "right",
491
- width: "0%",
492
- render: ({ type, id, name }) => /* @__PURE__ */ jsxs(Menu, {
493
- width: 200,
494
- position: "left-start",
495
- children: [/* @__PURE__ */ jsx(Menu.Target, { children: /* @__PURE__ */ jsx(ActionIcon, {
496
- variant: "subtle",
497
- onClick: (e) => e.stopPropagation(),
498
- children: /* @__PURE__ */ jsx(EllipsisVerticalIcon, { size: 20 })
499
- }) }), /* @__PURE__ */ jsxs(Menu.Dropdown, {
500
- onClick: (e) => e.stopPropagation(),
501
- children: [
502
- /* @__PURE__ */ jsx(Menu.Item, {
503
- leftSection: /* @__PURE__ */ jsx(PencilIcon, { size: 16 }),
504
- onClick: () => setRenamedEntitiy({
505
- id,
506
- value: name
507
- }),
508
- children: "Rename"
509
- }),
510
- type === "resource" && /* @__PURE__ */ jsx(MenuItem, {
511
- leftSection: /* @__PURE__ */ jsx(EyeIcon, { size: 16 }),
512
- onClick: () => setViewedResource(id),
513
- children: "Inspect"
514
- }),
515
- type === "ocel" ? /* @__PURE__ */ jsxs(Menu.Sub, {
516
- position: "right-start",
517
- children: [/* @__PURE__ */ jsx(Menu.Sub.Target, { children: /* @__PURE__ */ jsx(Menu.Sub.Item, {
518
- leftSection: /* @__PURE__ */ jsx(DownloadIcon, { size: 16 }),
519
- children: "Download"
520
- }) }), /* @__PURE__ */ jsx(Menu.Sub.Dropdown, { children: ocelExtenisions.map((extension) => /* @__PURE__ */ jsx(Menu.Item, {
521
- onClick: () => downloadFile(`/ocels/download?${new URLSearchParams({
522
- ocel_id: id,
523
- ext: extension
524
- }).toString()}`),
525
- children: extension
526
- }, extension)) })]
527
- }) : /* @__PURE__ */ jsx(Menu.Item, {
528
- onClick: () => downloadFile(`/resources/resource/${id}/download`),
529
- leftSection: /* @__PURE__ */ jsx(DownloadIcon, { size: 16 }),
530
- children: "Download"
531
- }),
532
- /* @__PURE__ */ jsx(Menu.Divider, {}),
533
- /* @__PURE__ */ jsx(Menu.Item, {
534
- leftSection: /* @__PURE__ */ jsx(TrashIcon, {
535
- size: 16,
536
- color: "red"
537
- }),
538
- color: "red",
539
- fw: "bold",
540
- onClick: () => deleteEntity(id, type),
541
- children: "Delete"
542
- })
543
- ]
544
- })]
545
- })
546
- }
547
- ],
548
- records: entities
549
- }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Title, {
550
- size: "h3",
551
- children: "Upload"
552
- }), /* @__PURE__ */ jsx(UploadSection_default, {})] })] });
553
- };
554
- var EntityTable_default = EntityTable;
555
-
556
- //#endregion
557
- //#region src/components/DefaultHomePage/DefaultHomePage.tsx
558
- const DefaultHomePage = ({ children }) => {
559
- return /* @__PURE__ */ jsx(Container, {
560
- h: "100%",
561
- children: /* @__PURE__ */ jsxs(Stack, {
562
- gap: "sm",
563
- h: "100%",
564
- children: [/* @__PURE__ */ jsx(EntityTable_default, {}), children]
565
- })
566
- });
1318
+ return { download: (...params) => {
1319
+ const result = getQueryKey(...params);
1320
+ const path = result[0];
1321
+ const searchParams = result[1];
1322
+ const queryString = qs.stringify(searchParams, {
1323
+ skipNulls: true,
1324
+ indices: false
1325
+ });
1326
+ downloadFile(queryString ? `${path}?${queryString}` : path);
1327
+ } };
567
1328
  };
568
- var DefaultHomePage_default = DefaultHomePage;
569
-
1329
+ const useDownloadOCEL = useDownloadFromServer(getDownloadOCELQueryKey);
1330
+ const useDownloadFlatOCEL = useDownloadFromServer(getDownloadFlatLogQueryKey);
1331
+ const useDownloadResource = useDownloadFromServer(getDownloadResourceQueryKey);
1332
+ const useDownloadResourceAsPnml = useDownloadFromServer(getDownloadResourceAsPnmlQueryKey);
570
1333
  //#endregion
571
- //#region src/components/ModulePage.tsx
572
- const createModulesPage = (config) => {
573
- const { modules = [], homePage: HomePage = DefaultHomePage_default } = config;
574
- const getStaticPaths = async () => {
575
- return {
576
- paths: [...modules.flatMap(({ name: moduleName, routes }) => routes.map(({ name: routeName }) => ({ params: { slug: [
577
- "modules",
578
- moduleName,
579
- routeName
580
- ] } }))), { params: { slug: [] } }],
581
- fallback: false
582
- };
583
- };
584
- const getStaticProps = async ({ params }) => {
585
- const slugs = params?.slug ?? [];
586
- const moduleConfig = modules.find(({ name }) => name === slugs[1]);
587
- const routeName = moduleConfig?.routes.find(({ name }) => name === slugs[2])?.name;
588
- return moduleConfig && routeName ? { props: { moduleProps: {
589
- moduleName: moduleConfig.name,
590
- routeName
591
- } } } : { props: {} };
592
- };
593
- const ModulePage = ({ moduleProps }) => {
594
- const RouteComponent = modules.find(({ name }) => name === moduleProps?.moduleName)?.routes.find(({ name }) => name === moduleProps?.routeName)?.component;
595
- return RouteComponent ? /* @__PURE__ */ jsx(RouteComponent, {}) : /* @__PURE__ */ jsx(HomePage, {});
596
- };
597
- return {
598
- getStaticPaths,
599
- getStaticProps,
600
- ModulePage
601
- };
602
- };
603
-
1334
+ //#region src/lib/config.ts
1335
+ const defineModule = (def) => def;
1336
+ const defineModuleRoute = (def) => def;
1337
+ const env = z.object({ NEXT_PUBLIC_PROJECT_PAGE: z.url().default("https://www.ocelescope.org/") }).transform(({ NEXT_PUBLIC_PROJECT_PAGE }) => ({ projectPage: NEXT_PUBLIC_PROJECT_PAGE })).parse(process.env);
604
1338
  //#endregion
605
- export { OcelescopeApp, OcelescopeDocument, createModulesPage };
1339
+ export { AttributesTable, BarList, FileDropzone, OcelSelect, OcelescopeApp, OcelescopeDocument, RelationTable, UploadModal, UploadSection, createModulesPage, defineModule, defineModuleRoute, env, formatAttributeValue, getModuleRoute, sseHandler, useCurrentOcel, useDownloadFile, useDownloadFlatOCEL, useDownloadOCEL, useDownloadResource, useDownloadResourceAsPnml, useInvalidate };