@ocelescope/plugin 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +553 -818
  2. package/package.json +14 -19
package/dist/index.js CHANGED
@@ -1,375 +1,124 @@
1
- import { BarList, FileDropzone, FullScreenUpload, OcelSelect, defineModule, defineModuleRoute, env, getModuleRoute, useInvalidate } from "@ocelescope/core";
2
- import { CheckIcon, Columns2Icon, DatabaseIcon, DownloadIcon, EllipsisVertical, PuzzleIcon, Rows2Icon, Trash2Icon, UploadIcon } from "lucide-react";
1
+ import { FileDropzone, FullScreenUpload, OcelSelect, defineModule, defineModuleRoute, env, getModuleRoute } from "@ocelescope/core";
2
+ import { ArrowRightIcon, ChevronRightIcon, EllipsisVerticalIcon, EyeClosedIcon, EyeIcon, PuzzleIcon, SearchIcon, Trash2Icon, UploadIcon } from "lucide-react";
3
3
  import { useRouter } from "next/router.js";
4
- import { ActionIcon, Anchor, Badge, Box, Breadcrumbs, Button, Card, Center, Container, Divider, Flex, Group, Input, Loader, LoadingOverlay, Menu, Modal, MultiSelect, ScrollArea, Select, SimpleGrid, Skeleton, Slider, Splitter, Stack, Switch, Text, ThemeIcon, Title, Tooltip, useComputedColorScheme } from "@mantine/core";
5
- import { useDeletePlugin, useDisableDiscoveryMethod, useDownloadPluginResults, useEnableDiscoveryMethod, useEventAttributes, useEventCounts, useEventIds, useGetComputedValues, useGetExtensionMeta, useGetPlugin, useGetPluginMethod, useGetResourceMeta, useListDiscoveryMethods, useObjectAttributes, useObjectCounts, useObjectIds, usePluginResult, usePlugins, useRunPlugin, useSavePluginResults, useUpload } from "@ocelescope/api-base";
6
- import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from "react";
7
- import { ResourceSelect, Visualization } from "@ocelescope/resources";
4
+ import { ActionIcon, Anchor, Badge, Box, Breadcrumbs, Button, Card, CloseButton, Divider, Group, LoadingOverlay, Menu, Modal, OverflowList, ScrollArea, SimpleGrid, Spoiler, Stack, Text, TextInput, ThemeIcon, Title, Tooltip, UnstyledButton } from "@mantine/core";
5
+ import { useDeletePlugin, useGetPlugin, useGetPluginMethod, usePlugins, useRunPlugin, useUpload } from "@ocelescope/api-base";
6
+ import { PluginDashboard, PluginForm } from "@ocelescope/plugin-components";
7
+ import { useCallback, useMemo, useRef, useState } from "react";
8
+ import { ResourceSelect } from "@ocelescope/resources";
8
9
  import { Controller, useForm, useWatch } from "react-hook-form";
9
- import validator from "@rjsf/validator-ajv8";
10
- import { getDefaultRegistry } from "@rjsf/core";
11
- import Editor from "@monaco-editor/react";
12
10
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
- import { useDebouncedValue } from "@mantine/hooks";
14
- import { keepPreviousData } from "@tanstack/react-query";
15
- import dynamic from "next/dynamic.js";
16
11
  import Link from "next/link.js";
12
+ import { useHover } from "@mantine/hooks";
17
13
  import { generateColor } from "@marko19907/string-to-color";
