@ocelescope/plugin 0.8.0 → 0.9.1

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 +554 -280
  2. package/package.json +7 -6
package/dist/index.js CHANGED
@@ -1,91 +1,123 @@
1
1
  import { FileDropzone, FullScreenUpload, OcelSelect, defineModule, defineModuleRoute, env, getModuleRoute } from "@ocelescope/core";
2
- import { EllipsisVertical, PuzzleIcon, Trash2Icon, UploadIcon } from "lucide-react";
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, Breadcrumbs, Button, Card, Container, Group, LoadingOverlay, Menu, Modal, ScrollArea, SimpleGrid, Splitter, Stack, Text, ThemeIcon, Title } from "@mantine/core";
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
5
  import { useDeletePlugin, useGetPlugin, useGetPluginMethod, usePlugins, useRunPlugin, useUpload } from "@ocelescope/api-base";
6
- import { PluginForm, ResultSection } from "@ocelescope/plugin-components";
7
- import { useCallback, useMemo, useState } from "react";
6
+ import { PluginDashboard, PluginForm } from "@ocelescope/plugin-components";
7
+ import { useCallback, useMemo, useRef, useState } from "react";
8
8
  import { ResourceSelect } from "@ocelescope/resources";
9
9
  import { Controller, useForm, useWatch } from "react-hook-form";
10
10
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
11
  import Link from "next/link.js";
12
+ import { useHover } from "@mantine/hooks";
12
13
  import { generateColor } from "@marko19907/string-to-color";
13
14
  //#region src/components/Form/index.tsx
14
15
  const compact = (record) => Object.fromEntries(Object.entries(record ?? {}).filter(([, value]) => !!value));
15
- const ConfigurationSection = ({ control, schema, pluginId, methodName, onSubmit }) => {
16
- const inputResources = useWatch({
17
- control,
18
- name: "input_resources"
19
- });
20
- return /* @__PURE__ */ jsx(Controller, {
21
- control,
22
- name: "input",
23
- render: ({ field }) => /* @__PURE__ */ jsx(PluginForm, {
24
- pluginId,
25
- methodName,
26
- schema,
27
- inputResources,
28
- formData: field.value,
29
- onChange: ({ formData }) => field.onChange(formData),
30
- onSubmit
31
- })
32
- });
33
- };
34
- const PluginInput = ({ pluginId, method, onSuccess }) => {
16
+ const PluginInput = ({ pluginId, method, onSuccess, autoShowFirstOutput, setAutoShowFirstOutput }) => {
35
17
  const { mutate: runPlugin } = useRunPlugin({ mutation: { onSuccess } });
36
18
  const defaultValue = {
37
19
  input_resources: Object.fromEntries(method.inputs.map(({ name }) => [name, void 0])),
38
20
  formData: {}
39
21
  };
40
22
  const { control, handleSubmit } = useForm({ defaultValues: defaultValue });
41
- const onSubmit = useCallback(() => handleSubmit(({ input_resources, input }) => runPlugin({
42
- data: {
43
- input_resources: compact(input_resources),
44
- input
45
- },
46
- methodName: method.name,
47
- pluginId
48
- }))(), [
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
+ }, [
49
34
  handleSubmit,
50
35
  pluginId,
51
36
  method,
52
37
  runPlugin
53
38
  ]);
39
+ const inputResources = useWatch({
40
+ control,
41
+ name: "input_resources"
42
+ });
54
43
  return /* @__PURE__ */ jsxs(Stack, {
55
- gap: "md",
56
- children: [method.inputs.map((io) => /* @__PURE__ */ jsx(Controller, {
57
- control,
58
- name: `input_resources.${io.name}`,
59
- rules: !io.is_optional ? { required: "Please select a value" } : {},
60
- render: ({ field, fieldState }) => io.type === "ocel" ? /* @__PURE__ */ jsx(OcelSelect, {
61
- label: io.label,
62
- clearable: io.is_optional,
63
- required: !io.is_optional,
64
- description: io.description,
65
- searchable: true,
66
- error: fieldState.error?.message,
67
- onChange: field.onChange,
68
- value: field.value
69
- }) : /* @__PURE__ */ jsx(ResourceSelect, {
70
- clearable: io.is_optional,
71
- label: io.label,
72
- required: !io.is_optional,
73
- type: io.schema_id,
74
- description: io.description,
75
- onChange: field.onChange,
76
- error: fieldState.error?.message,
77
- value: field.value,
78
- searchable: true
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
+ })]
79
97
  })
80
- }, io.name)), method.configuration_schema ? /* @__PURE__ */ jsx(ConfigurationSection, {
81
- control,
82
- pluginId,
83
- methodName: method.name,
84
- schema: method.configuration_schema,
85
- onSubmit
86
- }) : /* @__PURE__ */ jsx(Button, {
87
- onClick: onSubmit,
88
- children: "Submit"
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,
107
+ onClick: onSubmit,
108
+ children: "Submit"
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
+ })]
89
121
  })]
