diff --git a/package-lock.json b/package-lock.json index c01af8397..826495998 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33385,23 +33385,23 @@ "version": "0.31.0", "license": "Apache-2.0", "dependencies": { - "@deephaven/chart": "^1.4.0", - "@deephaven/components": "^1.4.0", - "@deephaven/console": "^1.4.0", - "@deephaven/dashboard": "^1.4.0", - "@deephaven/dashboard-core-plugins": "^1.4.0", - "@deephaven/golden-layout": "^1.4.0", + "@deephaven/chart": "^1.5.1", + "@deephaven/components": "^1.5.1", + "@deephaven/console": "^1.5.1", + "@deephaven/dashboard": "^1.5.1", + "@deephaven/dashboard-core-plugins": "^1.5.1", + "@deephaven/golden-layout": "^1.5.1", "@deephaven/grid": "^1.3.0", "@deephaven/icons": "^1.2.0", - "@deephaven/iris-grid": "^1.4.0", - "@deephaven/jsapi-bootstrap": "^1.4.0", - "@deephaven/jsapi-components": "^1.4.0", + "@deephaven/iris-grid": "^1.5.1", + "@deephaven/jsapi-bootstrap": "^1.5.1", + "@deephaven/jsapi-components": "^1.5.1", "@deephaven/jsapi-types": "^1.0.0-dev0.39.6", "@deephaven/jsapi-utils": "^1.4.0", "@deephaven/log": "^1.1.0", - "@deephaven/plugin": "^1.4.0", + "@deephaven/plugin": "^1.5.1", "@deephaven/react-hooks": "^1.2.0", - "@deephaven/redux": "^1.4.0", + "@deephaven/redux": "^1.5.1", "@deephaven/test-utils": "^1.1.0", "@deephaven/utils": "^1.1.0", "@fortawesome/react-fontawesome": "^0.2.0", @@ -33429,11 +33429,11 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/chart": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@deephaven/chart/-/chart-1.4.0.tgz", - "integrity": "sha512-Ek1Npk0UXvk3skpOPovHCza2Qw2z/Cdkk/MGygTdp0ufApNyRWhJWdmbFiz0DDSGU/Kd5ljTXt44cH+0gH7dHg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@deephaven/chart/-/chart-1.5.1.tgz", + "integrity": "sha512-PgqLsK3b7KBvpymOSscvmW5K//ZTWpjG13vgrMW7qIVQUAwwSNnnvsf5o5ahJnMq6olSMnoOFg2zBrAMyOvTaw==", "dependencies": { - "@deephaven/components": "^1.4.0", + "@deephaven/components": "^1.5.1", "@deephaven/icons": "^1.2.0", "@deephaven/jsapi-types": "^1.0.0-dev0.39.4", "@deephaven/jsapi-utils": "^1.4.0", @@ -33458,9 +33458,9 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/components": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@deephaven/components/-/components-1.4.0.tgz", - "integrity": "sha512-pu6/gOHfNwlpalV7g5T2zVnZiUKM7uvJwLIJ3PEDQfjyPEu7RmHfC+vWj86Vo6pIPuNoAcFt+K6NftqPJyBJ3w==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@deephaven/components/-/components-1.5.1.tgz", + "integrity": "sha512-AgShjar3dMqjeqqo7im76RoCeoMq9AbGWRp/agO+mi6Sw9Jg2lBmwnV0FPVmkgvp3JbBApINmD6U0WDMLCNWUw==", "dependencies": { "@adobe/react-spectrum": "3.38.0", "@deephaven/icons": "^1.2.0", @@ -33593,15 +33593,15 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/console": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@deephaven/console/-/console-1.4.0.tgz", - "integrity": "sha512-opTQvOiO5R5fI/nkFQqQPZLjvfuSZJob9Bpnub9x/b8U2eJKCYjAEnLTcV04sw7jVfXJ/xA86k+UWkdywuPwMQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@deephaven/console/-/console-1.5.1.tgz", + "integrity": "sha512-R7rBJrDtunGOuBMnGmERuHXV8rGV0QiKPAI+zZFMMgbNeq5gbEHhvGyfsO1vmXxwIdQoCWgsPSrYv9nDVc+NNQ==", "dependencies": { "@astral-sh/ruff-wasm-web": "0.6.4", - "@deephaven/chart": "^1.4.0", - "@deephaven/components": "^1.4.0", + "@deephaven/chart": "^1.5.1", + "@deephaven/components": "^1.5.1", "@deephaven/icons": "^1.2.0", - "@deephaven/jsapi-bootstrap": "^1.4.0", + "@deephaven/jsapi-bootstrap": "^1.5.1", "@deephaven/jsapi-types": "^1.0.0-dev0.39.4", "@deephaven/jsapi-utils": "^1.4.0", "@deephaven/log": "^1.1.0", @@ -33647,15 +33647,15 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/dashboard": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@deephaven/dashboard/-/dashboard-1.4.0.tgz", - "integrity": "sha512-GjaMaE5ga3k03Z5bjvRo8CKG/kiKdSGFW1fwrCnXaI9rbEagnMOxR7U0PEt3EGdFBx0aJAj61ynYcf1fyo56HA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@deephaven/dashboard/-/dashboard-1.5.1.tgz", + "integrity": "sha512-BMEWQSdpGiHVhLLsFtRGw5HLfGnZOR4zoZIDGCB6byxqPhvFKLSJ3zWk6bBahJ54hWGrLmkUZKdZ4e/BroF2wQ==", "dependencies": { - "@deephaven/components": "^1.4.0", - "@deephaven/golden-layout": "^1.4.0", + "@deephaven/components": "^1.5.1", + "@deephaven/golden-layout": "^1.5.1", "@deephaven/log": "^1.1.0", "@deephaven/react-hooks": "^1.2.0", - "@deephaven/redux": "^1.4.0", + "@deephaven/redux": "^1.5.1", "@deephaven/utils": "^1.1.0", "classnames": "^2.3.1", "fast-deep-equal": "^3.1.3", @@ -33675,28 +33675,28 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/dashboard-core-plugins": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@deephaven/dashboard-core-plugins/-/dashboard-core-plugins-1.4.0.tgz", - "integrity": "sha512-wpD60VrDdQmOL7DRKCyoxqudNGHNZAIBLJWdk6UIQxa3XUaXDNSDcUbKliAQh5Zljd1O+Bimf6p/MpVl3FwLRw==", - "dependencies": { - "@deephaven/chart": "^1.4.0", - "@deephaven/components": "^1.4.0", - "@deephaven/console": "^1.4.0", - "@deephaven/dashboard": "^1.4.0", - "@deephaven/file-explorer": "^1.4.0", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@deephaven/dashboard-core-plugins/-/dashboard-core-plugins-1.5.1.tgz", + "integrity": "sha512-UN3e7LtxP3DlP2FnqJ1eIHgI031hlOCfZlUmBfhUeqZt/aQvVXo1ajKcmYbcG8ECzsu4YmWJD/ISC4lhpIYKHw==", + "dependencies": { + "@deephaven/chart": "^1.5.1", + "@deephaven/components": "^1.5.1", + "@deephaven/console": "^1.5.1", + "@deephaven/dashboard": "^1.5.1", + "@deephaven/file-explorer": "^1.5.1", "@deephaven/filters": "^1.1.0", - "@deephaven/golden-layout": "^1.4.0", + "@deephaven/golden-layout": "^1.5.1", "@deephaven/grid": "^1.3.0", "@deephaven/icons": "^1.2.0", - "@deephaven/iris-grid": "^1.4.0", - "@deephaven/jsapi-bootstrap": "^1.4.0", - "@deephaven/jsapi-components": "^1.4.0", + "@deephaven/iris-grid": "^1.5.1", + "@deephaven/jsapi-bootstrap": "^1.5.1", + "@deephaven/jsapi-components": "^1.5.1", "@deephaven/jsapi-types": "^1.0.0-dev0.39.4", "@deephaven/jsapi-utils": "^1.4.0", "@deephaven/log": "^1.1.0", - "@deephaven/plugin": "^1.4.0", + "@deephaven/plugin": "^1.5.1", "@deephaven/react-hooks": "^1.2.0", - "@deephaven/redux": "^1.4.0", + "@deephaven/redux": "^1.5.1", "@deephaven/storage": "^1.1.0", "@deephaven/utils": "^1.1.0", "@fortawesome/react-fontawesome": "^0.2.0", @@ -33726,11 +33726,11 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/dashboard-core-plugins/node_modules/@deephaven/file-explorer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@deephaven/file-explorer/-/file-explorer-1.4.0.tgz", - "integrity": "sha512-89UA9yWf/EuBLyRdTvh4aobwWuvtOnY57naxm0z6nL/urX8J1FiBZKoDD17HmC1v9zl3j78x5OhYzFDldrrDYg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@deephaven/file-explorer/-/file-explorer-1.5.1.tgz", + "integrity": "sha512-slx7B6vfdxrSK0hRwMSTIhR7tuaP+fsQFJtTKWO0DHKpbeiGTJ49QRcWEO5TSJfIGkFsp5rQdmyz9+7nKZ6bIw==", "dependencies": { - "@deephaven/components": "^1.4.0", + "@deephaven/components": "^1.5.1", "@deephaven/icons": "^1.2.0", "@deephaven/log": "^1.1.0", "@deephaven/storage": "^1.1.0", @@ -33791,11 +33791,11 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/golden-layout": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@deephaven/golden-layout/-/golden-layout-1.4.0.tgz", - "integrity": "sha512-wJz/rvYPg316LHRj/rdB2ci/V2/zMFhCmV0x67rVyEn7UXyCqGugx5KpNuEqLW+HJ6gj4v/9YMifNjC75tv5KQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@deephaven/golden-layout/-/golden-layout-1.5.1.tgz", + "integrity": "sha512-aGXb/7JRGKJMfRyoR3K1bF2yxxoAUZNaGs7DxxZoPVYdH3OA82GMaJESmBGzmHuS82blfwkHtCSWt6nBXA8vhg==", "dependencies": { - "@deephaven/components": "^1.4.0", + "@deephaven/components": "^1.5.1", "jquery": "^3.6.0", "nanoid": "^5.0.7" }, @@ -33839,16 +33839,16 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/iris-grid": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@deephaven/iris-grid/-/iris-grid-1.4.0.tgz", - "integrity": "sha512-dkzteeeSWpqHKJ7qQJcKcHRYNdmIut742OeuhHgc3o8lhiM/xmfS7gPCB+E0e/JECHlBedueXzCHFTEilLHZpg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@deephaven/iris-grid/-/iris-grid-1.5.1.tgz", + "integrity": "sha512-MN0yUR0F+okQvSAlteh5vYFBeSyzmojmnh1DRvBUIkKLj4T/s3uUFErgyYG5hcY6R6yyfADedU3PUED65wcZOQ==", "dependencies": { - "@deephaven/components": "^1.4.0", - "@deephaven/console": "^1.4.0", + "@deephaven/components": "^1.5.1", + "@deephaven/console": "^1.5.1", "@deephaven/filters": "^1.1.0", "@deephaven/grid": "^1.3.0", "@deephaven/icons": "^1.2.0", - "@deephaven/jsapi-components": "^1.4.0", + "@deephaven/jsapi-components": "^1.5.1", "@deephaven/jsapi-types": "^1.0.0-dev0.39.4", "@deephaven/jsapi-utils": "^1.4.0", "@deephaven/log": "^1.1.0", @@ -33897,11 +33897,11 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/jsapi-bootstrap": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@deephaven/jsapi-bootstrap/-/jsapi-bootstrap-1.4.0.tgz", - "integrity": "sha512-1pBPMoibrUqz3yl4Y/3XMdfJzC3uM+/Ts8yfVccBCQOXpHUC4gPyp1rhrzce5QK7l01Z9QhPfWzieRixnhyAfQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@deephaven/jsapi-bootstrap/-/jsapi-bootstrap-1.5.1.tgz", + "integrity": "sha512-KKLpjChMf97QOAAac46owp6NLutcLqOGhb49ZbFb4rb4a+J4NKIaUO0EB2FUiYnrp4SNVri3izhIJXfN5J7OdA==", "dependencies": { - "@deephaven/components": "^1.4.0", + "@deephaven/components": "^1.5.1", "@deephaven/jsapi-types": "^1.0.0-dev0.39.4", "@deephaven/log": "^1.1.0", "@deephaven/react-hooks": "^1.2.0", @@ -33915,12 +33915,12 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/jsapi-components": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@deephaven/jsapi-components/-/jsapi-components-1.4.0.tgz", - "integrity": "sha512-rkUC6CuMMY+Uo9MMQN8Gn4ZyS6PCaGH0E+Shq9ttAVYBaz00zzmRi8PEdOs82UxBNmVo8JmX5x5j1CPd6litqg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@deephaven/jsapi-components/-/jsapi-components-1.5.1.tgz", + "integrity": "sha512-KJDEbnlTsxeroTzFdnlZ+Y8alHqajI56/U+KvQS1K5zk7B+pb5QS+ZHddcn5ndklUUnHOJd+2TmHjlT0LgyRYQ==", "dependencies": { - "@deephaven/components": "^1.4.0", - "@deephaven/jsapi-bootstrap": "^1.4.0", + "@deephaven/components": "^1.5.1", + "@deephaven/jsapi-bootstrap": "^1.5.1", "@deephaven/jsapi-types": "^1.0.0-dev0.39.4", "@deephaven/jsapi-utils": "^1.4.0", "@deephaven/log": "^1.1.0", @@ -33973,15 +33973,15 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/plugin": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@deephaven/plugin/-/plugin-1.4.0.tgz", - "integrity": "sha512-3G8hGv5YQ+mQ9aMhnNAmC6rRi6i7hoPQswwpkauegEtdau7ScVvO9ED+ttspLuhlksBzrjzBwjtHRn9GhLnXKg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@deephaven/plugin/-/plugin-1.5.1.tgz", + "integrity": "sha512-0xprRa9gzsiwgvoqananeuFE1cSofEpdBggG+/F6kh2IYopavVx9cl1KDjMvLeKomVANANGkfntIi46INKx3jA==", "dependencies": { - "@deephaven/components": "^1.4.0", - "@deephaven/golden-layout": "^1.4.0", + "@deephaven/components": "^1.5.1", + "@deephaven/golden-layout": "^1.5.1", "@deephaven/grid": "^1.3.0", "@deephaven/icons": "^1.2.0", - "@deephaven/iris-grid": "^1.4.0", + "@deephaven/iris-grid": "^1.5.1", "@deephaven/jsapi-types": "^1.0.0-dev0.39.4", "@deephaven/log": "^1.1.0", "@deephaven/react-hooks": "^1.2.0", @@ -34088,14 +34088,14 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/redux": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@deephaven/redux/-/redux-1.4.0.tgz", - "integrity": "sha512-+TAskt5HA1BVx1vG3IeIrLPGb48bXeCTDaDMlC8YS95ooBtkfOhNlUHzJPpKnxSX7nyVp69ZDMwLh7vrS7ptDQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@deephaven/redux/-/redux-1.5.1.tgz", + "integrity": "sha512-jTSD9rFVl14bUyOGuS7PZnXIcaYem80EoQjvNkfUcQZ8EYjOKt90iDvGve0dtNou6nsLRqggS7HeADBLh8jffw==", "dependencies": { "@deephaven/jsapi-types": "^1.0.0-dev0.39.4", "@deephaven/jsapi-utils": "^1.4.0", "@deephaven/log": "^1.1.0", - "@deephaven/plugin": "^1.4.0", + "@deephaven/plugin": "^1.5.1", "fast-deep-equal": "^3.1.3", "proxy-memoize": "^3.0.0", "redux-thunk": "2.4.1" diff --git a/plugins/ui/docs/components/combo_box.md b/plugins/ui/docs/components/combo_box.md index 38ad8dcb7..27f905ec2 100644 --- a/plugins/ui/docs/components/combo_box.md +++ b/plugins/ui/docs/components/combo_box.md @@ -47,7 +47,7 @@ Recommendations for creating clear and effective combo boxes: ## Data sources -For combo boxes, we can use a Deephaven table as a data source to populate the options. When using a table, it automatically uses the first column as both the key and label. If there are any duplicate keys, an error will be thrown; to avoid this, a `select_distinct` can be used on the table prior to using it as a combo box data source. +For combo boxes, we can use a Deephaven table or [URI](uri.md) as a data source to populate the options. When using a table, it automatically uses the first column as both the key and label. If there are any duplicate keys, an error will be thrown; to avoid this, a `select_distinct` can be used on the table prior to using it as a combo box data source. ```python order=my_combo_box_table_source_example,countries from deephaven import ui, empty_table diff --git a/plugins/ui/docs/components/list_view.md b/plugins/ui/docs/components/list_view.md index 5b9ec982c..0c546b157 100644 --- a/plugins/ui/docs/components/list_view.md +++ b/plugins/ui/docs/components/list_view.md @@ -26,7 +26,7 @@ my_list_view = ui_list_view() ## Table Source Example -List view items can also be generated from a table directly or using `item_table_source`. +List view items can also be generated from a table or [URI](uri.md) directly or using `item_table_source`. ### Passing Table Directly diff --git a/plugins/ui/docs/components/picker.md b/plugins/ui/docs/components/picker.md index a2671c5de..4f9e24c2c 100644 --- a/plugins/ui/docs/components/picker.md +++ b/plugins/ui/docs/components/picker.md @@ -41,7 +41,7 @@ Recommendations for creating pickers: ## Data sources -We can use a Deephaven table as a data source to populate the options for pickers. A table automatically uses the first column as both the key and label. If there are duplicate keys, an error will be thrown; to avoid this, a `select_distinct` can be used on the table before using it as a picker data source. +We can use a Deephaven table or [URI](uri.md) as a data source to populate the options for pickers. A table automatically uses the first column as both the key and label. If there are duplicate keys, an error will be thrown; to avoid this, a `select_distinct` can be used on the table before using it as a picker data source. ```python order=my_picker_table_source_example,stocks from deephaven import ui diff --git a/plugins/ui/docs/components/table.md b/plugins/ui/docs/components/table.md index 046ecfa2e..2d11d5830 100644 --- a/plugins/ui/docs/components/table.md +++ b/plugins/ui/docs/components/table.md @@ -19,6 +19,10 @@ t = ui.table(_t) 2. Use a UI table to show properties like filters as if the user had created them in the UI. Users can change the default values provided by the UI table, such as filters. 3. UI tables handle ticking tables automatically, so you can pass any Deephaven table to a UI table. +## Table data source + +The first argument to `ui.table` is the table data source. This can be any Deephaven table, a URI to a table, or a string which will be resolved as a URI. See the [URI Element](uri.md) documentation for more information on how to use URIs with UI elements. + ## Formatting You can format the table using the `format_` prop. This prop takes a `ui.TableFormmat` object or list of `ui.TableFormat` objects. `ui.TableFormat` is a dataclass that encapsulates the formatting options for a table. The full list of formatting options can be found in the [API Reference](#tableformat). diff --git a/plugins/ui/docs/components/uri.md b/plugins/ui/docs/components/uri.md new file mode 100644 index 000000000..9715cecb7 --- /dev/null +++ b/plugins/ui/docs/components/uri.md @@ -0,0 +1,74 @@ +# URI + +URIs are a way to reference Deephaven resources, such as tables or figures, from another instance. Deephaven UI has its own `ui.resolve` method that does not require server-to-server communication. Instead, the web client communicates directly with the appropriate server to get the resource. + +## Usage + +Deephaven UI provides a `resolve` method (not to be confused with the `resolve` method from the [Deephaven URI package](/core/pydoc/code/deephaven.uri.html)) that allows you to reference Deephaven resources from other instances. Unlike the Deephaven URI package, `ui.resolve` does not resolve the URI to its resource on the server, so you cannot apply operations to the resource. + +> [!NOTE] +> Currently, the only valid URIs for Deephaven UI are for Deephaven Enterprise Persistent Queries. +> See the [Deephaven Enterprise documentation](/enterprise/docs/deephaven-database/remote-tables-python/#uris) for more information on Persistent Query URIs. The optional parameters are ignored by `ui.resolve`. + +### Plain references + +One way to use `ui.resolve` is to assign the reference to a variable, which the web UI will open just as if you created the resource. This can be useful if you want to display tables from multiple sources in a single dashboard without the worker defining the dashboard pulling the data from each source. + +```py order=null +from deephaven import ui + +t = ui.resolve("pq://MyPersistentQuery/scope/table") # Can't do t.update() or any other operations +p = ui.resolve("pq://MyPersistentQuery/scope/plot") + + +@ui.component +def basic_dashboard(): + return ui.panel(ui.flex(t, p), title="Table and Plot") + + +my_dashboard = ui.dashboard(basic_dashboard()) +``` + +### Usage in UI components + +Some Deephaven UI components that accept tables as sources can also accept URIs. This includes [`ui.table`](table.md) and any components that accept an `item_table_source`. When using a URI with UI components, you can often just use the string without needing to call `ui.resolve`. However, if a component may take a string as a valid child (e.g., `ui.picker`), then you must use `ui.resolve` to distinguish between a string and a URI. You can always use `ui.resolve` in places where you can use just the string if you prefer to be explicit. + +> [!WARNING] +> Deephaven UI URIs cannot be used as table sources in the Deephaven Express plotting library. + +```py order=null +from deephaven import ui + +# You can use any ui.table props with a URI source +t = ui.table( + "pq://MyPersistentQuery/scope/table", + format_=ui.TableFormat(cols="A", background_color="salmon") +) + +# Must use ui.resolve because string is a valid child +picker = ui.picker( + ui.resolve("pq://MyPersistentQuery/scope/picker_table"), + label="Picker Table" +) + +list_view = ui.list_view( + ui.item_table_source( + "pq://MyPersistentQuery/scope/list_view_table", + key_column="Keys", + label_column="Labels" + ) +) +``` + +## URI Encoding + +If your URI contains any special characters, such as spaces or slashes, you must encode the URI components using standard URL encoding. This is because URIs are often used in web contexts where special characters can cause issues. You can use Python's built-in `urllib.parse.quote` function to encode your URIs. + +```py order=null +from urllib.parse import quote +from deephaven import ui + +# Encode the URI +pq_name = quote("My PQ/with spaces!", safe="") # safe="" will encode the forward slash +t = ui.resolve(f"pq://{pq_name}/scope/table") +``` diff --git a/plugins/ui/docs/sidebar.json b/plugins/ui/docs/sidebar.json index bc04c9c36..0c40e0db1 100644 --- a/plugins/ui/docs/sidebar.json +++ b/plugins/ui/docs/sidebar.json @@ -406,6 +406,10 @@ "label": "toggle_button", "path": "components/toggle_button.md" }, + { + "label": "URI/resolve", + "path": "components/uri.md" + }, { "label": "view", "path": "components/view.md" diff --git a/plugins/ui/docs/snapshots/33b30d272f1930e734245b40bfed92f7.json b/plugins/ui/docs/snapshots/33b30d272f1930e734245b40bfed92f7.json new file mode 100644 index 000000000..0af83a66e --- /dev/null +++ b/plugins/ui/docs/snapshots/33b30d272f1930e734245b40bfed92f7.json @@ -0,0 +1 @@ +{"file":"components/uri.md","objects":{"t":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.elements.UITable","props":{"table":{"__dhElemName":"deephaven.ui.elements.UriElement","props":{"uri":"pq://MyPersistentQuery/scope/table"}},"format_":{"cols":"A","background_color":"salmon"},"showQuickFilters":false,"showGroupingColumn":true,"showSearch":false,"reverse":false}},"state":"{}"}},"picker":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.components.Picker","props":{"align":"start","direction":"bottom","shouldFlip":true,"label":"Picker Table","labelPosition":"top","children":{"__dhElemName":"deephaven.ui.elements.UriElement","props":{"uri":"pq://MyPersistentQuery/scope/picker_table"}}}},"state":"{}"}},"list_view":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.components.ListView","props":{"density":"COMPACT","overflowMode":"truncate","selectionMode":"MULTIPLE","labelColumn":"Labels","keyColumn":"Keys","children":{"__dhElemName":"deephaven.ui.elements.UriElement","props":{"uri":"pq://MyPersistentQuery/scope/list_view_table"}}}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/894c61fa11e650e9541d5dc448179668.json b/plugins/ui/docs/snapshots/894c61fa11e650e9541d5dc448179668.json new file mode 100644 index 000000000..c6ff76553 --- /dev/null +++ b/plugins/ui/docs/snapshots/894c61fa11e650e9541d5dc448179668.json @@ -0,0 +1 @@ +{"file":"components/uri.md","objects":{"t":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.elements.UriElement","props":{"uri":"pq://MyPersistentQuery/scope/table"}},"state":"{}"}},"p":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.elements.UriElement","props":{"uri":"pq://MyPersistentQuery/scope/plot"}},"state":"{}"}},"my_dashboard":{"type":"deephaven.ui.Dashboard","data":{"document":{"__dhElemName":"deephaven.ui.components.Dashboard","props":{"children":{"__dhElemName":"__main__.basic_dashboard","props":{"children":{"__dhElemName":"deephaven.ui.components.Panel","props":{"title":"Table and Plot","direction":"column","alignItems":"start","gap":"size-100","overflow":"auto","padding":"size-100","children":{"__dhElemName":"deephaven.ui.components.Flex","props":{"gap":"size-100","flex":"auto","children":[{"__dhElemName":"deephaven.ui.elements.UriElement","props":{"uri":"pq://MyPersistentQuery/scope/table"}},{"__dhElemName":"deephaven.ui.elements.UriElement","props":{"uri":"pq://MyPersistentQuery/scope/plot"}}]}}}}}}}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/a289b826abe4f81d6a28be044d7501a9.json b/plugins/ui/docs/snapshots/a289b826abe4f81d6a28be044d7501a9.json new file mode 100644 index 000000000..bc48a1eea --- /dev/null +++ b/plugins/ui/docs/snapshots/a289b826abe4f81d6a28be044d7501a9.json @@ -0,0 +1 @@ +{"file":"components/uri.md","objects":{"t":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.elements.UriElement","props":{"uri":"pq://My%20PQ%2Fwith%20spaces%21/scope/table"}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/src/deephaven/ui/components/item_table_source.py b/plugins/ui/src/deephaven/ui/components/item_table_source.py index c26947ed6..a1a092c22 100644 --- a/plugins/ui/src/deephaven/ui/components/item_table_source.py +++ b/plugins/ui/src/deephaven/ui/components/item_table_source.py @@ -7,7 +7,8 @@ from .item import ItemElement from .list_action_group import ListActionGroupElement from .list_action_menu import ListActionMenuElement -from ..elements import Element +from ..elements import Element, resolve +from ..elements.UriElement import UriElement from ..types import ColumnName, Stringable ListViewItem = Union[Stringable, ItemElement] @@ -15,7 +16,7 @@ class ItemTableSource(TypedDict): - table: Table | PartitionedTable + table: Table | PartitionedTable | UriElement key_column: ColumnName | None label_column: ColumnName | None description_column: ColumnName | None @@ -25,7 +26,7 @@ class ItemTableSource(TypedDict): def item_table_source( - table: Table | PartitionedTable, + table: Table | PartitionedTable | UriElement | str, key_column: ColumnName | None = None, label_column: ColumnName | None = None, description_column: ColumnName | None = None, @@ -43,7 +44,7 @@ def item_table_source( Args: table: - The table to use as the source of items. + The table to use as the source of items. May be a UriElement or a URI string. key_column: The column of values to use as item keys. Defaults to the first column. label_column: @@ -66,4 +67,6 @@ def item_table_source( The item table source to pass as a child to a component that supports it. """ + table = resolve(table) if isinstance(table, str) else table + return cast(ItemTableSource, locals()) diff --git a/plugins/ui/src/deephaven/ui/components/table.py b/plugins/ui/src/deephaven/ui/components/table.py index 1766737b6..765af4ee6 100644 --- a/plugins/ui/src/deephaven/ui/components/table.py +++ b/plugins/ui/src/deephaven/ui/components/table.py @@ -3,7 +3,8 @@ from typing import Literal, Any, Optional import logging from deephaven.table import Table -from ..elements import Element +from ..elements import Element, resolve +from ..elements.UriElement import UriElement from .types import AlignSelf, DimensionValue, JustifySelf, LayoutFlex, Position from ..types import ( CellPressCallback, @@ -129,7 +130,7 @@ class table(Element): Customization to how a table is displayed, how it behaves, and listen to UI events. Args: - table: The table to wrap + table: The table to wrap. May be a UriElement or URI string. format_: A formatting rule or list of formatting rules for the table. on_row_press: The callback function to run when a row is clicked. The callback is invoked with the visible row data provided in a dictionary where the @@ -224,7 +225,7 @@ class table(Element): def __init__( self, - table: Table, + table: Table | UriElement | str, *, format_: TableFormat | list[TableFormat] | None = None, on_row_press: RowPressCallback | None = None, @@ -299,6 +300,7 @@ def __init__( ) props = locals() + props["table"] = resolve(table) if isinstance(table, str) else table del props["self"] self._props = props self._key = props.get("key") diff --git a/plugins/ui/src/deephaven/ui/elements/UriElement.py b/plugins/ui/src/deephaven/ui/elements/UriElement.py new file mode 100644 index 000000000..7eaf53c7f --- /dev/null +++ b/plugins/ui/src/deephaven/ui/elements/UriElement.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from .Element import Element, PropsType +from .._internal import RenderContext + + +class UriElement(Element): + """ + Represents a remote object to be fetched by the client. + + Args: + uri: The URI to fetch. + key: An optional key for the element. + """ + + _uri: str + + _key: str | None = None + + def __init__(self, uri: str, key: str | None = None): + self._uri = uri + self._key = key + + @property + def name(self) -> str: + return "deephaven.ui.elements.UriElement" + + @property + def key(self) -> str | None: + return self._key + + def render(self, context: RenderContext) -> PropsType: + return {"uri": self._uri} + + def __eq__(self, other: object) -> bool: + if not isinstance(other, UriElement): + return False + return self._uri == other._uri and self._key == other._key + + +def resolve(uri: str) -> UriElement: + """ + Resolve a URI to a UriNode which can be used to fetch an object on the client from another query. + + Args: + uri: The URI to resolve. + + Returns: + A UriNode with the given URI. + """ + return UriElement(uri) diff --git a/plugins/ui/src/deephaven/ui/elements/__init__.py b/plugins/ui/src/deephaven/ui/elements/__init__.py index 8065f588d..78eef2f88 100644 --- a/plugins/ui/src/deephaven/ui/elements/__init__.py +++ b/plugins/ui/src/deephaven/ui/elements/__init__.py @@ -2,6 +2,7 @@ from .BaseElement import BaseElement from .DashboardElement import DashboardElement from .FunctionElement import FunctionElement +from .UriElement import resolve __all__ = [ "BaseElement", @@ -9,4 +10,5 @@ "Element", "FunctionElement", "PropsType", + "resolve", ] diff --git a/plugins/ui/src/js/package.json b/plugins/ui/src/js/package.json index 178f291ff..d8d696870 100644 --- a/plugins/ui/src/js/package.json +++ b/plugins/ui/src/js/package.json @@ -40,23 +40,23 @@ "react-dom": "^17.0.2" }, "dependencies": { - "@deephaven/chart": "^1.4.0", - "@deephaven/components": "^1.4.0", - "@deephaven/console": "^1.4.0", - "@deephaven/dashboard": "^1.4.0", - "@deephaven/dashboard-core-plugins": "^1.4.0", - "@deephaven/golden-layout": "^1.4.0", + "@deephaven/chart": "^1.5.1", + "@deephaven/components": "^1.5.1", + "@deephaven/console": "^1.5.1", + "@deephaven/dashboard": "^1.5.1", + "@deephaven/dashboard-core-plugins": "^1.5.1", + "@deephaven/golden-layout": "^1.5.1", "@deephaven/grid": "^1.3.0", "@deephaven/icons": "^1.2.0", - "@deephaven/iris-grid": "^1.4.0", - "@deephaven/jsapi-bootstrap": "^1.4.0", - "@deephaven/jsapi-components": "^1.4.0", + "@deephaven/iris-grid": "^1.5.1", + "@deephaven/jsapi-bootstrap": "^1.5.1", + "@deephaven/jsapi-components": "^1.5.1", "@deephaven/jsapi-types": "^1.0.0-dev0.39.6", "@deephaven/jsapi-utils": "^1.4.0", "@deephaven/log": "^1.1.0", - "@deephaven/plugin": "^1.4.0", + "@deephaven/plugin": "^1.5.1", "@deephaven/react-hooks": "^1.2.0", - "@deephaven/redux": "^1.4.0", + "@deephaven/redux": "^1.5.1", "@deephaven/test-utils": "^1.1.0", "@deephaven/utils": "^1.1.0", "@fortawesome/react-fontawesome": "^0.2.0", diff --git a/plugins/ui/src/js/src/elements/ComboBox.tsx b/plugins/ui/src/js/src/elements/ComboBox.tsx index 5fa46037a..b818a1c3a 100644 --- a/plugins/ui/src/js/src/elements/ComboBox.tsx +++ b/plugins/ui/src/js/src/elements/ComboBox.tsx @@ -8,6 +8,8 @@ import { ComboBoxProps as DHComboBoxJSApiProps, } from '@deephaven/jsapi-components'; import { isElementOfType } from '@deephaven/react-hooks'; +import type { dh } from '@deephaven/jsapi-types'; +import { ApiContext } from '@deephaven/jsapi-bootstrap'; import { getSettings, RootState } from '@deephaven/redux'; import { SerializedPickerProps, @@ -15,7 +17,9 @@ import { WrappedDHPickerJSApiProps, } from './hooks/usePickerProps'; import ObjectView from './ObjectView'; -import { useReExportedTable } from './hooks/useReExportedTable'; +import { useObjectViewObject } from './hooks/useObjectViewObject'; +import UriObjectView from './UriObjectView'; +import { getErrorShortMessage } from '../widget/WidgetErrorUtils'; export function ComboBox( props: SerializedPickerProps< @@ -25,15 +29,43 @@ export function ComboBox( const settings = useSelector(getSettings); const { children, ...pickerProps } = usePickerProps(props); - const isObjectView = isElementOfType(children, ObjectView); - const table = useReExportedTable(children); + const isObjectView = + isElementOfType(children, ObjectView) || + isElementOfType(children, UriObjectView); + const { + widget: table, + api, + isLoading, + error, + } = useObjectViewObject(children); if (isObjectView) { - return ( - table && ( + if (error != null) { + const message = getErrorShortMessage(error); + return ( + + {[]} + + ); + } + if (isLoading || table == null || api == null) { + return ( // eslint-disable-next-line react/jsx-props-no-spreading + + {[]} + + ); + } + return ( + + {/* eslint-disable-next-line react/jsx-props-no-spreading */} - ) + ); } diff --git a/plugins/ui/src/js/src/elements/ListView.tsx b/plugins/ui/src/js/src/elements/ListView.tsx index a9db083df..406605583 100644 --- a/plugins/ui/src/js/src/elements/ListView.tsx +++ b/plugins/ui/src/js/src/elements/ListView.tsx @@ -3,33 +3,62 @@ import { isElementOfType } from '@deephaven/react-hooks'; import { getSettings, RootState } from '@deephaven/redux'; import { ListView as DHListView } from '@deephaven/components'; import { ListView as DHListViewJSApi } from '@deephaven/jsapi-components'; +import type { dh } from '@deephaven/jsapi-types'; +import { ApiContext } from '@deephaven/jsapi-bootstrap'; import { assertNotNull } from '@deephaven/utils'; import { SerializedListViewProps, useListViewProps, } from './hooks/useListViewProps'; import ObjectView from './ObjectView'; -import useReExportedTable from './hooks/useReExportedTable'; +import { useObjectViewObject } from './hooks/useObjectViewObject'; +import UriObjectView from './UriObjectView'; +import WidgetErrorView from '../widget/WidgetErrorView'; export function ListView(props: SerializedListViewProps): JSX.Element | null { const settings = useSelector(getSettings); const { children, ...listViewProps } = useListViewProps(props); - const isObjectView = isElementOfType(children, ObjectView); - const table = useReExportedTable(children); + const isObjectView = + isElementOfType(children, ObjectView) || + isElementOfType(children, UriObjectView); + const { + widget: table, + api, + isLoading, + error, + } = useObjectViewObject(children); assertNotNull(children, 'Children must be defined for list_view.'); if (isObjectView) { - return ( - table && ( - } + > + {[]} + + ); + } + if (isLoading || table == null || api == null) { + return ( + - ) + loadingState="loading" + > + {[]} + + ); + } + return ( + + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + + ); } diff --git a/plugins/ui/src/js/src/elements/ObjectView.tsx b/plugins/ui/src/js/src/elements/ObjectView.tsx index 6cb3024da..9dee364a9 100644 --- a/plugins/ui/src/js/src/elements/ObjectView.tsx +++ b/plugins/ui/src/js/src/elements/ObjectView.tsx @@ -12,7 +12,7 @@ export type ObjectViewProps = { function ObjectView(props: ObjectViewProps): JSX.Element { const { object, __dhId } = props; - log.info('Object is', object); + log.debug('Object is', object); const { type } = object; const fetch = useCallback(async () => { diff --git a/plugins/ui/src/js/src/elements/Picker.tsx b/plugins/ui/src/js/src/elements/Picker.tsx index 0d65bf6b3..c54b6903f 100644 --- a/plugins/ui/src/js/src/elements/Picker.tsx +++ b/plugins/ui/src/js/src/elements/Picker.tsx @@ -8,6 +8,8 @@ import { PickerProps as DHPickerJSApiProps, } from '@deephaven/jsapi-components'; import { isElementOfType } from '@deephaven/react-hooks'; +import type { dh } from '@deephaven/jsapi-types'; +import { ApiContext } from '@deephaven/jsapi-bootstrap'; import { getSettings, RootState } from '@deephaven/redux'; import { SerializedPickerProps, @@ -15,7 +17,9 @@ import { WrappedDHPickerJSApiProps, } from './hooks/usePickerProps'; import ObjectView from './ObjectView'; -import useReExportedTable from './hooks/useReExportedTable'; +import { useObjectViewObject } from './hooks/useObjectViewObject'; +import UriObjectView from './UriObjectView'; +import { getErrorShortMessage } from '../widget/WidgetErrorUtils'; export function Picker( props: SerializedPickerProps< @@ -25,15 +29,39 @@ export function Picker( const settings = useSelector(getSettings); const { children, ...pickerProps } = usePickerProps(props); - const isObjectView = isElementOfType(children, ObjectView); - const table = useReExportedTable(children); + const isObjectView = + isElementOfType(children, ObjectView) || + isElementOfType(children, UriObjectView); + const { + widget: table, + api, + isLoading, + error, + } = useObjectViewObject(children); if (isObjectView) { - return ( - table && ( + if (error != null) { + const message = getErrorShortMessage(error); + return ( + // eslint-disable-next-line react/jsx-props-no-spreading + + {[]} + + ); + } + if (isLoading || table == null || api == null) { + return ( // eslint-disable-next-line react/jsx-props-no-spreading + + {[]} + + ); + } + return ( + + {/* eslint-disable-next-line react/jsx-props-no-spreading */} - ) + ); } diff --git a/plugins/ui/src/js/src/elements/UITable/UITable.tsx b/plugins/ui/src/js/src/elements/UITable/UITable.tsx index 24d0f00d1..e3a897f3a 100644 --- a/plugins/ui/src/js/src/elements/UITable/UITable.tsx +++ b/plugins/ui/src/js/src/elements/UITable/UITable.tsx @@ -22,6 +22,7 @@ import { import { ColorValues, colorValueStyle, + LoadingOverlay, resolveCssVariablesInRecord, useStyleProps, useTheme, @@ -34,8 +35,7 @@ import { useTablePlugin, } from '@deephaven/dashboard-core-plugins'; import { useLayoutManager, useListener } from '@deephaven/dashboard'; -import { useApi } from '@deephaven/jsapi-bootstrap'; -import type { dh as DhType } from '@deephaven/jsapi-types'; +import { type dh as DhType } from '@deephaven/jsapi-types'; import Log from '@deephaven/log'; import { getSettings, RootState } from '@deephaven/redux'; import { GridMouseHandler, GridRange, GridState } from '@deephaven/grid'; @@ -56,6 +56,8 @@ import UITableContextMenuHandler, { } from './UITableContextMenuHandler'; import UITableModel, { makeUiTableModel } from './UITableModel'; import { UITableLayoutHints } from './JsTableProxy'; +import { useExportedObject } from '../hooks'; +import WidgetErrorView from '../../widget/WidgetErrorView'; const log = Log.module('@deephaven/js-plugin-ui/UITable'); @@ -90,14 +92,14 @@ function useThrowError(): [ function useUITableModel({ dh, databars, - exportedTable, + table, layoutHints, format, columnDisplayNames, }: { - dh: typeof DhType; + dh: typeof DhType | null; databars: DatabarConfig[]; - exportedTable: DhType.WidgetExportedObject; + table: DhType.Table | null; layoutHints: UITableLayoutHints; format: FormattingRule[]; columnDisplayNames: Record; @@ -109,9 +111,10 @@ function useUITableModel({ useEffect(() => { let isCancelled = false; async function loadModel() { + if (dh == null || table == null) { + return; + } try { - const reexportedTable = await exportedTable.reexport(); - const table = (await reexportedTable.fetch()) as DhType.Table; const newModel = await makeUiTableModel( dh, table, @@ -141,7 +144,7 @@ function useUITableModel({ }, [ databars, dh, - exportedTable, + table, layoutHints, format, columnDisplayNames, @@ -222,11 +225,21 @@ export function UITable({ viewStyleProps // Needed so spectrum applies styles from view instead of base which doesn't have padding ); - const dh = useApi(); const { eventHub } = useLayoutManager(); + + const { + widget: table, + api: dh, + isLoading, + error, + } = useExportedObject(exportedTable); + const theme = useTheme(); const [irisGrid, setIrisGrid] = useState(null); - const utils = useMemo(() => new IrisGridUtils(dh), [dh]); + const utils = useMemo( + () => (dh != null ? new IrisGridUtils(dh) : null), + [dh] + ); const settings = useSelector(getSettings); const format = useMemo(() => ensureArray(formatProp), [formatProp]); const layoutHints = useMemo( @@ -285,7 +298,7 @@ export function UITable({ const model = useUITableModel({ dh, databars, - exportedTable, + table, layoutHints, format, columnDisplayNames, @@ -318,7 +331,7 @@ export function UITable({ } = useTablePlugin({ model, irisGridRef, - irisGridUtils: utils, + irisGridUtils: utils ?? undefined, selectedRanges: selection, }); @@ -343,7 +356,7 @@ export function UITable({ ); const initialHydratedState = useMemo(() => { - if (model && initialState.current != null) { + if (model && utils && initialState.current != null) { return { ...utils.hydrateIrisGridState(model, initialState.current), ...IrisGridUtils.hydrateGridState(model, initialState.current), @@ -352,7 +365,7 @@ export function UITable({ }, [model, utils]); const hydratedSorts = useMemo(() => { - if (sorts !== undefined && columns !== undefined) { + if (utils && sorts !== undefined && columns !== undefined) { log.debug('Hydrating sorts', sorts); return utils.hydrateSort(columns, sorts); @@ -363,6 +376,7 @@ export function UITable({ const hydratedQuickFilters = useMemo(() => { if ( quickFilters !== undefined && + utils && model !== undefined && columns !== undefined ) { @@ -443,7 +457,7 @@ export function UITable({ const mouseHandlers = useMemo( () => - model && irisGrid + model && dh && irisGrid ? ([ new UITableMouseHandler( model, @@ -618,30 +632,34 @@ export function UITable({ handleClearAllFilters ); - return model ? ( + return (
- setIrisGrid(ref)} - model={model} - onStateChange={onStateChange} - onSelectionChanged={debouncedHandleSelectionChanged} - columnSelectionValidator={columnSelectionValidator} - isSelectingColumn={isSelectingColumn} - onColumnSelected={onColumnSelected} - onDataSelected={onDataSelected} - customFilters={customFilters} - // eslint-disable-next-line react/jsx-props-no-spreading - {...mergedIrisGridProps} - inputFilters={inputFilters} - > - {Plugin} - + {error != null && } + {error == null && !model && } + {error == null && model && ( + setIrisGrid(ref)} + model={model} + onStateChange={onStateChange} + onSelectionChanged={debouncedHandleSelectionChanged} + columnSelectionValidator={columnSelectionValidator} + isSelectingColumn={isSelectingColumn} + onColumnSelected={onColumnSelected} + onDataSelected={onDataSelected} + customFilters={customFilters} + // eslint-disable-next-line react/jsx-props-no-spreading + {...mergedIrisGridProps} + inputFilters={inputFilters} + > + {Plugin} + + )}
- ) : null; + ); } UITable.displayName = 'TableElementView'; diff --git a/plugins/ui/src/js/src/elements/UITable/UITableUtils.ts b/plugins/ui/src/js/src/elements/UITable/UITableUtils.ts index 4137ca6bd..6a0f9dd24 100644 --- a/plugins/ui/src/js/src/elements/UITable/UITableUtils.ts +++ b/plugins/ui/src/js/src/elements/UITable/UITableUtils.ts @@ -21,6 +21,7 @@ import { } from '../utils/ElementUtils'; import { ELEMENT_NAME, type ElementName } from '../model/ElementConstants'; import { type ResolvableUIContextItem } from './UITableContextMenuHandler'; +import UriExportedObject from '../../widget/UriExportedObject'; export type CellData = { type: string; @@ -72,7 +73,7 @@ type UIAggregation = { }; export type UITableProps = StyleProps & { - table: dh.WidgetExportedObject; + table: dh.WidgetExportedObject | UriExportedObject; format_?: FormattingRule | FormattingRule[]; onCellPress?: (data: CellData) => void; onCellDoublePress?: (data: CellData) => void; diff --git a/plugins/ui/src/js/src/elements/UriObjectView.tsx b/plugins/ui/src/js/src/elements/UriObjectView.tsx new file mode 100644 index 000000000..ff443c7e3 --- /dev/null +++ b/plugins/ui/src/js/src/elements/UriObjectView.tsx @@ -0,0 +1,69 @@ +import React, { useCallback, useMemo } from 'react'; +import Log from '@deephaven/log'; +import { LoadingOverlay } from '@deephaven/components'; +import { isWidgetPlugin, usePlugins } from '@deephaven/plugin'; +import { ApiContext, useWidget } from '@deephaven/jsapi-bootstrap'; +import WidgetErrorView from '../widget/WidgetErrorView'; +import { getWidgetType } from './hooks'; + +const log = Log.module('@deephaven/js-plugin-ui/UriObjectView'); + +export type UriObjectViewProps = { + uri: string; + __dhId?: string; +}; + +function UriObjectView(props: UriObjectViewProps): JSX.Element { + const { uri, __dhId } = props; + log.debug(`Fetching object for URI: ${uri}`); + + const { widget, api, error } = useWidget(uri); + + const widgetType = useMemo( + () => (widget && api ? getWidgetType(widget, api) : null), + [api, widget] + ); + + const fetch = useCallback(async () => widget, [widget]); + + const plugins = usePlugins(); + + const plugin = useMemo( + () => + widgetType == null + ? null + : [...plugins.values()] + .filter(isWidgetPlugin) + .find(p => [p.supportedTypes].flat().includes(widgetType)), + [plugins, widgetType] + ); + + if (error != null) { + return ; + } + + if (widget == null || api == null) { + return ; + } + + if (plugin != null) { + const Component = plugin.component; + return ( + + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + + + ); + } + + log.warn(`Unknown object type ${widgetType} for URI ${uri}`); + return ( +
+ Unknown object type {widgetType} from URI {uri} +
+ ); +} + +UriObjectView.displayName = 'UriObjectView'; + +export default UriObjectView; diff --git a/plugins/ui/src/js/src/elements/hooks/index.ts b/plugins/ui/src/js/src/elements/hooks/index.ts index b1cb8fed1..5addbb081 100644 --- a/plugins/ui/src/js/src/elements/hooks/index.ts +++ b/plugins/ui/src/js/src/elements/hooks/index.ts @@ -7,7 +7,7 @@ export * from './useKeyboardEventCallback'; export * from './useListViewProps'; export * from './usePickerProps'; export * from './usePressEventCallback'; -export * from './useReExportedTable'; +export * from './useExportedObject'; export * from './useSelectionProps'; export * from './useTimeComponentProps'; export * from './useTimeValueMemo'; diff --git a/plugins/ui/src/js/src/elements/hooks/useExportedObject.ts b/plugins/ui/src/js/src/elements/hooks/useExportedObject.ts new file mode 100644 index 000000000..6b1fab46d --- /dev/null +++ b/plugins/ui/src/js/src/elements/hooks/useExportedObject.ts @@ -0,0 +1,124 @@ +import { useMemo } from 'react'; +import { + useApi, + useWidget, + type WidgetTypes, +} from '@deephaven/jsapi-bootstrap'; +import type { dh } from '@deephaven/jsapi-types'; +import { TableUtils } from '@deephaven/jsapi-utils'; +import { useWidgetClose } from '@deephaven/jsapi-components'; +import { usePromiseFactory } from '@deephaven/react-hooks'; +import { + fetchReexportedObject, + isExportedObject, + isUriExportedObject, +} from '../utils'; +import type UriExportedObject from '../../widget/UriExportedObject'; + +export interface ResolvedExportedObject { + widget: T | null; + api: typeof dh | null; + error: NonNullable | null; + isLoading: boolean; +} + +/** + * Get the type of a widget. + * @param widget The widget to check. + * @param api The JS API instance to use. + * @returns The widget type. Throws if the type cannot be determined. + */ +export function getWidgetType(widget: dh.Widget, api: typeof dh): string { + if (widget.type != null) { + return widget.type; + } + + if ('charts' in widget) { + return api.VariableType.FIGURE; + } + + if ('columns' in widget) { + if (TableUtils.isTreeTable(widget)) { + return api.VariableType.TREETABLE; + } + + if (TableUtils.isPartitionedTable(widget)) { + return api.VariableType.PARTITIONEDTABLE; + } + + return api.VariableType.TABLE; + } + + throw new Error(`Unknown widget type for widget: ${JSON.stringify(widget)}`); +} + +export function useExportedObject( + descriptor: + | dh.WidgetExportedObject + | dh.ide.VariableDescriptor + | UriExportedObject + | null +): ResolvedExportedObject { + const { + widget: descriptorWidget, + api: descriptorApi, + error: descriptorError, + } = useWidget( + isUriExportedObject(descriptor) + ? descriptor.uri + : // useWidget will gracefully handle the error if it's not actually this type, + // but we don't want to modify the hook signature to accept unknown + (descriptor as dh.ide.VariableDescriptor) + ); + + useWidgetClose(descriptorWidget); + + const defaultApi = useApi(); + + const { + data: exportedWidget, + error: exportedWidgetError, + isLoading, + } = usePromiseFactory(fetchReexportedObject, [ + isExportedObject(descriptor) && !isUriExportedObject(descriptor) + ? descriptor + : null, + ]); + + useWidgetClose(exportedWidget); + + // If this was exported as part of the dh.ui widget + const isExportedWidget = + isExportedObject(descriptor) && !isUriExportedObject(descriptor); + + const exportedObject = useMemo( + () => + isExportedWidget + ? { + widget: exportedWidget, + api: defaultApi, + error: exportedWidgetError, + isLoading, + } + : { + widget: descriptorWidget, + api: descriptorApi ?? defaultApi, + error: descriptorError, + isLoading: descriptorWidget == null && descriptorError == null, + }, + [ + defaultApi, + descriptorApi, + descriptorError, + descriptorWidget, + exportedWidget, + exportedWidgetError, + isExportedWidget, + isLoading, + ] + ); + + return exportedObject; +} + +export default useExportedObject; diff --git a/plugins/ui/src/js/src/elements/hooks/useObjectViewObject.ts b/plugins/ui/src/js/src/elements/hooks/useObjectViewObject.ts new file mode 100644 index 000000000..e91609b7a --- /dev/null +++ b/plugins/ui/src/js/src/elements/hooks/useObjectViewObject.ts @@ -0,0 +1,33 @@ +import { ReactNode } from 'react'; +import type { dh } from '@deephaven/jsapi-types'; +import { isElementOfType } from '@deephaven/react-hooks'; +import { type WidgetTypes } from '@deephaven/jsapi-bootstrap'; +import ObjectView from '../ObjectView'; +import UriObjectView from '../UriObjectView'; +import useExportedObject, { ResolvedExportedObject } from './useExportedObject'; +import UriExportedObject from '../../widget/UriExportedObject'; + +/** + * Hook to fetch the object and JS API from an ObjectView or UriObjectView. + * If the node is not an ObjectView or UriObjectView, the returned object will contain an error. + * @param node The ReactNode to check and fetch the table from. + * @returns The fetched table or null if not applicable. + */ +export function useObjectViewObject( + node: ReactNode +): ResolvedExportedObject { + const maybeObjectView = isElementOfType(node, ObjectView) + ? node.props.object + : null; + const maybeUriObjectView = isElementOfType(node, UriObjectView) + ? new UriExportedObject(node.props.uri) + : null; + + const exportedObjectProp = maybeObjectView || maybeUriObjectView; + + const exportedObject = useExportedObject(exportedObjectProp); + + return exportedObject; +} + +export default useObjectViewObject; diff --git a/plugins/ui/src/js/src/elements/hooks/useReExportedTable.ts b/plugins/ui/src/js/src/elements/hooks/useReExportedTable.ts deleted file mode 100644 index 9123ba970..000000000 --- a/plugins/ui/src/js/src/elements/hooks/useReExportedTable.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ReactNode } from 'react'; -import { useTableClose } from '@deephaven/jsapi-components'; -import type { dh } from '@deephaven/jsapi-types'; -import { isElementOfType, usePromiseFactory } from '@deephaven/react-hooks'; -import ObjectView from '../ObjectView'; -import { fetchReexportedTable } from '../utils'; - -export function useReExportedTable(node: ReactNode): dh.Table | null { - const isObjectView = isElementOfType(node, ObjectView); - - const maybeExportedTable = - isObjectView && node.props.object.type === 'Table' - ? node.props.object - : null; - - const { data: table } = usePromiseFactory(fetchReexportedTable, [ - maybeExportedTable, - ]); - - useTableClose(table); - - return table; -} - -export default useReExportedTable; diff --git a/plugins/ui/src/js/src/elements/model/ElementConstants.ts b/plugins/ui/src/js/src/elements/model/ElementConstants.ts index 2c9528a98..97adbfd74 100644 --- a/plugins/ui/src/js/src/elements/model/ElementConstants.ts +++ b/plugins/ui/src/js/src/elements/model/ElementConstants.ts @@ -14,6 +14,7 @@ const uiElementName = (name: T) => export const ELEMENT_NAME = { /** Elements */ uiTable: uiElementName('UITable'), + uri: uiElementName('UriElement'), /** Layout Components */ column: uiComponentName('Column'), diff --git a/plugins/ui/src/js/src/elements/utils/ElementUtils.test.tsx b/plugins/ui/src/js/src/elements/utils/ElementUtils.test.tsx index 4b7f16256..2f1f30da5 100644 --- a/plugins/ui/src/js/src/elements/utils/ElementUtils.test.tsx +++ b/plugins/ui/src/js/src/elements/utils/ElementUtils.test.tsx @@ -4,7 +4,7 @@ import type { dh } from '@deephaven/jsapi-types'; import { TestUtils } from '@deephaven/test-utils'; import { ELEMENT_KEY, - fetchReexportedTable, + fetchReexportedObject, isPrimitive, wrapElementChildren, } from './ElementUtils'; @@ -13,9 +13,9 @@ import { ELEMENT_NAME } from '../model/ElementConstants'; const { asMock, createMockProxy } = TestUtils; -describe('fetchReexportedTable', () => { +describe('fetchReexportedObject', () => { it('should return null for null object', async () => { - const actual = await fetchReexportedTable(null); + const actual = await fetchReexportedObject(null); expect(actual).toBeNull(); }); @@ -28,7 +28,7 @@ describe('fetchReexportedTable', () => { const exported = createMockProxy(); asMock(exported.reexport).mockResolvedValue(reexported); - const actual = await fetchReexportedTable(exported); + const actual = await fetchReexportedObject(exported); expect(actual).toBe(table); }); }); diff --git a/plugins/ui/src/js/src/elements/utils/ElementUtils.tsx b/plugins/ui/src/js/src/elements/utils/ElementUtils.tsx index e0258a88b..72e8049ee 100644 --- a/plugins/ui/src/js/src/elements/utils/ElementUtils.tsx +++ b/plugins/ui/src/js/src/elements/utils/ElementUtils.tsx @@ -3,6 +3,8 @@ import { Text } from '@deephaven/components'; import type { dh } from '@deephaven/jsapi-types'; import { ELEMENT_NAME } from '../model/ElementConstants'; import ObjectView from '../ObjectView'; +import UriExportedObject from '../../widget/UriExportedObject'; +import UriObjectView from '../UriObjectView'; export const CALLABLE_KEY = '__dhCbid'; export const OBJECT_KEY = '__dhObid'; @@ -18,6 +20,8 @@ export type ObjectNode = { [OBJECT_KEY]: number; }; +export type UriNode = ElementNode; + /** * Describes an element that can be rendered in the UI. * Extend this type with stricter rules on the element key type to provide types. @@ -25,15 +29,22 @@ export type ObjectNode = { */ export type ElementNode< K extends string = string, - P extends Record = Record, -> = { - /** - * The type of this element. Can be something like `deephaven.ui.components.Panel`, or - * a custom component type defined by the user in their plugin. - */ - [ELEMENT_KEY]: K; - props?: P; -}; + P extends Record | undefined = + | Record + | undefined, +> = P extends undefined + ? { + /** + * The type of this element. Can be something like `deephaven.ui.components.Panel`, or + * a custom component type defined by the user in their plugin. + */ + [ELEMENT_KEY]: K; + props?: P; + } + : { + [ELEMENT_KEY]: K; + props: P; + }; export type ElementNodeWithChildren< K extends string = string, @@ -46,21 +57,25 @@ export function isObjectNode(obj: unknown): obj is ObjectNode { return obj != null && typeof obj === 'object' && OBJECT_KEY in obj; } +export function isUriNode(obj: unknown): obj is UriNode { + return isElementNode(obj, ELEMENT_NAME.uri); +} + /** - * Re-export and fetch the table from the given exported object. + * Re-export and fetch the object from the given exported object. * @param exportedObject - * @returns Promise that resolves to the table or null if given + * @returns Promise that resolves to the object or null if given * object is null */ -export async function fetchReexportedTable( +export async function fetchReexportedObject( exportedObject: dh.WidgetExportedObject | null -): Promise { +): Promise { if (exportedObject == null) { return null; } - const reexportedTable = await exportedObject.reexport(); - return reexportedTable.fetch(); + const reexportedObject = await exportedObject.reexport(); + return reexportedObject.fetch(); } /** @@ -95,6 +110,10 @@ export function isExportedObject(obj: unknown): obj is dh.WidgetExportedObject { ); } +export function isUriExportedObject(obj: unknown): obj is UriExportedObject { + return obj instanceof UriExportedObject; +} + /** * Typeguard for primitive values. * @param value The value to check @@ -190,15 +209,14 @@ export function wrapElementChildren(element: ElementNode): ElementNode { const wrappedChildren = children.map(child => { // Exported objects need to be converted to `ObjectView` to be rendered - if (isExportedObject(child)) { + if (isExportedObject(child) || isUriExportedObject(child)) { const key = getChildKey(child.type); - return ( - + // eslint-disable-next-line no-underscore-dangle + const dhId = `${element.props?.__dhId}/${key}`; + return isUriExportedObject(child) ? ( + + ) : ( + ); } diff --git a/plugins/ui/src/js/src/layout/PortalPanel.tsx b/plugins/ui/src/js/src/layout/PortalPanel.tsx index 642d7b090..e8ff7f3d4 100644 --- a/plugins/ui/src/js/src/layout/PortalPanel.tsx +++ b/plugins/ui/src/js/src/layout/PortalPanel.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef } from 'react'; import { DashboardPanelProps } from '@deephaven/dashboard'; -import { Panel } from '@deephaven/dashboard-core-plugins'; +import { CorePanel } from '@deephaven/dashboard-core-plugins'; import { emitPortalClosed, emitPortalOpened } from './PortalPanelEvent'; /** @@ -26,9 +26,9 @@ function PortalPanel({ }, [glContainer, glEventHub]); return ( - +
- + ); } diff --git a/plugins/ui/src/js/src/widget/UriExportedObject.ts b/plugins/ui/src/js/src/widget/UriExportedObject.ts new file mode 100644 index 000000000..dd6a907bb --- /dev/null +++ b/plugins/ui/src/js/src/widget/UriExportedObject.ts @@ -0,0 +1,10 @@ +export default class UriExportedObject { + static TYPE = 'deephaven.ui.URI'; + + constructor(readonly uri: string) {} + + // eslint-disable-next-line class-methods-use-this + get type(): string { + return UriExportedObject.TYPE; + } +} diff --git a/plugins/ui/src/js/src/widget/WidgetHandler.tsx b/plugins/ui/src/js/src/widget/WidgetHandler.tsx index 221fc45e5..cfd8e9649 100644 --- a/plugins/ui/src/js/src/widget/WidgetHandler.tsx +++ b/plugins/ui/src/js/src/widget/WidgetHandler.tsx @@ -29,6 +29,7 @@ import { isCallableNode, isElementNode, isObjectNode, + isUriNode, } from '../elements/utils/ElementUtils'; import { ReadonlyWidgetData, @@ -53,6 +54,7 @@ import WidgetStatusContext, { import WidgetErrorView from './WidgetErrorView'; import ReactPanel from '../layout/ReactPanel'; import Toast, { TOAST_EVENT } from '../events/Toast'; +import UriExportedObject from './UriExportedObject'; const log = Log.module('@deephaven/js-plugin-ui/WidgetHandler'); @@ -209,6 +211,8 @@ function WidgetHandler({ [error, initialData, widgetDescriptor] ); + const [uriObjectMap] = useState>(new Map()); + const renderDocument = useCallback( /** * Iterates through a document and renders it with the appropriate components. Returns the original object/arrays if there are no changes. @@ -227,6 +231,7 @@ function WidgetHandler({ // Keep track of exported objects and callables that are no longer in use after this render. // We close those objects that are no longer referenced, as they will never be referenced again. const deadObjectMap = new Map(exportedObjectMap.current); + const deadUriMap = new Map(uriObjectMap); const deadCallableMap = new Map(renderedCallableMap.current); const hydratedDocument = transformNode( doc, @@ -260,6 +265,19 @@ function WidgetHandler({ deadObjectMap.delete(objectKey); return exportedObject; } + + // If this is the root element, we want to return the element version instead + if (isUriNode(value) && key !== '') { + const { uri } = value.props; + let uriExportedObject = uriObjectMap.get(uri); + if (uriExportedObject == null) { + uriExportedObject = new UriExportedObject(uri); + uriObjectMap.set(uri, uriExportedObject); + } + deadUriMap.delete(uri); + return uriExportedObject; + } + if (isElementNode(value)) { // Replace the elements node with the Component it maps to try { @@ -282,6 +300,12 @@ function WidgetHandler({ exportedObjectMap.current.delete(objectKey); }); + // Cleanup any URI objects that are no longer referenced + deadUriMap.forEach((deadUriObject, uri) => { + log.debug('Cleaning up dead URI object', uri); + uriObjectMap.delete(uri); + }); + // Close any callables that are no longer referenced deadCallableMap.forEach((deadCallable, callableId) => { log.debug2('Callable no longer rendered:', callableId); @@ -304,6 +328,7 @@ function WidgetHandler({ id, renderEmptyDocument, callableFinalizationRegistry, + uriObjectMap, pluginsElementMap, ] ); diff --git a/plugins/ui/src/js/src/widget/WidgetUtils.tsx b/plugins/ui/src/js/src/widget/WidgetUtils.tsx index 0d7ad7719..1597c7946 100644 --- a/plugins/ui/src/js/src/widget/WidgetUtils.tsx +++ b/plugins/ui/src/js/src/widget/WidgetUtils.tsx @@ -103,6 +103,7 @@ import { UITable, Tabs, } from '../elements'; +import UriObjectView from '../elements/UriObjectView'; export const WIDGET_ELEMENT = 'deephaven.ui.Element'; export const DASHBOARD_ELEMENT = 'deephaven.ui.Dashboard'; @@ -125,6 +126,7 @@ const log = Log.module('@deephaven/js-plugin-ui/WidgetUtils'); export const elementComponentMap: Record, unknown> = { // Elements [ELEMENT_NAME.uiTable]: UITable, + [ELEMENT_NAME.uri]: UriObjectView, // Layout [ELEMENT_NAME.column]: Column, @@ -207,7 +209,9 @@ export const elementComponentMap: Record, unknown> = { [ELEMENT_NAME.view]: View, } as const satisfies Record, unknown>; -export function getComponentTypeForElement

>( +export function getComponentTypeForElement< + P extends Record | undefined, +>( element: ElementNode, elementMap: ElementMap = EMPTY_MAP ): ComponentType

| null { diff --git a/plugins/ui/src/ui.schema.json b/plugins/ui/src/ui.schema.json index e4fa98d86..bc0e2a164 100644 --- a/plugins/ui/src/ui.schema.json +++ b/plugins/ui/src/ui.schema.json @@ -46,6 +46,12 @@ "__dhObid": { "type": "number" } } }, + "uri": { + "type": "object", + "properties": { + "__dhUri": { "type": "string" } + } + }, "documentUpdatedParams": { "type": "array", "prefixItems": [{ "type": "string" }, { "type": "string" }], diff --git a/plugins/ui/test/deephaven/ui/test_utils.py b/plugins/ui/test/deephaven/ui/test_utils.py index d2f28c18c..31697a2da 100644 --- a/plugins/ui/test/deephaven/ui/test_utils.py +++ b/plugins/ui/test/deephaven/ui/test_utils.py @@ -1,4 +1,5 @@ import unittest +from typing import cast from .BaseTest import BaseTestCase @@ -18,7 +19,7 @@ unpack_item_table_source, ) from deephaven.ui.types import Undefined -from deephaven.ui import item_table_source +from deephaven.ui import item_table_source, resolve def my_test_func(): @@ -354,6 +355,7 @@ def test_convert_date_for_labeled_value(self): self.assertEqual(local_date, "2035-01-31") def test_unpack_item_table_source(self): + from deephaven.table import Table children = ("table",) props = { @@ -371,7 +373,7 @@ def test_unpack_item_table_source(self): ) item_data_source = item_table_source( - table="table", key_column="key", actions="actions" + table=cast(Table, {"table": "foo"}), key_column="key", actions="actions" ) children = (item_data_source,) @@ -379,7 +381,49 @@ def test_unpack_item_table_source(self): "test": "foo", } - expected_children = ("table",) + expected_children = ({"table": "foo"},) + + expected_props = { + "test": "foo", + "key_column": "key", + } + + self.assertTupleEqual( + unpack_item_table_source(children, props, {"table", "key_column"}), + (expected_children, expected_props), + ) + + item_data_source = item_table_source( + table="tableURI", key_column="key", actions="actions" + ) + + children = (item_data_source,) + props = { + "test": "foo", + } + + expected_children = (resolve("tableURI"),) + + expected_props = { + "test": "foo", + "key_column": "key", + } + + self.assertTupleEqual( + unpack_item_table_source(children, props, {"table", "key_column"}), + (expected_children, expected_props), + ) + + item_data_source = item_table_source( + table=resolve("tableURI"), key_column="key", actions="actions" + ) + + children = (item_data_source,) + props = { + "test": "foo", + } + + expected_children = (resolve("tableURI"),) expected_props = { "test": "foo", diff --git a/tsconfig.json b/tsconfig.json index 298ce2ca3..e515199a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "plugins/**/**.jsx", "tests/**/**.ts" ], - "exclude": ["plugins/*/src/js/dist/**/*"], + "exclude": ["**/node_modules", "plugins/*/src/js/dist/**/*"], "watchOptions": { "excludeDirectories": ["**/node_modules", "**/dist"] }