18
- //#region src/components/Form/Fields/CodeField.tsx
19
- const EDITOR_HEIGHT = 160;
20
- const OPTIONS = {
21
- minimap: { enabled: false },
22
- quickSuggestions: false,
23
- suggestOnTriggerCharacters: false,
24
- wordBasedSuggestions: "off",
25
- lineNumbers: "on",
26
- scrollBeyondLastLine: false,
27
- automaticLayout: true,
28
- scrollbar: { alwaysConsumeMouseWheel: false },
29
- wordWrap: "on",
30
- tabSize: 2,
31
- fontSize: 13,
32
- padding: {
33
- top: 8,
34
- bottom: 8
35
- }
36
- };
37
- const CodeField = memo(({ schema, required, formData, onChange, rawErrors, disabled, readonly, fieldPathId: { path } }) => {
38
- const meta = schema["x-ui-meta"];
39
- const colorScheme = useComputedColorScheme("light", { getInitialValueInEffect: true });
40
- const readOnly = disabled || readonly;
41
- const options = useMemo(() => ({
42
- ...OPTIONS,
43
- readOnly
44
- }), [readOnly]);
45
- return /* @__PURE__ */ jsx(Input.Wrapper, {
46
- label: schema.title,
47
- description: schema.description,
48
- required,
49
- error: rawErrors?.[0],
50
- children: /* @__PURE__ */ jsx(Box, {
51
- style: {
52
- border: "1px solid var(--mantine-color-default-border)",
53
- borderRadius: "var(--mantine-radius-default)",
54
- overflow: "hidden"
55
- },
56
- children: /* @__PURE__ */ jsx(Editor, {
57
- height: EDITOR_HEIGHT,
58
- language: meta?.language ?? "plaintext",
59
- value: formData ?? "",
60
- theme: colorScheme === "dark" ? "vs-dark" : "light",
61
- options,
62
- onChange: (value) => onChange(value ?? "", path),
63
- loading: /* @__PURE__ */ jsx(Skeleton, {
64
- height: EDITOR_HEIGHT,
65
- radius: 0
66
- })
67
- })
68
- })
69
- });
70
- });
71
- //#endregion
72
- //#region src/components/Form/PluginFormContext.tsx
73
- const PluginFormContext = createContext(void 0);
74
- const usePluginForm = () => {
75
- const context = useContext(PluginFormContext);
76
- if (!context) throw new Error("Custom plugin fields have to be rendered inside <PluginFormProvider>");
77
- return context;
78
- };
79
- const useOcelId = (ocelRef) => {
80
- const { control } = usePluginForm();
81
- return useWatch({
82
- control,
83
- name: `input_ocels.${ocelRef}`
84
- });
85
- };
86
- const PluginFormProvider = ({ pluginId, methodName, control, children }) => {
87
- const value = useMemo(() => ({
88
- pluginId,
89
- methodName,
90
- control
91
- }), [
92
- pluginId,
93
- methodName,
94
- control
95
- ]);
96
- return /* @__PURE__ */ jsx(PluginFormContext.Provider, {
97
- value,
98
- children
99
- });
100
- };
101
- //#endregion
102
- //#region src/components/Form/Fields/ComputedSelect.tsx
103
- const compact = (record) => Object.fromEntries(Object.entries(record ?? {}).filter(([, value]) => !!value));
104
- const useSelectOptions = (provider) => {
105
- const { pluginId, methodName, control } = usePluginForm();
106
- const formValues = useWatch({ control });
107
- const [debouncedBody] = useDebouncedValue(useMemo(() => JSON.stringify({
108
- input: formValues.input,
109
- input_ocels: compact(formValues.input_ocels),
110
- input_resources: compact(formValues.input_resources)
111
- }), [formValues]), 300);
112
- const { data } = useGetComputedValues(pluginId, methodName, provider, useMemo(() => JSON.parse(debouncedBody), [debouncedBody]), { query: {
113
- placeholderData: keepPreviousData,
114
- refetchOnWindowFocus: false,
115
- refetchOnReconnect: false
116
- } });
117
- return { options: data };
118
- };
119
- const ComputedSelect = memo(({ schema, required, formData, onChange, fieldPathId: { path } }) => {
120
- const meta = schema["x-ui-meta"];
121
- const isMulti = schema.type === "array";
122
- const SelectComponent = isMulti ? MultiSelect : Select;
123
- const { options } = useSelectOptions(meta.provider);
124
- return /* @__PURE__ */ jsx(SelectComponent, {
125
- label: schema.title,
126
- description: schema.description,
127
- required,
128
- value: formData ?? (isMulti ? [] : null),
129
- onChange: (value) => onChange(value, path),
130
- data: options
131
- });
132
- });
133
- //#endregion
134
- //#region src/components/Form/Fields/EnumMultiSelect.tsx
135
- const itemsOf = (schema, registry) => {
136
- const { items } = schema;
137
- if (!items || typeof items === "boolean" || Array.isArray(items)) return;
138
- return registry.schemaUtils.retrieveSchema(items);
139
- };
140
- const isStringEnumArray = (schema, registry) => {
141
- if (schema.type !== "array") return false;
142
- const enumValues = itemsOf(schema, registry)?.enum;
143
- return Array.isArray(enumValues) && enumValues.length > 0 && enumValues.every((value) => typeof value === "string");
144
- };
145
- const EnumMultiSelect = memo(({ schema, registry, required, formData, onChange, rawErrors, disabled, readonly, fieldPathId: { path } }) => {
146
- const options = useMemo(() => itemsOf(schema, registry)?.enum ?? [], [schema, registry]);
147
- return /* @__PURE__ */ jsx(MultiSelect, {
148
- label: schema.title,
149
- description: schema.description,
150
- required,
151
- error: rawErrors?.[0],
152
- data: options,
153
- value: Array.isArray(formData) ? formData : [],
154
- onChange: (value) => onChange(value, path),
155
- disabled: disabled || readonly,
156
- clearable: true,
157
- searchable: true
158
- });
159
- });
160
- //#endregion
161
- //#region src/components/Form/Fields/OCELField.tsx
162
- const AttributeSelect = (useAttributes) => ({ ocelId, isMulti, value, ...props }) => {
163
- const { data: attributes } = useAttributes(ocelId);
164
- const names = useMemo(() => [...new Set((attributes ?? []).map(({ name }) => name))], [attributes]);
165
- return /* @__PURE__ */ jsx(isMulti ? MultiSelect : Select, {
166
- value: value ?? (isMulti ? [] : null),
167
- ...props,
168
- data: names,
169
- disabled: !ocelId,
170
- clearable: true
171
- });
172
- };
173
- const TypeSelect = (useCounts) => ({ ocelId, isMulti, value, ...props }) => {
174
- const { data: counts } = useCounts(ocelId);
175
- const types = useMemo(() => Object.keys(counts ?? {}), [counts]);
176
- return /* @__PURE__ */ jsx(isMulti ? MultiSelect : Select, {
177
- ...props,
178
- value: value ?? (isMulti ? [] : null),
179
- data: types,
180
- clearable: true
181
- });
182
- };
183
- const IdSelect = (useIds) => ({ ocelId, isMulti, value, ...props }) => {
184
- const [search, setSearch] = useState();
185
- const [debouncedSearch] = useDebouncedValue(search, 300);
186
- const { data: ids } = useIds(ocelId, { search: debouncedSearch });
187
- return /* @__PURE__ */ jsx(isMulti ? MultiSelect : Select, {
188
- ...props,
189
- value: value ?? (isMulti ? [] : null),
190
- data: ids?.response,
191
- searchValue: search,
192
- onSearchChange: setSearch,
193
- searchable: true
194
- });
195
- };
196
- const OCEL_FIELDS = {
197
- event_type: TypeSelect(useEventCounts),
198
- object_type: TypeSelect(useObjectCounts),
199
- event_attribute: AttributeSelect(useEventAttributes),
200
- object_attribute: AttributeSelect(useObjectAttributes),
201
- event_id: IdSelect(useEventIds),
202
- object_id: IdSelect(useObjectIds)
203
- };
204
- const OCELField = memo(({ schema, required, formData, onChange, fieldPathId: { path } }) => {
205
- const ocelRef = schema["x-ui-meta"]?.["ocel_id"];
206
- const ocelFieldType = schema["x-ui-meta"]?.["field_type"];
207
- const ocelId = useOcelId(ocelRef);
208
- const isMulti = schema.type === "array";
209
- useEffect(() => {
210
- onChange(isMulti ? [] : void 0, path);
211
- }, [
212
- ocelId,
213
- isMulti,
214
- onChange,
215
- path
216
- ]);
217
- const Selector = OCEL_FIELDS[ocelFieldType];
218
- return Selector && /* @__PURE__ */ jsx(Selector, {
219
- ocelId,
220
- isMulti,
221
- value: formData,
222
- onChange: (value) => onChange(value, path),
223
- label: schema.title,
224
- description: schema.description,
225
- disabled: !ocelId,
226
- required
227
- }, ocelId);
228
- });
229
- //#endregion
230
- //#region src/components/Form/Fields/SliderField.tsx
231
- const decimalsOf = (step) => String(step).split(".")[1]?.length ?? 0;
232
- const SliderField = memo(({ schema, required, formData, onChange, rawErrors, disabled, readonly, fieldPathId: { path } }) => {
233
- const meta = schema["x-ui-meta"];
234
- const { min, max } = meta;
235
- const step = meta.step ?? (schema.type === "integer" ? 1 : (max - min) / 100);
236
- const decimals = decimalsOf(step);
237
- const marks = useMemo(() => meta.marks?.map((value) => ({
238
- value,
239
- label: value.toFixed(decimals)
240
- })), [meta.marks, decimals]);
241
- const fallback = typeof schema.default === "number" ? schema.default : min;
242
- const value = typeof formData === "number" ? formData : fallback;
243
- return /* @__PURE__ */ jsx(Input.Wrapper, {
244
- label: schema.title,
245
- description: schema.description,
246
- required,
247
- error: rawErrors?.[0],
248
- children: /* @__PURE__ */ jsx(Box, {
249
- px: marks ? "md" : void 0,
250
- pb: marks ? "xl" : void 0,
251
- children: /* @__PURE__ */ jsx(Slider, {
252
- mt: "xs",
253
- min,
254
- max,
255
- step,
256
- marks,
257
- label: (current) => current.toFixed(decimals),
258
- value,
259
- onChange: (current) => onChange(current, path),
260
- disabled: disabled || readonly
261
- })
262
- })
263
- });
264
- });
265
- //#endregion
266
- //#region src/components/Form/Fields/index.tsx
267
- const DefaultSchemaField = getDefaultRegistry().fields.SchemaField;
268
- const CUSTOM_FORM_MAP = {
269
- ocel: OCELField,
270
- computed_select: ComputedSelect,
271
- code: CodeField,
272
- slider: SliderField
273
- };
274
- const resolveField = ({ schema, registry }) => {
275
- const uiType = schema["x-ui-meta"]?.type;
276
- if (uiType) return CUSTOM_FORM_MAP[uiType];
277
- if (isStringEnumArray(schema, registry)) return EnumMultiSelect;
278
- };
279
- const CustomSchemaField = (props) => {
280
- const FormComponent = resolveField(props);
281
- return FormComponent ? /* @__PURE__ */ jsx(FormComponent, { ...props }) : /* @__PURE__ */ jsx(DefaultSchemaField, { ...props });
282
- };
283
- //#endregion
284
- //#region src/components/Form/MantineForm.tsx
285
- const Form = dynamic(() => import("@rjsf/mantine").then((m) => m.Form), { ssr: false });
286
- //#endregion
287
- //#region src/components/Form/PluginForm.tsx
288
- const FIELDS = { SchemaField: CustomSchemaField };
289
- const PluginForm = ({ schema, control, pluginId, methodName, onSubmit }) => {
290
- const formSchema = useMemo(() => ({
291
- ...schema,
292
- title: ""
293
- }), [schema]);
294
- return /* @__PURE__ */ jsx(PluginFormProvider, {
295
- control,
296
- pluginId,
297
- methodName,
298
- children: /* @__PURE__ */ jsx(Controller, {
299
- control,
300
- name: "input",
301
- render: ({ field }) => /* @__PURE__ */ jsx(Form, {
302
- schema: formSchema,
303
- formData: field.value,
304
- validator,
305
- fields: FIELDS,
306
- onChange: ({ formData }) => field.onChange(formData),
307
- onSubmit
308
- })
309
- })
310
- });
311
- };
312
- //#endregion
313
14
  //#region src/components/Form/index.tsx