90
122
  });
91
123
  };
@@ -96,212 +128,334 @@ const PluginBreadcrumbs = () => {
96
128
  const { pluginId, methodName } = query;
97
129
  const { data: plugin } = useGetPlugin(pluginId ?? "", { query: { enabled: !!pluginId } });
98
130
  const pluginMethod = useMemo(() => plugin?.methods.find(({ name }) => name === methodName), [plugin, methodName]);
99
- return /* @__PURE__ */ jsxs(Breadcrumbs, { children: [
100
- /* @__PURE__ */ jsx(Anchor, {
101
- component: Link,
102
- href: { query: {
103
- ...query,
104
- pluginId: void 0,
105
- methodName: void 0
106
- } },
107
- children: "Plugins"
108
- }),
109
- plugin && /* @__PURE__ */ jsx(Anchor, {
110
- component: Link,
111
- href: { query: {
112
- ...query,
113
- pluginId: plugin?.id,
114
- methodName: void 0
115
- } },
116
- children: plugin?.label
117
- }),
118
- pluginMethod && /* @__PURE__ */ jsx(Anchor, {
119
- component: Link,
120
- href: { query: {
121
- ...query,
122
- pluginId: plugin?.id,
123
- methodName: pluginMethod.name
124
- } },
125
- children: pluginMethod.label ?? pluginMethod.name
126
- })
127
- ] });
131
+ return /* @__PURE__ */ jsxs(Breadcrumbs, {
132
+ styles: {
133
+ root: {
134
+ display: "grid",
135
+ gridAutoFlow: "column",
136
+ gridAutoColumns: "minmax(0, max-content)"
137
+ },
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
+ ]
178
+ });
128
179
  };
129
180
  //#endregion
130
181
  //#region src/pages/MethodPage.tsx
