@ocelescope/resources 0.11.0 → 0.13.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.d.ts CHANGED
@@ -1,19 +1,31 @@
1
- import { Select } from "@mantine/core";
2
- import { GetResourceResponse } from "@ocelescope/api-base";
3
- import { ComponentProps } from "react";
1
+ import { MultiSelectProps, SelectProps } from "@mantine/core";
2
+ import { resourceVisualization } from "@ocelescope/api-base";
3
+ import { ReactNode } from "react";
4
+ type Controlled<P extends SelectProps | MultiSelectProps> = Omit<P, "defaultValue"> & Required<Pick<P, "value" | "onChange">>;
5
+ type ActionSelectProps = {
6
+ actions?: ReactNode;
7
+ } & ((Controlled<SelectProps> & {
8
+ multiple?: false;
9
+ }) | (Controlled<MultiSelectProps> & {
10
+ multiple: true;
11
+ }));
12
+ export declare const ActionSelect: React.FC<ActionSelectProps>;
4
13
  export declare const ResourceModal: React.FC<{
5
14
  id: string;
6
15
  onClose: () => void;
7
16
  }>;
8
- export declare const ResourceSelect: React.FC<Omit<ComponentProps<typeof Select<string>>, "data"> & {
17
+ type ResourceSelectProps = ActionSelectProps & {
9
18
  type?: string;
10
- }>;
19
+ withUpload?: boolean;
20
+ withView?: boolean;
21
+ };
22
+ export declare const ResourceSelect: React.FC<ResourceSelectProps>;
11
23
  type RequireType<T> = T extends {
12
24
  type?: infer L;
13
25
  } ? Omit<T, "type"> & {
14
26
  type: L;
15
27
  } : T;
16
- type VisualizationsType = RequireType<NonNullable<GetResourceResponse["visualization"]>>;
28
+ type VisualizationsType = RequireType<NonNullable<Awaited<ReturnType<typeof resourceVisualization>>>>;
17
29
  type VisualizationsTypes = NonNullable<VisualizationsType["type"]>;
18
30
  type VisualizationByType<T extends VisualizationsTypes> = Extract<VisualizationsType, {
19
31
  type: T;
@@ -33,4 +45,4 @@ export declare const ResourceViewer: React.FC<{
33
45
  id: string;
34
46
  isPreview?: boolean;
35
47
  }>;
36
- export type { VisualizationsType };
48
+ export type { ActionSelectProps, ResourceSelectProps, VisualizationsType };
package/dist/index.js CHANGED
@@ -1,9 +1,13 @@
1
- import { ActionIcon, Alert, Badge, Box, Button, ButtonGroup, Code, Group, LoadingOverlay, Menu, Modal, Popover, ScrollArea, Select, Stack, Text, ThemeIcon, Title } from "@mantine/core";
2
- import { useResource, useResources } from "@ocelescope/api-base";
3
- import { DownloadIcon, EyeOffIcon, Maximize2Icon, MaximizeIcon, MinusIcon, PlusIcon, XIcon } from "lucide-react";
4
- import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
5
- import dynamic from "next/dynamic.js";
1
+ import { ActionIcon, Alert, Autocomplete, Badge, Box, Button, ButtonGroup, Center, Code, FileButton, Group, LoadingOverlay, Menu, Modal, MultiSelect, Popover, ScrollArea, Scroller, Select, Stack, Tabs, Text, Title, Tooltip } from "@mantine/core";
6
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useResource, useResourceTableColumnValues, useResourceTableRows, useResourceTables, useResourceVisualization, useResources, useUpload } from "@ocelescope/api-base";
4
+ import { DownloadIcon, EyeIcon, EyeOffIcon, Maximize2Icon, MaximizeIcon, MinusIcon, PlusIcon, SearchIcon, UploadIcon, XIcon } from "lucide-react";
5
+ import { useDebouncedValue, useDisclosure } from "@mantine/hooks";
6
+ import { DataTable } from "mantine-datatable";
7
+ import { memo, useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } from "react";
8
+ import dayjs from "dayjs";
9
+ import isBetween from "dayjs/plugin/isBetween.js";
10
+ import dynamic from "next/dynamic.js";
7
11
  import { Graphviz } from "@hpcc-js/wasm-graphviz";