314
- const PluginInput = ({ pluginId, method, onSuccess }) => {
15
+ const compact = (record) => Object.fromEntries(Object.entries(record ?? {}).filter(([, value]) => !!value));
16
+ const PluginInput = ({ pluginId, method, onSuccess, autoShowFirstOutput, setAutoShowFirstOutput }) => {
315
17
  const { mutate: runPlugin } = useRunPlugin({ mutation: { onSuccess } });
316
- const { control, handleSubmit } = useForm({ defaultValues: {
317
- input_ocels: Object.fromEntries(Object.keys(method.input_ocels ?? {}).map((name) => [name, void 0])),
318
- input_resources: Object.fromEntries(Object.keys(method.input_resources ?? {}).map((name) => [name, void 0])),
18
+ const defaultValue = {
19
+ input_resources: Object.fromEntries(method.inputs.map(({ name }) => [name, void 0])),
319
20
  formData: {}
320
- } });
321
- const onSubmit = useCallback(() => handleSubmit((data) => runPlugin({
322
- data,
323
- methodName: method.name,
324
- pluginId
325
- }))(), [
21
+ };
22
+ const { control, handleSubmit } = useForm({ defaultValues: defaultValue });
23
+ const ref = useRef(null);
24
+ const onSubmit = useCallback(() => {
25
+ if (!method.configuration_schema || ref.current?.validateForm()) handleSubmit(({ input_resources, input }) => runPlugin({
26
+ data: {
27
+ input_resources: compact(input_resources),
28
+ input
29
+ },
30
+ methodName: method.name,
31
+ pluginId
32
+ }))();
33
+ }, [
326
34
  handleSubmit,
327
35
  pluginId,
328
36
  method,
329
37
  runPlugin
330
38
  ]);
39
+ const inputResources = useWatch({
40
+ control,
41
+ name: "input_resources"
42
+ });
331
43
  return /* @__PURE__ */ jsxs(Stack, {
332
- gap: "md",
333
- children: [
334
- Object.entries(method.input_ocels ?? {}).map(([name, { label, description, extension }]) => /* @__PURE__ */ jsx(Controller, {
335
- control,
336
- name: `input_ocels.${name}`,
337
- rules: { required: "Please select a value" },
338
- render: ({ field, fieldState }) => /* @__PURE__ */ jsx(OcelSelect, {
339
- label,
340
- required: true,
341
- extension: extension ?? void 0,
342
- description,
343
- error: fieldState.error?.message,
344
- onChange: field.onChange,
345
- value: field.value
346
- })
347
- }, name)),
348
- Object.entries(method.input_resources ?? {}).map(([name, [resource_type, { label, description }]]) => /* @__PURE__ */ jsx(Controller, {
349
- control,
350
- name: `input_resources.${name}`,
351
- rules: { required: "Please select a value" },
352
- render: ({ field, fieldState }) => /* @__PURE__ */ jsx(ResourceSelect, {
353
- label,
354
- required: true,
355
- type: resource_type,
356
- description,
357
- onChange: field.onChange,
358
- error: fieldState.error?.message,
359
- value: field.value
360
- })
361
- }, name)),
362
- method.input_schema ? /* @__PURE__ */ jsx(PluginForm, {
363
- pluginId,
364
- methodName: method.name,
365
- schema: method.input_schema,
366
- control,
367
- onSubmit
368
- }) : /* @__PURE__ */ jsx(Button, {
44
+ gap: 0,
45
+ flex: 1,
46
+ mih: 0,
47
+ children: [/* @__PURE__ */ jsx(ScrollArea, {
48
+ flex: 1,
49
+ mih: 0,
50
+ type: "auto",
51
+ children: /* @__PURE__ */ jsxs(Stack, {
52
+ gap: "md",
53
+ px: "md",
54
+ pb: "md",
55
+ maw: 640,
56
+ mx: "auto",
57
+ w: "100%",
58
+ children: [method.inputs.map((io) => /* @__PURE__ */ jsx(Controller, {
59
+ control,
60
+ name: `input_resources.${io.name}`,
61
+ rules: !io.is_optional ? { required: "Please select a value" } : {},
62
+ render: ({ field, fieldState }) => io.type === "ocel" ? /* @__PURE__ */ jsx(OcelSelect, {
63
+ label: io.label,
64
+ clearable: io.is_optional,
65
+ required: !io.is_optional,
66
+ description: io.description,
67
+ searchable: true,
68
+ error: fieldState.error?.message,
69
+ onChange: field.onChange,
70
+ value: field.value
71
+ }) : /* @__PURE__ */ jsx(ResourceSelect, {
72
+ clearable: io.is_optional,
73
+ label: io.label,
74
+ required: !io.is_optional,
75
+ type: io.schema_id,
76
+ description: io.description,
77
+ onChange: field.onChange,
78
+ error: fieldState.error?.message,
79
+ value: field.value,
80
+ searchable: true
81
+ })
82
+ }, io.name)), method.configuration_schema && /* @__PURE__ */ jsx(Controller, {
83
+ control,
84
+ name: "input",
85
+ render: ({ field }) => /* @__PURE__ */ jsx(PluginForm, {
86
+ ref,
87
+ pluginId,
88
+ methodName: method.name,
89
+ schema: method.configuration_schema,
90
+ inputResources,
91
+ formData: field.value,
92
+ onChange: ({ formData }) => field.onChange(formData),
93
+ uiSchema: { "ui:submitButtonOptions": { norender: true } },
94
+ onSubmit
95
+ })
96
+ })]
97
+ })
98
+ }), /* @__PURE__ */ jsxs(Group, {
99
+ gap: "xs",
100
+ wrap: "nowrap",
101
+ p: "md",
102
+ maw: 640,
103
+ mx: "auto",
104
+ w: "100%",
105
+ children: [/* @__PURE__ */ jsx(Button, {
106
+ flex: 1,
369
107
  onClick: onSubmit,
370
108
  children: "Submit"
371
- })
372
- ]
109
+ }), /* @__PURE__ */ jsx(Tooltip, {
110
+ label: autoShowFirstOutput ? "Opening first output after run" : "Hide outputs after run",
111
+ withArrow: true,
112
+ children: /* @__PURE__ */ jsx(ActionIcon, {
113
+ onClick: () => setAutoShowFirstOutput(!autoShowFirstOutput),
114
+ size: "input-sm",
115
+ variant: autoShowFirstOutput ? "light" : "default",
116
+ "aria-label": "Open first output after run",
117
+ "aria-pressed": autoShowFirstOutput,
118
+ children: autoShowFirstOutput ? /* @__PURE__ */ jsx(EyeIcon, { size: 18 }) : /* @__PURE__ */ jsx(EyeClosedIcon, { size: 18 })
119
+ })
120
+ })]
121
+ })]
373
122
  });
374
123
  };
375
124
  //#endregion
@@ -379,408 +128,274 @@ const PluginBreadcrumbs = () => {
379
128
  const { pluginId, methodName } = query;
380
129
  const { data: plugin } = useGetPlugin(pluginId ?? "", { query: { enabled: !!pluginId } });
381
130
  const pluginMethod = useMemo(() => plugin?.methods.find(({ name }) => name === methodName), [plugin, methodName]);
382
- return /* @__PURE__ */ jsxs(Breadcrumbs, { children: [
383
- /* @__PURE__ */ jsx(Anchor, {
384
- component: Link,
385
- href: { query: {
386
- ...query,
387
- pluginId: void 0,
388
- methodName: void 0
389
- } },
390
- children: "Plugins"
391
- }),
392
- plugin && /* @__PURE__ */ jsx(Anchor, {
393
- component: Link,
394
- href: { query: {
395
- ...query,
396
- pluginId: plugin?.id,
397
- methodName: void 0
398
- } },
399
- children: plugin?.meta.label
400
- }),
401
- pluginMethod && /* @__PURE__ */ jsx(Anchor, {
402
- component: Link,
403
- href: { query: {
404
- ...query,
405
- pluginId: plugin?.id,
406
- methodName: pluginMethod.name
407
- } },
408
- children: pluginMethod.label ?? pluginMethod.name
409
- })
410
- ] });
411
- };
412
- //#endregion
413
- //#region src/components/ResultSection/ResultSection.tsx
414
- const ResourceView = ({ resource }) => {
415
- return /* @__PURE__ */ jsx(Visualization, { visualization: resource.visualization });
416
- };
417
- const OCELView = ({ ocel }) => {
418
- return /* @__PURE__ */ jsxs(Stack, {
419
- gap: "md",
420
- children: [/* @__PURE__ */ jsx(BarList, {
421
- data: ocel.activity_count,
422
- labelHeader: "Activity"
423
- }), /* @__PURE__ */ jsx(BarList, {
424
- data: ocel.object_count,
425
- labelHeader: "Object type"
426
- })]
427
- });
428
- };
429
- const entityTypeOf = (output) => output.type === "ocel" ? "ocel" : output.resource_type || "resource";
430
- const ResultLabel = ({ label, entityType, bold }) => /* @__PURE__ */ jsxs(Group, {
431
- gap: "xs",
432
- wrap: "nowrap",
433
- children: [/* @__PURE__ */ jsx(Text, {
434
- fw: bold ? 600 : void 0,
435
- truncate: true,
436
- children: label
437
- }), /* @__PURE__ */ jsx(Badge, {
438
- size: "sm",
439
- color: generateColor(entityType),
440
- children: entityType
441
- })]
442
- });
443
- const ResultSection = ({ pluginId, methodName, taskId }) => {
444
- const { data: pluginSummary } = usePluginResult(pluginId, methodName, taskId, { query: { refetchInterval: ({ state }) => {
445
- if (state.data == null) return 1e3;
446
- return false;
447
- } } });
448
- const [selected, setSelected] = useState([0]);
449
- const [orientation, setOrientation] = useState("vertical");
450
- const { mutate: saveResults, isPending: isSaving, isSuccess: isSaved, reset: resetSave } = useSavePluginResults();
451
- const { mutate: downloadResults, isPending: isDownloading } = useDownloadPluginResults({ request: { responseType: "blob" } });
452
- const options = useMemo(() => (pluginSummary ?? []).map((output) => {
453
- return {
454
- value: output.result_index,
455
- label: entityTypeOf(output),
456
- entityType: output.type
457
- };
458
- }), [pluginSummary]);
459
- if (!pluginSummary) return /* @__PURE__ */ jsx(Center, {
460
- h: "100%",
461
- children: /* @__PURE__ */ jsxs(Stack, {
462
- align: "center",
463
- gap: "sm",
464
- children: [/* @__PURE__ */ jsx(Loader, {}), /* @__PURE__ */ jsx(Text, {
465
- c: "dimmed",
466
- children: "Running…"
467
- })]
468
- })
469
- });
470
- if (pluginSummary.length === 0) return /* @__PURE__ */ jsx(Center, {
471
- h: "100%",
472
- children: /* @__PURE__ */ jsx(Text, {
473
- c: "dimmed",
474
- children: "This run produced no results."
475
- })
476
- });
477
- const handleSelectionChange = (value) => {
478
- setSelected(value);
479
- resetSave();
480
- };
481
- const handleDownload = () => {
482
- downloadResults({
483
- pluginId,
484
- methodName,
485
- taskId,
486
- data: { indices: selected }
487
- }, { onSuccess: (data) => {
488
- const url = URL.createObjectURL(data);
489
- const anchor = document.createElement("a");
490
- anchor.href = url;
491
- anchor.download = `${methodName}_results.zip`;
492
- anchor.click();
493
- URL.revokeObjectURL(url);
494
- } });
495
- };
496
- const handleSaveToSession = () => {
497
- saveResults({
498
- pluginId,
499
- methodName,
500
- taskId,
501
- data: { indices: selected }
502
- });
503
- };
504
- return /* @__PURE__ */ jsxs(Stack, {
505
- gap: 0,
506
- h: "100%",
507
- children: [/* @__PURE__ */ jsxs(Group, {
508
- px: "sm",
509
- py: "xs",
510
- wrap: "nowrap",
511
- style: { borderBottom: "1px solid var(--mantine-color-default-border)" },
512
- children: [options.length > 1 ? /* @__PURE__ */ jsx(MultiSelect, {
513
- flex: 1,
514
- data: options,
515
- onChange: handleSelectionChange,
516
- value: selected,
517
- placeholder: "Select results to display",
518
- searchable: true,
519
- clearable: true,
520
- comboboxProps: { withinPortal: true },
521
- renderOption: ({ option }) => {
522
- const output = pluginSummary[Number(option.value)];
523
- return /* @__PURE__ */ jsx(ResultLabel, {
524
- label: option.label,
525
- entityType: output ? entityTypeOf(output) : option.label
526
- });
527
- }
528
- }) : options[0] && /* @__PURE__ */ jsx(ResultLabel, {
529
- label: options[0].label,
530
- entityType: options[0].entityType,
531
- bold: true
532
- }), /* @__PURE__ */ jsxs(Group, {
533
- gap: "xs",
534
- ml: "auto",
535
- wrap: "nowrap",
536
- children: [
537
- selected.length > 1 && /* @__PURE__ */ jsx(Tooltip, {
538
- label: orientation === "vertical" ? "Show side by side" : "Stack vertically",
539
- children: /* @__PURE__ */ jsx(ActionIcon, {
540
- variant: "default",
541
- size: "lg",
542
- onClick: () => setOrientation((prev) => prev === "vertical" ? "horizontal" : "vertical"),
543
- children: orientation === "vertical" ? /* @__PURE__ */ jsx(Columns2Icon, { size: 16 }) : /* @__PURE__ */ jsx(Rows2Icon, { size: 16 })
544
- })
545
- }),
546
- /* @__PURE__ */ jsx(Button, {
547
- variant: "default",
548
- leftSection: /* @__PURE__ */ jsx(DownloadIcon, { size: 16 }),
549
- onClick: handleDownload,
550
- loading: isDownloading,
551
- disabled: selected.length === 0,
552
- children: "Download"
553
- }),
554
- /* @__PURE__ */ jsx(Button, {
555
- variant: "light",
556
- color: isSaved ? "green" : void 0,
557
- leftSection: isSaved ? /* @__PURE__ */ jsx(CheckIcon, { size: 16 }) : /* @__PURE__ */ jsx(DatabaseIcon, { size: 16 }),
558
- onClick: handleSaveToSession,
559
- loading: isSaving,
560
- disabled: selected.length === 0,
561
- children: isSaved ? "Saved" : "Save to session"
562
- })
563
- ]
564
- })]
565
- }), selected.length === 0 ? /* @__PURE__ */ jsx(Center, {
566
- style: { flex: 1 },
567
- children: /* @__PURE__ */ jsx(Text, {
568
- c: "dimmed",
569
- children: "Select one or more results to display."
570
- })
571
- }) : /* @__PURE__ */ jsx(Splitter, {
572
- orientation,
573
- lineSize: 4,
574
- handleColor: "var(--mantine-color-default-border)",
575
- style: {
576
- flex: 1,
577
- minHeight: 0
131
+ return /* @__PURE__ */ jsxs(Breadcrumbs, {
132
+ styles: {
133
+ root: {
134
+ display: "grid",
135
+ gridAutoFlow: "column",
136
+ gridAutoColumns: "minmax(0, max-content)"
578
137
  },
579
- children: pluginSummary?.filter(({ result_index }) => selected.includes(result_index)).map((output, index) => /* @__PURE__ */ jsx(Splitter.Pane, {
580
- defaultSize: 100 / selected.length,
581
- min: "15%",
582
- children: /* @__PURE__ */ jsx(Box, {
583
- h: "100%",
584
- p: "xs",
585
- pos: "relative",
586
- children: output.type === "ocel" ? /* @__PURE__ */ jsx(OCELView, { ocel: output }) : /* @__PURE__ */ jsx(ResourceView, { resource: output })
587
- })
588
- }, index))
589
- }, `${orientation}:${selected.join(",")}`)]
138
+ breadcrumb: {
139
+ minWidth: 0,
140
+ lineHeight: "var(--mantine-line-height)"
141
+ }
142
+ },
143
+ children: [
144
+ /* @__PURE__ */ jsx(Anchor, {
145
+ component: Link,
146
+ truncate: true,
147
+ title: "Plugins",
148
+ href: { query: {
149
+ ...query,
150
+ pluginId: void 0,
151
+ methodName: void 0
152
+ } },
153
+ children: "Plugins"
154
+ }),
155
+ plugin && /* @__PURE__ */ jsx(Anchor, {
156
+ component: Link,
157
+ truncate: true,
158
+ title: plugin.label,
159
+ href: { query: {
160
+ ...query,
161
+ pluginId: plugin?.id,
162
+ methodName: void 0
163
+ } },
164
+ children: plugin?.label
165
+ }),
166
+ pluginMethod && /* @__PURE__ */ jsx(Anchor, {
167
+ component: Link,
168
+ truncate: true,
169
+ title: pluginMethod.label ?? pluginMethod.name,
170
+ href: { query: {
171
+ ...query,
172
+ pluginId: plugin?.id,
173
+ methodName: pluginMethod.name
174
+ } },
175
+ children: pluginMethod.label ?? pluginMethod.name
176
+ })
177
+ ]
590
178
  });
591
179
  };
592
180
  //#endregion
593
181
  //#region src/pages/MethodPage.tsx
594
- const COLLAPSED_SIZES = [0, 100];
595
- const SPLIT_SIZES = [75, 25];
596
182
  const MethodPage = ({ pluginId, methodName }) => {
597
183
  const { data: pluginMethod } = useGetPluginMethod(pluginId, methodName);
598
184
  const [currentTask, setCurrentTask] = useState();
599
- const [sizes, setSizes] = useState(COLLAPSED_SIZES);
185
+ const [autoShowFirstOutput, setAutoShowFirstOutput] = useState(true);
600
186
  if (!pluginMethod) return /* @__PURE__ */ jsx(LoadingOverlay, { visible: true });
601
- const handleSuccess = (taskId) => {
602
- setCurrentTask(taskId);
603
- setSizes((prev) => prev[0] === 0 ? SPLIT_SIZES : prev);
604
- };
605
- return /* @__PURE__ */ jsx(Flex, {
606
- direction: "column",
607
- h: "100%",
608
- children: /* @__PURE__ */ jsxs(Splitter, {
609
- sizes,
610
- onSizeChange: setSizes,
611
- lineSize: 4,
612
- handleColor: "var(--mantine-color-default-border)",
613
- px: "xs",
614
- style: {
615
- flex: 1,
616
- minHeight: 0
617
- },
618
- children: [/* @__PURE__ */ jsx(Splitter.Pane, {
619
- defaultSize: 0,
620
- min: "20%",
621
- collapsible: true,
622
- children: /* @__PURE__ */ jsx(Box, {
623
- h: "100%",
624
- pos: "relative",
625
- style: { overflow: "hidden" },
626
- children: currentTask ? /* @__PURE__ */ jsx(ResultSection, {
627
- pluginId,
628
- methodName,
629
- taskId: currentTask
630
- }) : /* @__PURE__ */ jsx(Center, {
631
- h: "100%",
632
- p: "md",
187
+ return /* @__PURE__ */ jsx(PluginDashboard, {
188
+ taskId: currentTask,
189
+ autoShowFirstOutput,
190
+ children: /* @__PURE__ */ jsxs(Stack, {
191
+ h: "100%",
192
+ gap: 0,
193
+ children: [/* @__PURE__ */ jsxs(Stack, {
194
+ gap: "sm",
195
+ p: "md",
196
+ maw: 640,
197
+ mx: "auto",
198
+ w: "100%",
199
+ children: [
200
+ /* @__PURE__ */ jsx(Group, {
201
+ justify: "center",
202
+ children: /* @__PURE__ */ jsx(PluginBreadcrumbs, {})
203
+ }),
204
+ /* @__PURE__ */ jsx(Title, {
205
+ ta: "center",
206
+ order: 2,
207
+ children: pluginMethod.label ?? methodName
208
+ }),
209
+ pluginMethod.description && /* @__PURE__ */ jsx(Spoiler, {
210
+ maxHeight: 120,
211
+ showLabel: "Show more",
212
+ hideLabel: "Hide",
633
213
  children: /* @__PURE__ */ jsx(Text, {
634
214
  c: "dimmed",
635
215
  ta: "center",
636
- children: "Run the method to see its results here."
216
+ children: pluginMethod.description
637
217
  })
638
218
  })
639
- })
640
- }), /* @__PURE__ */ jsx(Splitter.Pane, {
641
- defaultSize: 100,
642
- min: "20%",
643
- collapsible: true,
644
- children: /* @__PURE__ */ jsx(ScrollArea, {
645
- h: "100%",
646
- type: "auto",
647
- children: /* @__PURE__ */ jsxs(Stack, {
648
- gap: "sm",
649
- p: "md",
650
- maw: 640,
651
- mx: "auto",
652
- w: "100%",
653
- children: [
654
- /* @__PURE__ */ jsx(Group, {
655
- justify: "center",
656
- children: /* @__PURE__ */ jsx(PluginBreadcrumbs, {})
657
- }),
658
- /* @__PURE__ */ jsx(Title, {
659
- ta: "center",
660
- children: pluginMethod.label ?? methodName
661
- }),
662
- pluginMethod.description && /* @__PURE__ */ jsx(Text, {
663
- c: "dimmed",
664
- ta: "center",
665
- children: pluginMethod.description
666
- }),
667
- /* @__PURE__ */ jsx(PluginInput, {
668
- onSuccess: handleSuccess,
669
- pluginId,
670
- method: pluginMethod
671
- })
672
- ]
673
- })
674
- })
219
+ ]
220
+ }), /* @__PURE__ */ jsx(PluginInput, {
221
+ onSuccess: setCurrentTask,
222
+ pluginId,
223
+ method: pluginMethod,
224
+ autoShowFirstOutput,
225
+ setAutoShowFirstOutput
675
226
  })]
676
227
  })
677
228
  });
678
229
  };
679
230
  //#endregion
680
- //#region src/components/PluginCard/GenericCard.tsx
681
- const GenericCard = ({ menuItems, title, description, link, tags, version }) => {
231
+ //#region src/components/LinkCard/LinkCard.tsx
232
+ const INTERACTIVE_LAYER = {
233
+ position: "relative",
234
+ zIndex: 1
235
+ };
236
+ const LinkCard = ({ href, height, children }) => {
237
+ const { hovered, ref } = useHover();
682
238
  return /* @__PURE__ */ jsxs(Card, {
683
- shadow: "sm",
684
- padding: "sm",
685
- radius: "md",
239
+ ref,
686
240
  withBorder: true,
687
- h: "100%",
241
+ shadow: hovered ? "md" : "xs",
242
+ radius: "md",
243
+ padding: "md",
244
+ h: height,
245
+ style: {
246
+ transition: "box-shadow 150ms ease, border-color 150ms ease",
247
+ ...hovered && { borderColor: "var(--mantine-primary-color-filled)" }
248
+ },
249
+ children: [/* @__PURE__ */ jsx(Box, {
250
+ component: Link,
251
+ href,
252
+ "aria-hidden": true,
253
+ tabIndex: -1,
254
+ pos: "absolute",
255
+ inset: 0
256
+ }), children]
257
+ });
258
+ };
259
+ const LinkCardTitle = ({ href, label }) => /* @__PURE__ */ jsx(Anchor, {
260
+ component: Link,
261
+ href,
262
+ fw: 600,
263
+ title: `Open ${label}`,
264
+ style: INTERACTIVE_LAYER,
265
+ children: /* @__PURE__ */ jsxs(Group, {
266
+ gap: 4,
267
+ wrap: "nowrap",
268
+ children: [/* @__PURE__ */ jsx(Text, {
269
+ inherit: true,
270
+ truncate: "end",
271
+ children: label
272
+ }), /* @__PURE__ */ jsx(ChevronRightIcon, {
273
+ size: 16,
274
+ style: { flexShrink: 0 }
275
+ })]
276
+ })
277
+ });
278
+ const LinkCardDescription = ({ description }) => /* @__PURE__ */ jsx(Text, {
279
+ size: "sm",
280
+ c: "dimmed",
281
+ mt: "sm",
282
+ lineClamp: 2,
283
+ h: "calc(2em * var(--mantine-line-height-sm))",
284
+ children: description || "No description provided."
285
+ });
286
+ const ResourceTypeBadge = ({ resourceType }) => /* @__PURE__ */ jsx(Badge, {
287
+ size: "sm",
288
+ color: generateColor(resourceType),
289
+ style: { flexShrink: 0 },
290
+ children: resourceType
291
+ });
292
+ const ResourceTypeOverflowList = ({ resourceTypes, maxRows = 1 }) => /* @__PURE__ */ jsx(OverflowList, {
293
+ gap: 6,
294
+ maxRows,
295
+ data: resourceTypes,
296
+ renderItem: (resourceType) => /* @__PURE__ */ jsx(ResourceTypeBadge, { resourceType }, resourceType),
297
+ renderOverflow: (hiddenTypes) => /* @__PURE__ */ jsx(Tooltip, {
298
+ withArrow: true,
299
+ label: /* @__PURE__ */ jsx(Stack, {
300
+ gap: 4,
301
+ children: hiddenTypes.map((resourceType) => /* @__PURE__ */ jsx(ResourceTypeBadge, { resourceType }, resourceType))
302
+ }),
303
+ children: /* @__PURE__ */ jsxs(Badge, {
304
+ size: "sm",
305
+ variant: "default",
306
+ style: {
307
+ ...INTERACTIVE_LAYER,
308
+ flexShrink: 0
309
+ },
310
+ children: ["+", hiddenTypes.length]
311
+ })
312
+ })
313
+ });
314
+ const getResourceTypes = (ios) => Array.from(new Set(ios.map((io) => io.type === "ocel" ? "OCEL" : io.resource_label)));
315
+ const IOColumn = ({ label, resourceTypes }) => /* @__PURE__ */ jsxs(Stack, {
316
+ gap: 6,
317
+ flex: 1,
318
+ miw: 0,
319
+ children: [/* @__PURE__ */ jsx(Text, {
320
+ size: "xs",
321
+ fw: 600,
322
+ tt: "uppercase",
323
+ c: "dimmed",
324
+ children: label
325
+ }), resourceTypes.length > 0 ? /* @__PURE__ */ jsx(ResourceTypeOverflowList, {
326
+ resourceTypes,
327
+ maxRows: 2
328
+ }) : /* @__PURE__ */ jsx(Text, {
329
+ size: "xs",
330
+ c: "dimmed",
331
+ children: "None"
332
+ })]
333
+ });
334
+ const MethodCard = ({ pluginId, method }) => {
335
+ const href = getModuleRoute({
336
+ moduleName: "plugins",
337
+ routeName: "plugins",
338
+ query: {
339
+ pluginId,
340
+ methodName: method.name
341
+ }
342
+ });
343
+ return /* @__PURE__ */ jsxs(LinkCard, {
344
+ href,
345
+ height: 240,
688
346
  children: [
347
+ /* @__PURE__ */ jsx(LinkCardTitle, {
348
+ href,
349
+ label: method.label ?? method.name
350
+ }),
351
+ /* @__PURE__ */ jsx(LinkCardDescription, { description: method.description }),
689
352
  /* @__PURE__ */ jsxs(Group, {
690
- justify: "space-between",
353
+ mt: "auto",
691
354
  align: "start",
692
355
  wrap: "nowrap",
693
- children: [/* @__PURE__ */ jsxs(Stack, {
694
- gap: "xs",
695
- mb: "xs",
696
- children: [tags && /* @__PURE__ */ jsx(Group, {
697
- gap: "xs",
698
- children: tags.map((tag) => /* @__PURE__ */ jsx(Badge, {
699
- size: "sm",
700
- color: generateColor(tag),
701
- children: tag
702
- }, tag))
703
- }), /* @__PURE__ */ jsxs(Text, {
704
- fw: 500,
705
- children: [title, version && /* @__PURE__ */ jsx(Text, {
706
- ml: "xs",
707
- c: "dimmed",
708
- component: "span",
709
- children: `v${version}`
710
- })]
711
- })]
712
- }), menuItems && /* @__PURE__ */ jsxs(Menu, {
713
- width: 200,
714
- position: "left",
715
- children: [/* @__PURE__ */ jsx(Menu.Target, { children: /* @__PURE__ */ jsx(ActionIcon, {
716
- variant: "transparent",
717
- size: "sm",
718
- children: /* @__PURE__ */ jsx(EllipsisVertical, {})
719
- }) }), /* @__PURE__ */ jsx(Menu.Dropdown, { children: menuItems })]
720
- })]
721
- }),
722
- /* @__PURE__ */ jsx(Text, {
723
- size: "sm",
724
- mb: "xs",
725
- c: "dimmed",
726
- children: description
727
- }),
728
- link && /* @__PURE__ */ jsx(Button, {
729
- mt: "auto",
730
- component: Link,
731
- variant: "outline",
732
- ...link
356
+ gap: "xs",
357
+ children: [
358
+ /* @__PURE__ */ jsx(IOColumn, {
359
+ label: "Input",
360
+ resourceTypes: getResourceTypes(method.inputs)
361
+ }),
362
+ /* @__PURE__ */ jsx(ArrowRightIcon, {
363
+ size: 16,
364
+ style: {
365
+ flexShrink: 0,
366
+ marginTop: 20,
367
+ opacity: .5
368
+ }
369
+ }),
370
+ /* @__PURE__ */ jsx(IOColumn, {
371
+ label: "Output",
372
+ resourceTypes: getResourceTypes(method.outputs)
373
+ })
374
+ ]
733
375
  })
734
376
  ]
735
377
  });
736
378
  };
737
379
  //#endregion
738
380
  //#region src/pages/PluginPage.tsx
739
- const MethodCard = ({ method }) => {
740
- const { query } = useRouter();
741
- const { data: resourceMeta = {} } = useGetResourceMeta();
742
- const { data: extensionMeta = {} } = useGetExtensionMeta();
743
- const tags = useMemo(() => {
744
- const extensions = Object.values(method.input_ocels ?? {}).map(({ extension }) => extension ? extensionMeta[extension]?.label : void 0);
745
- const inputResources = Object.values(method.input_resources ?? {}).map(([resourceName]) => resourceMeta[resourceName]?.label ?? resourceName);
746
- const resultNames = (method.results ?? []).filter((result) => result.type === "resource").map((result) => resourceMeta[result.resource_type]?.label ?? result.resource_type);
747
- return Array.from(/* @__PURE__ */ new Set([
748
- ...inputResources,
749
- ...resultNames,
750
- ...extensions
751
- ])).filter((tag) => !!tag);
752
- }, [
753
- resourceMeta,
754
- extensionMeta,
755
- method
756
- ]);
757
- return /* @__PURE__ */ jsx(GenericCard, {
758
- title: method.label ?? method.name,
759
- description: method.description ?? "",
760
- tags,
761
- link: {
762
- href: { query: {
763
- ...query,
764
- methodName: method.name
765
- } },
766
- children: "Run Method"
767
- }
768
- });
769
- };
381
+ const matchesSearch = (method, search) => [
382
+ method.name,
383
+ method.label,
384
+ method.description,
385
+ ...[...method.inputs, ...method.outputs].map((io) => io.type === "ocel" ? "OCEL" : io.resource_label)
386
+ ].some((value) => value?.toLowerCase().includes(search));
770
387
  const PluginPage = ({ pluginId }) => {
771
388
  const { data: plugin } = useGetPlugin(pluginId);
772
- const { data: allMethods = [] } = useListDiscoveryMethods();
773
- const invalidate = useInvalidate();
774
- const onSuccess = () => invalidate(["discoveryMethods"]);
775
- const { mutate: disableMethod } = useDisableDiscoveryMethod({ mutation: { onSuccess } });
776
- const { mutate: enableMethod } = useEnableDiscoveryMethod({ mutation: { onSuccess } });
777
- const discoveryMethods = useMemo(() => allMethods.flatMap((meta) => meta.variants.filter((v) => v.pluginId === pluginId).map((v) => ({
778
- name: meta.name,
779
- variant: v
780
- }))), [allMethods, pluginId]);
781
- return /* @__PURE__ */ jsx(Container, {
782
- fluid: true,
783
- children: /* @__PURE__ */ jsxs(Stack, { children: [
389
+ const [search, setSearch] = useState("");
390
+ const filteredMethods = useMemo(() => {
391
+ const normalizedSearch = search.trim().toLowerCase();
392
+ return (plugin?.methods ?? []).filter((method) => matchesSearch(method, normalizedSearch));
393
+ }, [plugin, search]);
394
+ return /* @__PURE__ */ jsxs(Stack, {
395
+ gap: 0,
396
+ p: "md",
397
+ h: "100%",
398
+ children: [
784
399
  /* @__PURE__ */ jsxs(Stack, {
785
400
  gap: 0,
786
401
  align: "center",
@@ -788,61 +403,59 @@ const PluginPage = ({ pluginId }) => {
788
403
  /* @__PURE__ */ jsx(PluginBreadcrumbs, {}),
789
404
  /* @__PURE__ */ jsxs(Title, {
790
405
  mt: "xs",
791
- children: [" ", plugin?.meta.label]
406
+ children: [" ", plugin?.label]
792
407
  }),
793
408
  /* @__PURE__ */ jsx(Text, {
794
409
  c: "dimmed",
795
- children: plugin?.meta.description
410
+ children: plugin?.description
796
411
  })
797
412
  ]
798
413
  }),
799
- /* @__PURE__ */ jsx(SimpleGrid, {
800
- cols: {
801
- base: 1,
802
- sm: 2,
803
- lg: 4
804
- },
805
- spacing: {
806
- base: 10,
807
- sm: "xl"
808
- },
809
- verticalSpacing: {
810
- base: "md",
811
- sm: "xl"
812
- },
813
- children: plugin?.methods.map((method) => /* @__PURE__ */ jsx(MethodCard, {
814
- method,
815
- pluginId: plugin.id
816
- }, method.name))
414
+ /* @__PURE__ */ jsx(TextInput, {
415
+ mt: "md",
416
+ placeholder: "Search methods by name, description or resource type",
417
+ leftSection: /* @__PURE__ */ jsx(SearchIcon, { size: 16 }),
418
+ rightSection: search && /* @__PURE__ */ jsx(CloseButton, {
419
+ size: "sm",
420
+ "aria-label": "Clear search",
421
+ onClick: () => setSearch("")
422
+ }),
423
+ value: search,
424
+ onChange: (event) => setSearch(event.currentTarget.value)
817
425
  }),
818
- discoveryMethods.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Divider, {}), /* @__PURE__ */ jsxs(Stack, {
819
- gap: "xs",
820
- children: [/* @__PURE__ */ jsx(Title, {
821
- order: 4,
822
- children: "Discovery Methods"
823
- }), discoveryMethods.map(({ name, variant }) => /* @__PURE__ */ jsxs(Group, {
824
- justify: "space-between",
825
- children: [/* @__PURE__ */ jsxs(Stack, {
826
- gap: 0,
827
- children: [/* @__PURE__ */ jsx(Text, {
828
- fw: 500,
829
- children: name
830
- }), variant.description && /* @__PURE__ */ jsx(Text, {
831
- size: "sm",
832
- c: "dimmed",
833
- children: variant.description
834
- })]
835
- }), /* @__PURE__ */ jsx(Switch, {
836
- label: "Active in Discovery",
837
- checked: variant.enabled ?? true,
838
- onChange: (e) => {
839
- if (e.currentTarget.checked) enableMethod({ methodId: variant.methodId });
840
- else disableMethod({ methodId: variant.methodId });
841
- }
842
- })]
843
- }, variant.methodId))]
844
- })] })
845
- ] })
426
+ plugin && filteredMethods.length === 0 ? /* @__PURE__ */ jsx(Text, {
427
+ c: "dimmed",
428
+ ta: "center",
429
+ mt: "xl",
430
+ children: search.trim() ? `No methods match "${search.trim()}".` : "This plugin has no methods."
431
+ }) : /* @__PURE__ */ jsx(ScrollArea, {
432
+ flex: 1,
433
+ mih: 0,
434
+ mt: "sm",
435
+ type: "auto",
436
+ offsetScrollbars: true,
437
+ children: /* @__PURE__ */ jsx(SimpleGrid, {
438
+ p: "xs",
439
+ cols: {
440
+ base: 1,
441
+ sm: 2,
442
+ lg: 3
443
+ },
444
+ spacing: {
445
+ base: 10,
446
+ sm: "xl"
447
+ },
448
+ verticalSpacing: {
449
+ base: "md",
450
+ sm: "xl"
451
+ },
452
+ children: plugin && filteredMethods.map((method) => /* @__PURE__ */ jsx(MethodCard, {
453
+ method,
454
+ pluginId: plugin.id
455
+ }, method.name))
456
+ })
457
+ })
458
+ ]
846
459
  });
847
460
  };
848
461
  //#endregion
@@ -855,32 +468,140 @@ const PluginUploadSection = ({ onSuccess }) => {
855
468
  accept: ["application/x-zip-compressed", "application/zip"]
856
469
  });
857
470
  };
858
- //#endregion
859
- //#region src/components/PluginCard/PluginCard.tsx
471
+ const MAX_METHOD_LINKS = 3;
860
472
  const PluginCard = ({ plugin }) => {
861
- const { description, label, version } = plugin.meta;
473
+ const { id, description, label, version, methods } = plugin;
862
474
  const { mutate: deletePlugin } = useDeletePlugin();
863
- return /* @__PURE__ */ jsx(GenericCard, {
864
- title: label,
865
- description: description ?? "",
866
- version,
867
- menuItems: /* @__PURE__ */ jsx(Menu.Item, {
868
- leftSection: /* @__PURE__ */ jsx(Trash2Icon, {}),
869
- color: "red",
870
- fw: "bold",
871
- onClick: () => {
872
- deletePlugin({ pluginId: plugin.id });
873
- },
874
- children: "Delete"
875
- }),
876
- link: {
877
- href: getModuleRoute({
878
- routeName: "plugins",
879
- moduleName: "plugins",
880
- query: { pluginId: plugin.id }
475
+ const resourceTypes = Array.from(new Set(methods.flatMap((method) => [...method.inputs, ...method.outputs].flatMap((io) => io.type === "resource" ? [io.resource_label] : []))));
476
+ const pluginHref = getModuleRoute({
477
+ moduleName: "plugins",
478
+ routeName: "plugins",
479
+ query: { pluginId: id }
480
+ });
481
+ const visibleMethods = methods.slice(0, MAX_METHOD_LINKS);
482
+ const hiddenCount = methods.length - visibleMethods.length;
483
+ return /* @__PURE__ */ jsxs(LinkCard, {
484
+ href: pluginHref,
485
+ height: 330,
486
+ children: [
487
+ /* @__PURE__ */ jsxs(Group, {
488
+ justify: "space-between",
489
+ align: "start",
490
+ wrap: "nowrap",
491
+ gap: "sm",
492
+ children: [/* @__PURE__ */ jsxs(Stack, {
493
+ gap: 0,
494
+ miw: 0,
495
+ children: [/* @__PURE__ */ jsx(LinkCardTitle, {
496
+ href: pluginHref,
497
+ label
498
+ }), /* @__PURE__ */ jsxs(Text, {
499
+ size: "xs",
500
+ c: "dimmed",
501
+ children: ["v", version]
502
+ })]
503
+ }), /* @__PURE__ */ jsxs(Menu, {
504
+ width: 160,
505
+ position: "bottom-end",
506
+ children: [/* @__PURE__ */ jsx(Menu.Target, { children: /* @__PURE__ */ jsx(ActionIcon, {
507
+ variant: "subtle",
508
+ color: "gray",
509
+ size: "sm",
510
+ style: INTERACTIVE_LAYER,
511
+ children: /* @__PURE__ */ jsx(EllipsisVerticalIcon, { size: 16 })
512
+ }) }), /* @__PURE__ */ jsx(Menu.Dropdown, { children: /* @__PURE__ */ jsx(Menu.Item, {
513
+ leftSection: /* @__PURE__ */ jsx(Trash2Icon, { size: 14 }),
514
+ color: "red",
515
+ onClick: () => deletePlugin({ pluginId: id }),
516
+ children: "Delete"
517
+ }) })]
518
+ })]
881
519
  }),
882
- children: "View Plugin"
883
- }
520
+ /* @__PURE__ */ jsx(LinkCardDescription, { description }),
521
+ /* @__PURE__ */ jsx(Divider, {
522
+ my: "sm",
523
+ labelPosition: "left",
524
+ label: /* @__PURE__ */ jsxs(Group, {
525
+ gap: 6,
526
+ children: [/* @__PURE__ */ jsx(Text, {
527
+ size: "xs",
528
+ fw: 600,
529
+ tt: "uppercase",
530
+ children: "Methods"
531
+ }), /* @__PURE__ */ jsx(Badge, {
532
+ size: "xs",
533
+ variant: "light",
534
+ color: "gray",
535
+ circle: true,
536
+ children: methods.length
537
+ })]
538
+ })
539
+ }),
540
+ /* @__PURE__ */ jsxs(Stack, {
541
+ gap: 4,
542
+ flex: 1,
543
+ children: [visibleMethods.length === 0 ? /* @__PURE__ */ jsx(Text, {
544
+ size: "sm",
545
+ c: "dimmed",
546
+ children: "This plugin has no methods."
547
+ }) : visibleMethods.map((method) => /* @__PURE__ */ jsx(UnstyledButton, {
548
+ component: Link,
549
+ href: getModuleRoute({
550
+ moduleName: "plugins",
551
+ routeName: "plugins",
552
+ query: {
553
+ pluginId: id,
554
+ methodName: method.name
555
+ }
556
+ }),
557
+ px: "xs",
558
+ py: 6,
559
+ style: {
560
+ ...INTERACTIVE_LAYER,
561
+ borderRadius: "var(--mantine-radius-sm)"
562
+ },
563
+ bg: "var(--mantine-color-default-hover)",
564
+ children: /* @__PURE__ */ jsxs(Group, {
565
+ justify: "space-between",
566
+ wrap: "nowrap",
567
+ gap: "xs",
568
+ children: [/* @__PURE__ */ jsx(Text, {
569
+ size: "sm",
570
+ truncate: "end",
571
+ title: method.label,
572
+ children: method.label ?? method.name
573
+ }), /* @__PURE__ */ jsx(ChevronRightIcon, {
574
+ size: 14,
575
+ style: {
576
+ flexShrink: 0,
577
+ opacity: .6
578
+ }
579
+ })]
580
+ })
581
+ }, method.name)), hiddenCount > 0 && /* @__PURE__ */ jsxs(Anchor, {
582
+ component: Link,
583
+ href: pluginHref,
584
+ size: "xs",
585
+ mt: 2,
586
+ style: INTERACTIVE_LAYER,
587
+ children: [
588
+ "+",
589
+ hiddenCount,
590
+ " more ",
591
+ hiddenCount === 1 ? "method" : "methods"
592
+ ]
593
+ })]
594
+ }),
595
+ resourceTypes.length > 0 && /* @__PURE__ */ jsxs(Stack, {
596
+ gap: 6,
597
+ mt: "sm",
598
+ children: [/* @__PURE__ */ jsx(Text, {
599
+ size: "xs",
600
+ c: "dimmed",
601
+ children: "Works with"
602
+ }), /* @__PURE__ */ jsx(ResourceTypeOverflowList, { resourceTypes })]
603
+ })
604
+ ]
884
605
  });
885
606
  };
886
607
  const UploadPluginCard = () => {
@@ -902,26 +623,37 @@ const UploadPluginCard = () => {
902
623
  children: /* @__PURE__ */ jsx(PluginUploadSection, { onSuccess: () => setIsModalOpen(false) })
903
624
  }),
904
625
  /* @__PURE__ */ jsx(Card, {
905
- shadow: "sm",
906
- padding: "lg",
626
+ component: "button",
907
627
  radius: "md",
908
- withBorder: true,
909
- h: "100%",
628
+ h: 330,
910
629
  onClick: () => setIsModalOpen(true),
911
- style: { cursor: "pointer" },
630
+ style: {
631
+ cursor: "pointer",
632
+ border: "2px dashed var(--mantine-color-default-border)",
633
+ background: "transparent"
634
+ },
912
635
  children: /* @__PURE__ */ jsxs(Stack, {
913
636
  align: "center",
914
637
  justify: "center",
915
638
  h: "100%",
916
- children: [/* @__PURE__ */ jsx(ThemeIcon, {
917
- size: "xl",
918
- variant: "transparent",
919
- children: /* @__PURE__ */ jsx(UploadIcon, { size: 50 })
920
- }), /* @__PURE__ */ jsx(Text, {
921
- fw: "bold",
922
- c: "blue.6",
923
- children: "Upload Plugin"
924
- })]
639
+ gap: "xs",
640
+ children: [
641
+ /* @__PURE__ */ jsx(ThemeIcon, {
642
+ size: 56,
643
+ radius: "xl",
644
+ variant: "light",
645
+ children: /* @__PURE__ */ jsx(UploadIcon, { size: 26 })
646
+ }),
647
+ /* @__PURE__ */ jsx(Text, {
648
+ fw: 600,
649
+ children: "Upload plugin"
650
+ }),
651
+ /* @__PURE__ */ jsx(Text, {
652
+ size: "xs",
653
+ c: "dimmed",
654
+ children: ".zip archive — or drop it anywhere on this page"
655
+ })
656
+ ]
925
657
  })
926
658
  })
927
659
  ] });
@@ -931,9 +663,11 @@ const UploadPluginCard = () => {
931
663
  const PluginsOverview = () => {
932
664
  const { data: plugins } = usePlugins();
933
665
  return /* @__PURE__ */ jsxs(Stack, {
934
- gap: "md",
935
- children: [/* @__PURE__ */ jsxs("div", {
936
- style: { maxWidth: 720 },
666
+ gap: "xs",
667
+ p: "md",
668
+ h: "100%",
669
+ children: [/* @__PURE__ */ jsxs(Stack, {
670
+ gap: 0,
937
671
  children: [/* @__PURE__ */ jsx(Title, {
938
672
  order: 2,
939
673
  children: "Plugin Overview"
@@ -1004,6 +738,11 @@ const PluginRoute = () => {
1004
738
  }
1005
739
  return /* @__PURE__ */ jsx(PluginsOverview, {});
1006
740
  };
741
+ var pages_default = defineModuleRoute({
742
+ name: "plugins",
743
+ label: "Plugins",
744
+ component: PluginRoute
745
+ });
1007
746
  //#endregion
1008
747
  //#region src/index.ts
1009
748
  var src_default = defineModule({
@@ -1011,11 +750,7 @@ var src_default = defineModule({
1011
750
  description: "Run process mining plugins inside ocelescope",
1012
751
  label: "Plugins",
1013
752
  authors: [{ name: "Öztürk, Görkem-Emre" }],
1014
- routes: [defineModuleRoute({
1015
- name: "plugins",
1016
- label: "Plugins",
1017
- component: PluginRoute
1018
- })],
753
+ routes: [pages_default],
1019
754
  icon: PuzzleIcon
1020
755
  });
1021
756
  //#endregion