131
- const COLLAPSED_SIZES = [0, 100];
132
- const SPLIT_SIZES = [75, 25];
133
182
  const MethodPage = ({ pluginId, methodName }) => {
134
183
  const { data: pluginMethod } = useGetPluginMethod(pluginId, methodName);
135
184
  const [currentTask, setCurrentTask] = useState();
136
- const [sizes, setSizes] = useState(COLLAPSED_SIZES);
185
+ const [autoShowFirstOutput, setAutoShowFirstOutput] = useState(true);
137
186
  if (!pluginMethod) return /* @__PURE__ */ jsx(LoadingOverlay, { visible: true });
138
- const handleSuccess = (taskId) => {
139
- setCurrentTask(taskId);
140
- setSizes((prev) => prev[0] === 0 ? SPLIT_SIZES : prev);
141
- };
142
- return /* @__PURE__ */ jsxs(Splitter, {
143
- sizes,
144
- onSizeChange: setSizes,
145
- lineSize: 4,
146
- handleColor: "var(--mantine-color-default-border)",
147
- h: "100%",
148
- children: [/* @__PURE__ */ jsx(Splitter.Pane, {
149
- defaultSize: 0,
150
- min: "20%",
151
- collapsible: true,
152
- children: /* @__PURE__ */ jsx(ResultSection, { taskId: currentTask })
153
- }), /* @__PURE__ */ jsx(Splitter.Pane, {
154
- defaultSize: 100,
155
- min: "20%",
156
- collapsible: true,
157
- children: /* @__PURE__ */ jsx(ScrollArea, {
158
- h: "100%",
159
- type: "auto",
160
- children: /* @__PURE__ */ jsxs(Stack, {
161
- gap: "sm",
162
- p: "md",
163
- maw: 640,
164
- mx: "auto",
165
- w: "100%",
166
- children: [
167
- /* @__PURE__ */ jsx(Group, {
168
- justify: "center",
169
- children: /* @__PURE__ */ jsx(PluginBreadcrumbs, {})
170
- }),
171
- /* @__PURE__ */ jsx(Title, {
172
- ta: "center",
173
- children: pluginMethod.label ?? methodName
174
- }),
175
- pluginMethod.description && /* @__PURE__ */ jsx(Text, {
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",
213
+ children: /* @__PURE__ */ jsx(Text, {
176
214
  c: "dimmed",
177
215
  ta: "center",
178
216
  children: pluginMethod.description
179
- }),
180
- /* @__PURE__ */ jsx(PluginInput, {
181
- onSuccess: handleSuccess,
182
- pluginId,
183
- method: pluginMethod
184
217
  })
185
- ]
186
- })
187
- })
188
- })]
218
+ })
219
+ ]
220
+ }), /* @__PURE__ */ jsx(PluginInput, {
221
+ onSuccess: setCurrentTask,
222
+ pluginId,
223
+ method: pluginMethod,
224
+ autoShowFirstOutput,
225
+ setAutoShowFirstOutput
226
+ })]
227
+ })
189
228
  });
190
229
  };
191
230
  //#endregion
192
- //#region src/components/PluginCard/GenericCard.tsx
193
- 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();
194
238
  return /* @__PURE__ */ jsxs(Card, {
195
- shadow: "sm",
196
- padding: "sm",
197
- radius: "md",
239
+ ref,
198
240
  withBorder: true,
199
- 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,
200
346
  children: [
347
+ /* @__PURE__ */ jsx(LinkCardTitle, {
348
+ href,
349
+ label: method.label ?? method.name
350
+ }),
351
+ /* @__PURE__ */ jsx(LinkCardDescription, { description: method.description }),
201
352
  /* @__PURE__ */ jsxs(Group, {
202
- justify: "space-between",
353
+ mt: "auto",
203
354
  align: "start",
204
355
  wrap: "nowrap",
205
- children: [/* @__PURE__ */ jsxs(Stack, {
206
- gap: "xs",
207
- mb: "xs",
208
- children: [tags && /* @__PURE__ */ jsx(Group, {
209
- gap: "xs",
210
- children: tags.map((tag) => /* @__PURE__ */ jsx(Badge, {
211
- size: "sm",
212
- color: generateColor(tag),
213
- children: tag
214
- }, tag))
215
- }), /* @__PURE__ */ jsxs(Text, {
216
- fw: 500,
217
- children: [title, version && /* @__PURE__ */ jsx(Text, {
218
- ml: "xs",
219
- c: "dimmed",
220
- component: "span",
221
- children: `v${version}`
222
- })]
223
- })]
224
- }), menuItems && /* @__PURE__ */ jsxs(Menu, {
225
- width: 200,
226
- position: "left",
227
- children: [/* @__PURE__ */ jsx(Menu.Target, { children: /* @__PURE__ */ jsx(ActionIcon, {
228
- variant: "transparent",
229
- size: "sm",
230
- children: /* @__PURE__ */ jsx(EllipsisVertical, {})
231
- }) }), /* @__PURE__ */ jsx(Menu.Dropdown, { children: menuItems })]
232
- })]
233
- }),
234
- /* @__PURE__ */ jsx(Text, {
235
- size: "sm",
236
- mb: "xs",
237
- c: "dimmed",
238
- children: description
239
- }),
240
- link && /* @__PURE__ */ jsx(Button, {
241
- mt: "auto",
242
- component: Link,
243
- variant: "outline",
244
- ...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
+ ]
245
375
  })
246
376
  ]
247
377
  });