8
12
  import FileSaver from "file-saver";
9
13
  import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
@@ -11,9 +15,296 @@ import { Background, BaseEdge, ControlButton, Controls, EdgeLabelRenderer, Handl
11
15
  import { toPng, toSvg } from "html-to-image";
12
16
  import { jsPDF } from "jspdf";
13
17
  import ELK from "elkjs/lib/elk.bundled.js";
14
- import { DataTable } from "mantine-datatable";
15
- import dayjs from "dayjs";
16
- import isBetween from "dayjs/plugin/isBetween.js";
18
+ // src/components/ActionSelect/ActionSelect.tsx
19
+ const attachedStyles = { input: {
20
+ borderTopRightRadius: 0,
21
+ borderBottomRightRadius: 0
22
+ } };
23
+ const renderSelect = (props) => {
24
+ const styles = props.actions ? attachedStyles : void 0;
25
+ if (props.multiple) {
26
+ const { multiple: _, actions: __, ...rest } = props;
27
+ return /* @__PURE__ */ jsx(MultiSelect, {
28
+ flex: 1,
29
+ miw: 0,
30
+ styles,
31
+ ...rest
32
+ });
33
+ }
34
+ const { multiple: _, actions: __, ...rest } = props;
35
+ return /* @__PURE__ */ jsx(Select, {
36
+ flex: 1,
37
+ miw: 0,
38
+ styles,
39
+ ...rest
40
+ });
41
+ };
42
+ const ActionSelect = (props) => /* @__PURE__ */ jsxs(Group, {
43
+ wrap: "nowrap",
44
+ gap: 0,
45
+ align: "flex-end",
46
+ children: [renderSelect(props), props.actions && /* @__PURE__ */ jsxs(ActionIcon.Group, { children: [/* @__PURE__ */ jsx(ActionIcon.GroupSection, { hidden: true }), props.actions] })]
47
+ });
48
+ // src/util/dayjs.ts
49
+ dayjs.extend(isBetween);
50
+ var dayjs_default = dayjs;
51
+ const formatDateTime = (dateString) => dayjs(dateString).format("YYYY-MM-DD HH:mm");
52
+ const formatDate = (dateString) => dayjs(dateString).format("YYYY-MM-DD");
53
+ // src/components/ResourceView/Viewers/Table.tsx
54
+ const formatCell = (value, dataType) => {
55
+ if (value == null) return "";
56
+ switch (dataType) {
57
+ case "boolean": return value ? "✅" : "❌";
58
+ case "date": return dayjs_default(value).isValid() ? formatDate(value) : String(value);
59
+ case "datetime": return dayjs_default(value).isValid() ? formatDateTime(value) : String(value);
60
+ default: return String(value);
61
+ }
62
+ };
63
+ const PAGE_SIZES$1 = [
64
+ 20,
65
+ 50,
66
+ 75
67
+ ];
68
+ const TableView = ({ visualization }) => {
69
+ const [pageSize, setPageSize] = useState(PAGE_SIZES$1[0]);
70
+ const columns = useMemo(() => visualization.columns.map((col) => ({
71
+ accessor: col.id,
72
+ title: col.label ?? col.id,
73
+ render: (record) => formatCell(record[col.id], col.data_type)
74
+ })), [visualization.columns]);
75
+ const [page, setPage] = useState(1);
76
+ const [records, setRecords] = useState(visualization.rows.slice(0, pageSize));
77
+ useEffect(() => {
78
+ const from = (page - 1) * pageSize;
79
+ const to = from + pageSize;
80
+ setRecords(visualization.rows.slice(from, to));
81
+ }, [
82
+ page,
83
+ pageSize,
84
+ visualization
85
+ ]);
86
+ const paginationProps = visualization.rows.length > PAGE_SIZES$1[0] ? {
87
+ totalRecords: visualization.rows.length,
88
+ recordsPerPage: pageSize,
89
+ onPageChange: (p) => setPage(p),
90
+ page,
91
+ recordsPerPageOptions: [...PAGE_SIZES$1],
92
+ onRecordsPerPageChange: (newPageSize) => {
93
+ setPageSize(newPageSize);
94
+ setPage(1);
95
+ }
96
+ } : {};
97
+ return /* @__PURE__ */ jsx(DataTable, {
98
+ records,
99
+ columns,
100
+ withTableBorder: true,
101
+ withColumnBorders: true,
102
+ withRowBorders: true,
103
+ ...paginationProps
104
+ });
105
+ };
106
+ // src/components/ResourceView/TabularView.tsx
107
+ const PAGE_SIZES = [
108
+ 20,
109
+ 50,
110
+ 100
111
+ ];
112
+ const UNSORTED = {
113
+ columnAccessor: "",
114
+ direction: "asc"
115
+ };
116
+ const VALUE_LIMIT = 50;
117
+ const ColumnFilter = ({ resourceId, tableName, columnId, selected, onChange }) => {
118
+ const [search, setSearch] = useState("");
119
+ const [debouncedSearch] = useDebouncedValue(search, 250);
120
+ const { data, isFetching } = useResourceTableColumnValues(resourceId, tableName, columnId, {
121
+ size: VALUE_LIMIT,
122
+ ...debouncedSearch && { search: debouncedSearch }
123
+ }, { query: { placeholderData: (previous) => previous } });
124
+ const options = useMemo(() => [.../* @__PURE__ */ new Set([...selected, ...data?.response ?? []])], [selected, data?.response]);
125
+ const total = data?.total_items ?? 0;
126
+ return /* @__PURE__ */ jsx(MultiSelect, {
127
+ w: 280,
128
+ searchable: true,
129
+ clearable: true,
130
+ data: options,
131
+ value: selected,
132
+ onChange,
133
+ searchValue: search,
134
+ onSearchChange: setSearch,
135
+ filter: ({ options: unfiltered }) => unfiltered,
136
+ placeholder: `Search ${columnId}`,
137
+ nothingFoundMessage: isFetching ? "Loading..." : "No values found",
138
+ description: total > VALUE_LIMIT ? `Showing ${VALUE_LIMIT} of ${total} values, type to narrow down` : void 0,
139
+ comboboxProps: { withinPortal: false }
140
+ });
141
+ };
142
+ const TableRows = ({ resourceId, table }) => {
143
+ const [page, setPage] = useState(1);
144
+ const [pageSize, setPageSize] = useState(PAGE_SIZES[0]);
145
+ const [sortStatus, setSortStatus] = useState(UNSORTED);
146
+ const [filters, setFilters] = useState({});
147
+ const hasFilters = Object.values(filters).some((values) => values.length > 0);
148
+ const { data, isError, isFetching } = useResourceTableRows(resourceId, table.name, {
149
+ page,
150
+ size: pageSize,
151
+ ...sortStatus.columnAccessor && {
152
+ sort_by: sortStatus.columnAccessor,
153
+ descending: sortStatus.direction === "desc"
154
+ },
155
+ ...hasFilters && { filters: JSON.stringify(filters) }
156
+ }, { query: { placeholderData: (previous) => previous } });
157
+ const columns = useMemo(() => table.columns.map((column) => ({
158
+ accessor: column.id,
159
+ title: column.id,
160
+ sortable: true,
161
+ render: (row) => formatCell(row[column.id], column.data_type),
162
+ filtering: (filters[column.id] ?? []).length > 0,
163
+ filter: /* @__PURE__ */ jsx(ColumnFilter, {
164
+ resourceId,
165
+ tableName: table.name,
166
+ columnId: column.id,
167
+ selected: filters[column.id] ?? [],
168
+ onChange: (values) => {
169
+ setFilters((current) => ({
170
+ ...current,
171
+ [column.id]: values
172
+ }));
173
+ setPage(1);
174
+ }
175
+ })
176
+ })), [
177
+ resourceId,
178
+ table.name,
179
+ table.columns,
180
+ filters
181
+ ]);
182
+ const records = useMemo(() => (data?.response ?? []).map((row, index) => ({
183
+ ...row,
184
+ rowKey: (page - 1) * pageSize + index
185
+ })), [
186
+ data?.response,
187
+ page,
188
+ pageSize
189
+ ]);
190
+ if (isError) return /* @__PURE__ */ jsx(Center, {
191
+ h: "100%",
192
+ p: "md",
193
+ children: /* @__PURE__ */ jsx(Text, {
194
+ c: "dimmed",
195
+ children: "Could not load the table"
196
+ })
197
+ });
198
+ return /* @__PURE__ */ jsx(DataTable, {
199
+ height: "100%",
200
+ idAccessor: "rowKey",
201
+ records,
202
+ columns,
203
+ fetching: isFetching,
204
+ withTableBorder: true,
205
+ withColumnBorders: true,
206
+ sortStatus,
207
+ onSortStatusChange: (status) => {
208
+ setSortStatus(status);
209
+ setPage(1);
210
+ },
211
+ totalRecords: data?.total_items ?? 0,
212
+ recordsPerPage: pageSize,
213
+ page,
214
+ onPageChange: setPage,
215
+ recordsPerPageOptions: PAGE_SIZES,
216
+ onRecordsPerPageChange: (size) => {
217
+ setPageSize(size);
218
+ setPage(1);
219
+ }
220
+ });
221
+ };
222
+ const TableTabs = ({ resourceId, tables }) => {
223
+ const [activeTable, setActiveTable] = useState(tables[0]?.name ?? null);
224
+ const [searchOpened, { toggle: toggleSearch, close: closeSearch }] = useDisclosure(false);
225
+ return /* @__PURE__ */ jsxs(Tabs, {
226
+ keepMounted: false,
227
+ h: "100%",
228
+ style: {
229
+ display: "flex",
230
+ flexDirection: "column"
231
+ },
232
+ value: activeTable,
233
+ onChange: setActiveTable,
234
+ children: [tables.length > 1 && /* @__PURE__ */ jsxs(Group, {
235
+ wrap: "nowrap",
236
+ gap: "0",
237
+ style: { flexShrink: 0 },
238
+ children: [/* @__PURE__ */ jsxs(Popover, {
239
+ width: 300,
240
+ position: "bottom-start",
241
+ withArrow: true,
242
+ shadow: "md",
243
+ opened: searchOpened,
244
+ onChange: closeSearch,
245
+ trapFocus: true,
246
+ returnFocus: true,
247
+ children: [/* @__PURE__ */ jsx(Popover.Target, { children: /* @__PURE__ */ jsx(ActionIcon, {
248
+ size: "lg",
249
+ h: "100%",
250
+ onClick: toggleSearch,
251
+ "aria-label": "Search tables",
252
+ bdrs: 0,
253
+ children: /* @__PURE__ */ jsx(SearchIcon, { size: 16 })
254
+ }) }), /* @__PURE__ */ jsx(Popover.Dropdown, { children: /* @__PURE__ */ jsx(Autocomplete, {
255
+ data: tables.map(({ name }) => name),
256
+ "aria-label": "Table",
257
+ placeholder: "Search tables",
258
+ selectFirstOptionOnChange: true,
259
+ comboboxProps: { withinPortal: false },
260
+ onOptionSubmit: (name) => {
261
+ setActiveTable(name);
262
+ closeSearch();
263
+ }
264
+ }) })]
265
+ }), /* @__PURE__ */ jsx(Tabs.List, {
266
+ flex: 1,
267
+ miw: 0,
268
+ children: /* @__PURE__ */ jsx(Scroller, { children: tables.map(({ name }) => /* @__PURE__ */ jsx(Tabs.Tab, {
269
+ value: name,
270
+ children: name
271
+ }, name)) })
272
+ })]
273
+ }), tables.map((table) => /* @__PURE__ */ jsx(Tabs.Panel, {
274
+ value: table.name,
275
+ flex: 1,
276
+ mih: 0,
277
+ children: /* @__PURE__ */ jsx(TableRows, {
278
+ resourceId,
279
+ table
280
+ })
281
+ }, table.name))]
282
+ });
283
+ };
284
+ const TabularView = ({ resourceId }) => {
285
+ const { data: tables, isError, isPending } = useResourceTables(resourceId);
286
+ if (isError) return /* @__PURE__ */ jsx(Center, {
287
+ h: "100%",
288
+ p: "md",
289
+ children: /* @__PURE__ */ jsx(Text, {
290
+ c: "dimmed",
291
+ children: "Could not load the tables"
292
+ })
293
+ });
294
+ if (isPending) return /* @__PURE__ */ jsx(LoadingOverlay, { visible: true });
295
+ if (tables.length === 0) return /* @__PURE__ */ jsx(Center, {
296
+ h: "100%",
297
+ p: "md",
298
+ children: /* @__PURE__ */ jsx(Text, {
299
+ c: "dimmed",
300
+ children: "This resource has no tables"
301
+ })
302
+ });
303
+ return /* @__PURE__ */ jsx(TableTabs, {
304
+ resourceId,
305
+ tables
306
+ });
307
+ };
17
308
  // src/components/ResourceView/Viewers/R4pm.tsx
18
309
  const r4pmViewer = (pick) => dynamic(async () => {
19
310
  const [components, { wasmViz }] = await Promise.all([import("@r4pm/components"), import("@r4pm/components/viz/wasm")]);
@@ -1788,64 +2079,6 @@ const PlotlyViewer = ({ visualization, isPreview }) => {
1788
2079
  }
1789
2080
  });
1790
2081
  };