248
378
  };
249
379
  //#endregion
250
380
  //#region src/pages/PluginPage.tsx
251
- const MethodCard = ({ method }) => {
252
- const { query } = useRouter();
253
- const tags = Array.from(new Set([...method.inputs, ...method.outputs].map((io) => io.type === "ocel" ? "OCEL" : io.resource_label)));
254
- return /* @__PURE__ */ jsx(GenericCard, {
255
- title: method.label ?? method.name,
256
- description: method.description ?? "",
257
- link: {
258
- href: { query: {
259
- ...query,
260
- methodName: method.name
261
- } },
262
- children: "Run Method"
263
- },
264
- tags
265
- });
266
- };
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));
267
387
  const PluginPage = ({ pluginId }) => {
268
388
  const { data: plugin } = useGetPlugin(pluginId);
269
- return /* @__PURE__ */ jsxs(Container, {
270
- fluid: true,
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,
271
396
  p: "md",
272
- children: [/* @__PURE__ */ jsxs(Stack, {
273
- gap: 0,
274
- align: "center",
275
- children: [
276
- /* @__PURE__ */ jsx(PluginBreadcrumbs, {}),
277
- /* @__PURE__ */ jsxs(Title, {
278
- mt: "xs",
279
- children: [" ", plugin?.label]
397
+ h: "100%",
398
+ children: [
399
+ /* @__PURE__ */ jsxs(Stack, {
400
+ gap: 0,
401
+ align: "center",
402
+ children: [
403
+ /* @__PURE__ */ jsx(PluginBreadcrumbs, {}),
404
+ /* @__PURE__ */ jsxs(Title, {
405
+ mt: "xs",
406
+ children: [" ", plugin?.label]
407
+ }),
408
+ /* @__PURE__ */ jsx(Text, {
409
+ c: "dimmed",
410
+ children: plugin?.description
411
+ })
412
+ ]
413
+ }),
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("")
280
422
  }),
281
- /* @__PURE__ */ jsx(Text, {
282
- c: "dimmed",
283
- children: plugin?.description
423
+ value: search,
424
+ onChange: (event) => setSearch(event.currentTarget.value)
425
+ }),
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))
284
456
  })
285
- ]
286
- }), /* @__PURE__ */ jsx(SimpleGrid, {
287
- cols: {
288
- base: 1,
289
- sm: 2,
290
- lg: 4
291
- },
292
- spacing: {
293
- base: 10,
294
- sm: "xl"
295
- },
296
- verticalSpacing: {
297
- base: "md",
298
- sm: "xl"
299
- },
300
- children: plugin?.methods.map((method) => /* @__PURE__ */ jsx(MethodCard, {
301
- method,
302
- pluginId: plugin.id
303
- }, method.name))
304
- })]
457
+ })
458
+ ]
305
459
  });
306
460
  };
307
461
  //#endregion
@@ -314,32 +468,140 @@ const PluginUploadSection = ({ onSuccess }) => {
314
468
  accept: ["application/x-zip-compressed", "application/zip"]
315
469
  });
316
470
  };
317
- //#endregion
318
- //#region src/components/PluginCard/PluginCard.tsx
471
+ const MAX_METHOD_LINKS = 3;
319
472
  const PluginCard = ({ plugin }) => {
320
- const { description, label, version } = plugin;
473
+ const { id, description, label, version, methods } = plugin;
321
474
  const { mutate: deletePlugin } = useDeletePlugin();
322
- return /* @__PURE__ */ jsx(GenericCard, {
323
- title: label,
324
- description: description ?? "",
325
- version,
326
- menuItems: /* @__PURE__ */ jsx(Menu.Item, {
327
- leftSection: /* @__PURE__ */ jsx(Trash2Icon, {}),
328
- color: "red",
329
- fw: "bold",
330
- onClick: () => {
331
- deletePlugin({ pluginId: plugin.id });
332
- },
333
- children: "Delete"
334
- }),
335
- link: {
336
- href: getModuleRoute({
337
- routeName: "plugins",
338
- moduleName: "plugins",
339
- 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
+ })]
340
519
  }),
341
- children: "View Plugin"
342
- }
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
+ ]
343
605
  });
344
606
  };
345
607
  const UploadPluginCard = () => {
@@ -361,26 +623,37 @@ const UploadPluginCard = () => {
361
623
  children: /* @__PURE__ */ jsx(PluginUploadSection, { onSuccess: () => setIsModalOpen(false) })
362
624
  }),
363
625
  /* @__PURE__ */ jsx(Card, {
364
- shadow: "sm",
365
- padding: "lg",
626
+ component: "button",
366
627
  radius: "md",
367
- withBorder: true,
368
- h: "100%",
628
+ h: 330,
369
629
  onClick: () => setIsModalOpen(true),
370
- style: { cursor: "pointer" },
630
+ style: {
631
+ cursor: "pointer",
632
+ border: "2px dashed var(--mantine-color-default-border)",
633
+ background: "transparent"
634
+ },
371
635
  children: /* @__PURE__ */ jsxs(Stack, {
372
636
  align: "center",
373
637
  justify: "center",
374
638
  h: "100%",
375
- children: [/* @__PURE__ */ jsx(ThemeIcon, {
376
- size: "xl",
377
- variant: "transparent",
378
- children: /* @__PURE__ */ jsx(UploadIcon, { size: 50 })
379
- }), /* @__PURE__ */ jsx(Text, {
380
- fw: "bold",
381
- c: "blue.6",
382
- children: "Upload Plugin"
383
- })]
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
+ ]
384
657
  })
385
658
  })
386
659
  ] });
@@ -390,10 +663,11 @@ const UploadPluginCard = () => {
390
663
  const PluginsOverview = () => {
391
664
  const { data: plugins } = usePlugins();
392
665
  return /* @__PURE__ */ jsxs(Stack, {
393
- gap: "md",
666
+ gap: "xs",
394
667
  p: "md",
395
- children: [/* @__PURE__ */ jsxs("div", {
396
- style: { maxWidth: 720 },
668
+ h: "100%",
669
+ children: [/* @__PURE__ */ jsxs(Stack, {
670
+ gap: 0,
397
671
  children: [/* @__PURE__ */ jsx(Title, {
398
672
  order: 2,
399
673
  children: "Plugin Overview"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ocelescope/plugin",
3
3
  "type": "module",
4
- "version": "0.8.0",
4
+ "version": "0.9.1",
5
5
  "description": "A module for inspections of OCELs",
6
6
  "author": "Görkem-Emre Öztürk <goerkem.oeztuerk@rwth-aachen.de>",
7
7
  "license": "MIT",
@@ -31,14 +31,15 @@
31
31
  "@mantine/core": "^9.5.1",
32
32
  "@mantine/hooks": "^9.5.1",
33
33
  "@marko19907/string-to-color": "^1.0.2",
34
- "@ocelescope/api-base": "^0.8.0",
35
- "@ocelescope/core": "^0.8.0",
36
- "@ocelescope/plugin-components": "^0.8.0",
37
- "@ocelescope/resources": "^0.8.0",
34
+ "@ocelescope/api-base": "^0.9.1",
35
+ "@ocelescope/core": "^0.9.1",
36
+ "@ocelescope/plugin-components": "^0.9.1",
37
+ "@ocelescope/resources": "^0.9.1",
38
38
  "embla-carousel": "^8.6.0",
39
+ "@rjsf/core": "^6.8.0",
39
40
  "embla-carousel-react": "^8.6.0",
40
41
  "lucide-react": "^1.0.0",
41
- "next": "16.3.2",
42
+ "next": "16.3.6",
42
43
  "react": "^19.3.0",
43
44
  "react-dom": "^19.3.0",
44
45
  "react-hook-form": "^7.71.2"