1791
- // src/util/dayjs.ts
1792
- dayjs.extend(isBetween);
1793
- var dayjs_default = dayjs;
1794
- const formatDateTime = (dateString) => dayjs(dateString).format("YYYY-MM-DD HH:mm");
1795
- const formatDate = (dateString) => dayjs(dateString).format("YYYY-MM-DD");
1796
- // src/components/ResourceView/Viewers/Table.tsx
1797
- const formatCell = (value, dataType) => {
1798
- if (value == null) return "";
1799
- switch (dataType) {
1800
- case "boolean": return value ? "✅" : "❌";
1801
- case "date": return dayjs_default(value).isValid() ? formatDate(value) : String(value);
1802
- case "datetime": return dayjs_default(value).isValid() ? formatDateTime(value) : String(value);
1803
- default: return String(value);
1804
- }
1805
- };
1806
- const PAGE_SIZES = [
1807
- 20,
1808
- 50,
1809
- 75
1810
- ];
1811
- const TableView = ({ visualization }) => {
1812
- const [pageSize, setPageSize] = useState(PAGE_SIZES[0]);
1813
- const columns = useMemo(() => visualization.columns.map((col) => ({
1814
- accessor: col.id,
1815
- title: col.label ?? col.id,
1816
- render: (record) => formatCell(record[col.id], col.data_type)
1817
- })), [visualization.columns]);
1818
- const [page, setPage] = useState(1);
1819
- const [records, setRecords] = useState(visualization.rows.slice(0, pageSize));
1820
- useEffect(() => {
1821
- const from = (page - 1) * pageSize;
1822
- const to = from + pageSize;
1823
- setRecords(visualization.rows.slice(from, to));
1824
- }, [
1825
- page,
1826
- pageSize,
1827
- visualization
1828
- ]);
1829
- const paginationProps = visualization.rows.length > PAGE_SIZES[0] ? {
1830
- totalRecords: visualization.rows.length,
1831
- recordsPerPage: pageSize,
1832
- onPageChange: (p) => setPage(p),
1833
- page,
1834
- recordsPerPageOptions: [...PAGE_SIZES],
1835
- onRecordsPerPageChange: (newPageSize) => {
1836
- setPageSize(newPageSize);
1837
- setPage(1);
1838
- }
1839
- } : {};
1840
- return /* @__PURE__ */ jsx(DataTable, {
1841
- records,
1842
- columns,
1843
- withTableBorder: true,
1844
- withColumnBorders: true,
1845
- withRowBorders: true,
1846
- ...paginationProps
1847
- });
1848
- };
1849
2082
  // src/components/ResourceView/index.tsx
1850
2083
  const visualizationMap = {
1851
2084
  table: TableView,
@@ -1866,33 +2099,72 @@ const Visualization = ({ visualization, isPreview = false, menuItems }) => {
1866
2099
  ...menuItems && { menuItems }
1867
2100
  });
1868
2101
  };
1869
- const ResourceViewer = ({ id, isPreview = false }) => {
1870
- const { data } = useResource(id);
1871
- if (!data) return /* @__PURE__ */ jsx(LoadingOverlay, {});
1872
- if (!data.visualization?.type) return /* @__PURE__ */ jsx(Stack, {
1873
- justify: "center",
2102
+ const LoadError = () => /* @__PURE__ */ jsx(Center, {
2103
+ h: "100%",
2104
+ p: "md",
2105
+ children: /* @__PURE__ */ jsx(Text, {
2106
+ c: "dimmed",
2107
+ children: "Could not load the visualization"
2108
+ })
2109
+ });
2110
+ const NoVisualization = ({ resourceTypeLabel }) => /* @__PURE__ */ jsx(Center, {
2111
+ h: "100%",
2112
+ p: "md",
2113
+ children: /* @__PURE__ */ jsxs(Stack, {
1874
2114
  align: "center",
1875
- h: "100%",
1876
- children: /* @__PURE__ */ jsx(ThemeIcon, {
1877
- w: 150,
1878
- h: 150,
1879
- variant: "transparent",
1880
- color: "gray",
1881
- children: /* @__PURE__ */ jsx(EyeOffIcon, {
1882
- width: 150,
1883
- height: 150
2115
+ gap: "xs",
2116
+ children: [
2117
+ /* @__PURE__ */ jsx(EyeOffIcon, {
2118
+ size: 64,
2119
+ strokeWidth: 1.5,
2120
+ color: "var(--mantine-color-dimmed)"
2121
+ }),
2122
+ /* @__PURE__ */ jsx(Text, {
2123
+ fw: 500,
2124
+ children: "No visualization available"
2125
+ }),
2126
+ /* @__PURE__ */ jsxs(Group, {
2127
+ gap: 6,
2128
+ justify: "center",
2129
+ children: [/* @__PURE__ */ jsx(Badge, {
2130
+ size: "sm",
2131
+ color: "gray",
2132
+ children: resourceTypeLabel
2133
+ }), /* @__PURE__ */ jsx(Text, {
2134
+ size: "sm",
2135
+ c: "dimmed",
2136
+ children: "does not provide a visualization"
2137
+ })]
1884
2138
  })
1885
- })
1886
- });
2139
+ ]
2140
+ })
2141
+ });
2142
+ const ResourceVisualization = ({ id, resourceTypeLabel, isPreview }) => {
2143
+ const { data, isError, isPending } = useResourceVisualization(id);
2144
+ if (isError) return /* @__PURE__ */ jsx(LoadError, {});
2145
+ if (isPending) return /* @__PURE__ */ jsx(LoadingOverlay, { visible: true });
2146
+ if (!data) return /* @__PURE__ */ jsx(NoVisualization, { resourceTypeLabel });
1887
2147
  return /* @__PURE__ */ jsx(Visualization, {
1888
- visualization: data.visualization,
2148
+ visualization: data,
1889
2149
  isPreview
1890
2150
  });
1891
2151
  };
2152
+ const ResourceViewer = ({ id, isPreview = false }) => {
2153
+ const { data: resource, isError, isPending } = useResource(id);
2154
+ if (isError) return /* @__PURE__ */ jsx(LoadError, {});
2155
+ if (isPending) return /* @__PURE__ */ jsx(LoadingOverlay, { visible: true });
2156
+ if (resource.has_visualization) return /* @__PURE__ */ jsx(ResourceVisualization, {
2157
+ id,
2158
+ resourceTypeLabel: resource.resource_type_label,
2159
+ isPreview
2160
+ });
2161
+ if (resource.kind === "tabular") return /* @__PURE__ */ jsx(TabularView, { resourceId: id });
2162
+ return /* @__PURE__ */ jsx(NoVisualization, { resourceTypeLabel: resource.resource_type_label });
2163
+ };
1892
2164
  // src/components/ResourceModal/ResourceModal.tsx
1893
2165
  const ResourceModal = ({ id, onClose }) => {
1894
2166
  const { data } = useResource(id, { query: { enabled: !!id } });
1895
- return data?.resource.id && /* @__PURE__ */ jsx(Modal, {
2167
+ return data?.id && /* @__PURE__ */ jsx(Modal, {
1896
2168
  opened: !!data,
1897
2169
  onClose,
1898
2170
  fullScreen: true,
@@ -1913,7 +2185,7 @@ const ResourceModal = ({ id, onClose }) => {
1913
2185
  justify: "space-between",
1914
2186
  children: [/* @__PURE__ */ jsx(Title, {
1915
2187
  size: "h5",
1916
- children: data.resource.name
2188
+ children: data.name
1917
2189
  }), /* @__PURE__ */ jsx(ActionIcon, {
1918
2190
  variant: "subtle",
1919
2191
  color: "red",
@@ -1925,20 +2197,92 @@ const ResourceModal = ({ id, onClose }) => {
1925
2197
  h: "100%",
1926
2198
  flex: 1,
1927
2199
  mih: 0,
1928
- children: /* @__PURE__ */ jsx(ResourceViewer, { id: data.resource.id })
2200
+ children: /* @__PURE__ */ jsx(ResourceViewer, { id: data.id })
1929
2201
  })]
1930
2202
  })
1931
2203
  });
1932
2204
  };
1933
2205
  // src/components/ResourceSelect/ResourceSelect.tsx
1934
- const ResourceSelect = ({ type, value, ...props }) => {
2206
+ const useResourceOptions = (type, onUploaded) => {
1935
2207
  const { data: resources = [] } = useResources({ schema_hash: type ?? null });
1936
- return /* @__PURE__ */ jsx(Select, {
1937
- ...props,
1938
- data: resources.map(({ name, id }) => ({
2208
+ const [pendingTaskIds, setPendingTaskIds] = useState([]);
2209
+ const options = useMemo(() => resources.map(({ name, id }) => ({
2210
+ value: id,
2211
+ label: name
2212
+ })), [resources]);
2213
+ const handleUploaded = useEffectEvent(onUploaded);
2214
+ useEffect(() => {
2215
+ const uploaded = resources.filter(({ source_id }) => source_id && pendingTaskIds.includes(source_id));
2216
+ if (!uploaded.length) return;
2217
+ setPendingTaskIds((taskIds) => taskIds.filter((taskId) => !uploaded.some(({ source_id }) => source_id === taskId)));
2218
+ handleUploaded(uploaded.map(({ id, name }) => ({
1939
2219
  value: id,
1940
2220
  label: name
1941
- }))
2221
+ })));
2222
+ }, [resources, pendingTaskIds]);
2223
+ const trackUpload = (taskIds) => setPendingTaskIds((pending) => [...pending, ...taskIds]);
2224
+ return {
2225
+ options,
2226
+ trackUpload
2227
+ };
2228
+ };
2229
+ const UploadButton = ({ disabled, onUpload }) => {
2230
+ const { mutate: upload, isPending } = useUpload({ mutation: { onSuccess: onUpload } });
2231
+ return /* @__PURE__ */ jsx(FileButton, {
2232
+ accept: ".ocelescope",
2233
+ multiple: true,
2234
+ onChange: (files) => {
2235
+ if (files.length) upload({ data: { files } });
2236
+ },
2237
+ children: (props) => /* @__PURE__ */ jsx(Tooltip, {
2238
+ label: "Upload resource",
2239
+ withArrow: true,
2240
+ children: /* @__PURE__ */ jsx(ActionIcon, {
2241
+ ...props,
2242
+ size: "input-sm",
2243
+ loading: isPending,
2244
+ disabled,
2245
+ "aria-label": "Upload resource",
2246
+ children: /* @__PURE__ */ jsx(UploadIcon, { size: 16 })
2247
+ })
2248
+ })
2249
+ });
2250
+ };
2251
+ const ViewButton = ({ resourceId, onView }) => /* @__PURE__ */ jsx(Tooltip, {
2252
+ label: resourceId ? "View resource" : "Select a resource to view it",
2253
+ withArrow: true,
2254
+ children: /* @__PURE__ */ jsx(ActionIcon, {
2255
+ variant: "default",
2256
+ size: "input-sm",
2257
+ "data-disabled": !resourceId || void 0,
2258
+ "aria-disabled": !resourceId,
2259
+ onClick: resourceId ? () => onView(resourceId) : void 0,
2260
+ "aria-label": "View resource",
2261
+ children: /* @__PURE__ */ jsx(EyeIcon, { size: 16 })
2262
+ })
2263
+ });
2264
+ const ResourceSelect = (props) => {
2265
+ const { type, withUpload = true, withView = true, ...selectProps } = props;
2266
+ const [viewedId, setViewedId] = useState();
2267
+ const { options, trackUpload } = useResourceOptions(type, (uploaded) => {
2268
+ if (props.multiple) props.onChange([.../* @__PURE__ */ new Set([...props.value, ...uploaded.map(({ value }) => value)])]);
2269
+ else if (uploaded[0]) props.onChange(uploaded[0].value, uploaded[0]);
1942
2270
  });
2271
+ const viewId = props.multiple ? null : props.value;
2272
+ const showView = withView && !props.multiple;
2273
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ActionSelect, {
2274
+ ...selectProps,
2275
+ data: options,
2276
+ actions: (showView || withUpload) && /* @__PURE__ */ jsxs(Fragment, { children: [showView && /* @__PURE__ */ jsx(ViewButton, {
2277
+ resourceId: viewId,
2278
+ onView: setViewedId
2279
+ }), withUpload && /* @__PURE__ */ jsx(UploadButton, {
2280
+ disabled: props.disabled,
2281
+ onUpload: trackUpload
2282
+ })] })
2283
+ }), viewedId && /* @__PURE__ */ jsx(ResourceModal, {
2284
+ id: viewedId,
2285
+ onClose: () => setViewedId(void 0)
2286
+ })] });
1943
2287
  };
1944
- export { ResourceModal, ResourceSelect, ResourceViewer, Visualization };
2288
+ export { ActionSelect, ResourceModal, ResourceSelect, ResourceViewer, Visualization };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ocelescope/resources",
3
3
  "type": "module",
4
- "version": "0.11.0",
4
+ "version": "0.13.0",
5
5
  "description": "Ocelescope resources: helpers and UI components for rendering and exporting data.",
6
6
  "author": "Görkem-Emre Öztürk <goerkem.oeztuerk@rwth-aachen.de>",
7
7
  "license": "MIT",
@@ -30,7 +30,8 @@
30
30
  "@floating-ui/react": "^0.27.18",
31
31
  "@hpcc-js/wasm-graphviz": "^1.30.0",
32
32
  "@mantine/core": "^9.7.4",
33
- "@ocelescope/api-base": "^0.11.0",
33
+ "@mantine/hooks": "^9.7.4",
34
+ "@ocelescope/api-base": "^0.13.0",
34
35
  "@r4pm/components": "^0.4.0",
35
36
  "@xyflow/react": "^12.12.0",
36
37
  "dayjs": "^1.11.23",
@@ -38,7 +39,7 @@
38
39
  "file-saver": "^2.0.5",
39
40
  "lucide-react": "^1.63.0",
40
41
  "mantine-datatable": "^9.4.0",
41
- "next": "16.4.3",
42
+ "next": "16.4.5",
42
43
  "react": "^19.3.1",
43
44
  "react-dom": "^19.3.1",
44
45
  "react-plotly.js": "^4.0.0",