From e3af9f53560bfcf89caf19983a4419fec1552e02 Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Tue, 5 Mar 2024 10:11:08 -0600 Subject: [PATCH 1/8] feat: UI Picker JS (#333) - @adobe/react-spectrum upgrade to ^3.34.1 - DH version bump to ^0.67.0 - Split out element constants from various utils into centralized location + simplified the component type mapping - Picker React component + event handler serialization - Fixed minor issue in `update-dh-packages` script **Test Example** Shows primitive, Item, and Section children + event handlers ``` import deephaven.ui as ui from deephaven.ui import use_state, use_callback, use_effect @ui.component def picker(): value, set_value = use_state('') def handle_change(v): print(v) set_value(v) on_selection_change = use_callback(handle_change, []) return ui.fragment( ui.picker( label="Picker Example", selected_key=value, on_focus=lambda e : print('on_focus:', e), on_blur=lambda e : print('on_blur:', e), on_key_down=lambda e : print('on_key_down:', e), on_key_up=lambda e : print('on_key_up:', e), on_selection_change=on_selection_change, children = [ 'Text 1', 999, True, False, 'Really long text items should be truncated', ui.item('Item'), ui.section( 'Subtext 1', 'Subtext 2', title='Section' ), ], ) ) pick = picker() ``` resolves #292 --- .gitignore | 1 + docker/data/storage/notebooks/DEMO.md | 39 + package-lock.json | 884 ++++++++---------- package.json | 2 +- plugins/ui/examples/README.md | 39 + plugins/ui/src/js/package.json | 30 +- .../src/js/src/elements/ElementConstants.ts | 42 + .../ui/src/js/src/elements/ElementUtils.ts | 24 - .../src/js/src/elements/HTMLElementUtils.ts | 6 +- .../src/js/src/elements/IconElementUtils.ts | 6 +- plugins/ui/src/js/src/elements/Picker.tsx | 15 + .../ui/src/js/src/elements/UITableUtils.tsx | 5 +- .../ui/src/js/src/elements/usePickerProps.ts | 48 + .../ui/src/js/src/layout/LayoutUtils.test.tsx | 10 +- plugins/ui/src/js/src/layout/LayoutUtils.tsx | 24 +- .../js/src/widget/DocumentHandler.test.tsx | 8 +- .../ui/src/js/src/widget/WidgetUtils.test.tsx | 71 ++ plugins/ui/src/js/src/widget/WidgetUtils.tsx | 78 +- tools/update-dh-packages.mjs | 2 +- 19 files changed, 723 insertions(+), 611 deletions(-) create mode 100644 plugins/ui/src/js/src/elements/ElementConstants.ts create mode 100644 plugins/ui/src/js/src/elements/Picker.tsx create mode 100644 plugins/ui/src/js/src/elements/usePickerProps.ts create mode 100644 plugins/ui/src/js/src/widget/WidgetUtils.test.tsx diff --git a/.gitignore b/.gitignore index e3c012457..812a2add3 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ tsconfig.tsbuildinfo .stylelintcache # Ignore the test reports +coverage/ junit.xml # Allow for local overrides of docker-compose.yml. https://docs.docker.com/compose/multiple-compose-files/merge/ diff --git a/docker/data/storage/notebooks/DEMO.md b/docker/data/storage/notebooks/DEMO.md index a5a224d6f..9db7ef902 100644 --- a/docker/data/storage/notebooks/DEMO.md +++ b/docker/data/storage/notebooks/DEMO.md @@ -61,6 +61,45 @@ def my_input(): result = my_input() ``` +## Picker (string values) + +The `ui.picker` component can be used to select from a list of items. Here's a basic example for selecting from a list of string values and displaying the selected key in a text field. + +```python +import deephaven.ui as ui +from deephaven.ui import use_state + + +@ui.component +def picker(): + value, set_value = use_state("") + + # Picker for selecting values + pick = ui.picker( + "Text 1", + "Text 2", + "Text 3", + label="Text", + on_selection_change=set_value, + selected_key=value, + ) + + # Show current selection in a ui.text component + text = ui.text("Selection: " + value) + + # Display picker and output in a flex column + return ui.flex( + pick, + text, + direction="column", + margin=10, + gap=10, + ) + + +p = picker() +``` + ## Using Tables You can open a Deephaven Table and control it using callbacks, as well. Let\'s create a table with some data, and then create a component that allows us to filter the table, and a button group it or ungroup it. diff --git a/package-lock.json b/package-lock.json index 5c1aa7bbc..3ece149cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31636,23 +31636,6 @@ "dev": true, "peer": true }, - "node_modules/stylelint/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/stylelint/node_modules/brace-expansion/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "peer": true - }, "node_modules/stylelint/node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -31694,43 +31677,19 @@ } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.0.tgz", - "integrity": "sha512-EryKbCE/wxpxKniQlyas6PY1I9vwtF3uCBweX+N8KYTCn3Y12RTGtQAJ/bd5pl7kxUAc8v/R3Ake/N17OZiFqA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "peer": true, "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.4", - "rimraf": "^5.0.5" + "keyv": "^4.5.4" }, "engines": { "node": ">=16" } }, - "node_modules/stylelint/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "peer": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/stylelint/node_modules/meow": { "version": "13.2.0", "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", @@ -31744,22 +31703,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stylelint/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "peer": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/stylelint/node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -31770,25 +31713,6 @@ "node": ">=8" } }, - "node_modules/stylelint/node_modules/rimraf": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", - "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", - "dev": true, - "peer": true, - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/stylelint/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -35403,21 +35327,21 @@ "version": "0.8.0", "license": "Apache-2.0", "dependencies": { - "@adobe/react-spectrum": "^3.29.0", - "@deephaven/chart": "^0.66.1", - "@deephaven/components": "^0.66.1", - "@deephaven/dashboard": "^0.66.1", - "@deephaven/dashboard-core-plugins": "^0.66.1", - "@deephaven/icons": "^0.66.0", - "@deephaven/iris-grid": "^0.66.1", - "@deephaven/jsapi-bootstrap": "^0.66.1", - "@deephaven/jsapi-components": "^0.66.1", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/plugin": "^0.66.1", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/redux": "^0.66.1", - "@deephaven/utils": "^0.66.0", + "@adobe/react-spectrum": "^3.34.1", + "@deephaven/chart": "^0.67.0", + "@deephaven/components": "^0.67.0", + "@deephaven/dashboard": "^0.67.0", + "@deephaven/dashboard-core-plugins": "^0.67.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/iris-grid": "^0.67.0", + "@deephaven/jsapi-bootstrap": "^0.67.0", + "@deephaven/jsapi-components": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/plugin": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/redux": "^0.67.0", + "@deephaven/utils": "^0.67.0", "@fortawesome/react-fontawesome": "^0.2.0", "@react-types/shared": "^3.22.0", "json-rpc-2.0": "^1.6.0", @@ -35438,23 +35362,23 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/chart": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/chart/-/chart-0.66.1.tgz", - "integrity": "sha512-2svx9FcuDuJiH6Va+82weQUB/n2g9vjeunanNf6hN8Bgyo87NpWFI3ExK15iEaPYE+oO85FUkPOEtefDcqbpuw==", - "dependencies": { - "@deephaven/components": "^0.66.1", - "@deephaven/icons": "^0.66.0", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/jsapi-utils": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/chart/-/chart-0.67.0.tgz", + "integrity": "sha512-Jkq9Lh647JWRvUSuqKnyqeMWjsSDDQY+uUpcAkw9F4J2Fl+j80TcG6Pk9xyocEJ1KwvCVojNtZbDlQHHPkVIMQ==", + "dependencies": { + "@deephaven/components": "^0.67.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/jsapi-utils": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/utils": "^0.67.0", "deep-equal": "^2.0.5", "lodash.debounce": "^4.0.8", "lodash.set": "^4.3.2", "memoize-one": "^5.1.1", "memoizee": "^0.4.15", - "plotly.js": "^2.18.2", + "plotly.js": "^2.29.1", "prop-types": "^15.7.2", "react-plotly.js": "^2.6.0" }, @@ -35466,18 +35390,19 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/components": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/components/-/components-0.66.1.tgz", - "integrity": "sha512-r2NfEI9LXkK5izL6SOQugmtuDv8Q06jOtBWD/uYcl4aE6moACZU8QoxUwMVQhmA0MMwlzZ6lJ0ZVVmxrz6an9w==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/components/-/components-0.67.0.tgz", + "integrity": "sha512-wpyBFL+/ePadhtxFu2+lG55UbWfzcwGyFtarLL7x3aVJdTTcwi5YtgUdYxHgbpmBfhG+mht2aLU59mLh2dhZ0A==", "dependencies": { "@adobe/react-spectrum": "^3.34.1", - "@deephaven/icons": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/utils": "^0.67.0", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/react-fontawesome": "^0.2.0", "@react-spectrum/theme-default": "^3.5.1", + "@react-types/shared": "^3.22.1", "bootstrap": "4.6.2", "classnames": "^2.3.1", "event-target-shim": "^6.0.2", @@ -35502,19 +35427,19 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/console": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/console/-/console-0.66.1.tgz", - "integrity": "sha512-Jx7hr9YBv63kNJabxU4HJNEsitNXgQByU3Ilq6Rle5TFg66VdZhO0WJVlnyH+kMgU9UIEeYp0FivzCW3PB77eA==", - "dependencies": { - "@deephaven/chart": "^0.66.1", - "@deephaven/components": "^0.66.1", - "@deephaven/icons": "^0.66.0", - "@deephaven/jsapi-bootstrap": "^0.66.1", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/storage": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/console/-/console-0.67.0.tgz", + "integrity": "sha512-umMLDPdaAyLafXW35fr7kWk/5ZfEd44lyTgu9GDVVththSLyKj3hpIBUWpfIGekwTrb46wfZbVmRbPKEmZNa4w==", + "dependencies": { + "@deephaven/chart": "^0.67.0", + "@deephaven/components": "^0.67.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/jsapi-bootstrap": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/storage": "^0.67.0", + "@deephaven/utils": "^0.67.0", "@fortawesome/react-fontawesome": "^0.2.0", "classnames": "^2.3.1", "linkifyjs": "^4.1.0", @@ -35538,16 +35463,16 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/dashboard": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/dashboard/-/dashboard-0.66.1.tgz", - "integrity": "sha512-YuE1d6S6Q1xYPZY2iuKSuoYD5zMrwMNTnuax+KqCsWxmclcNsB1ZDWxLdAntcByJA3G0Dc6ggSrS01QgZhcFuw==", - "dependencies": { - "@deephaven/components": "^0.66.1", - "@deephaven/golden-layout": "^0.66.1", - "@deephaven/log": "^0.66.0", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/redux": "^0.66.1", - "@deephaven/utils": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/dashboard/-/dashboard-0.67.0.tgz", + "integrity": "sha512-Sx16vqAxPKkF1x1xDfgKnntn7JW37mEBiV1D8qUgCHYUOM+uYiHoh1/UIU/cJfGrdx9ln+vUN21bkLDs++e1iQ==", + "dependencies": { + "@deephaven/components": "^0.67.0", + "@deephaven/golden-layout": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/redux": "^0.67.0", + "@deephaven/utils": "^0.67.0", "deep-equal": "^2.0.5", "lodash.ismatch": "^4.1.1", "lodash.throttle": "^4.1.1", @@ -35565,30 +35490,30 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/dashboard-core-plugins": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/dashboard-core-plugins/-/dashboard-core-plugins-0.66.1.tgz", - "integrity": "sha512-tSv8cFIUoo9qObidyqzOawyHxWi9Y8UdTOxG+HduTc4PyIoZR3rOJIrGAL4okb6bCUVGKuVAqyGz9QMuyCwZYA==", - "dependencies": { - "@deephaven/chart": "^0.66.1", - "@deephaven/components": "^0.66.1", - "@deephaven/console": "^0.66.1", - "@deephaven/dashboard": "^0.66.1", - "@deephaven/file-explorer": "^0.66.1", - "@deephaven/filters": "^0.66.0", - "@deephaven/golden-layout": "^0.66.1", - "@deephaven/grid": "^0.66.0", - "@deephaven/icons": "^0.66.0", - "@deephaven/iris-grid": "^0.66.1", - "@deephaven/jsapi-bootstrap": "^0.66.1", - "@deephaven/jsapi-components": "^0.66.1", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/jsapi-utils": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/plugin": "^0.66.1", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/redux": "^0.66.1", - "@deephaven/storage": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/dashboard-core-plugins/-/dashboard-core-plugins-0.67.0.tgz", + "integrity": "sha512-1XrHcvLclyIZLttFO0hgCncK/jEPT02NTObI/7OHLeTKpl6OgHuzmabYFxB0QrPAWqmlPaURtaDCH/kun4t54A==", + "dependencies": { + "@deephaven/chart": "^0.67.0", + "@deephaven/components": "^0.67.0", + "@deephaven/console": "^0.67.0", + "@deephaven/dashboard": "^0.67.0", + "@deephaven/file-explorer": "^0.67.0", + "@deephaven/filters": "^0.67.0", + "@deephaven/golden-layout": "^0.67.0", + "@deephaven/grid": "^0.67.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/iris-grid": "^0.67.0", + "@deephaven/jsapi-bootstrap": "^0.67.0", + "@deephaven/jsapi-components": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/jsapi-utils": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/plugin": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/redux": "^0.67.0", + "@deephaven/storage": "^0.67.0", + "@deephaven/utils": "^0.67.0", "@fortawesome/react-fontawesome": "^0.2.0", "classnames": "^2.3.1", "deep-equal": "^2.0.5", @@ -35617,15 +35542,15 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/file-explorer": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/file-explorer/-/file-explorer-0.66.1.tgz", - "integrity": "sha512-HS3E6bBx/u6bYMd85pZcjVrH9Z4dBufBBBzbwECczxnJUZ8rg56VyAef30Bz8aO0lfVxf6Im8S5mxWw3J/53CQ==", - "dependencies": { - "@deephaven/components": "^0.66.1", - "@deephaven/icons": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/storage": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/file-explorer/-/file-explorer-0.67.0.tgz", + "integrity": "sha512-Be4E+PK7lgwzGpOY3rnD2PssY/7cVBVcn7RMq9cPnWAcQZpkQg0IGCYpu+ahmRwqv3joeDiHUS7Vr1Fim73j+Q==", + "dependencies": { + "@deephaven/components": "^0.67.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/storage": "^0.67.0", + "@deephaven/utils": "^0.67.0", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/react-fontawesome": "^0.2.0", "classnames": "^2.3.1", @@ -35640,19 +35565,19 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/filters": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/filters/-/filters-0.66.0.tgz", - "integrity": "sha512-8hMpkwMhdJdFn35zRvE/6S6R+XS9nNuc0SKz4JGGg7bsZNpuOHt8iYRkxWVu88kbgJIOqMBs+y/38TI8sl1jnA==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/filters/-/filters-0.67.0.tgz", + "integrity": "sha512-k/qSHXmvAzfDn/EkA+hpsB5nwB/LUYntDH9lHUDix9ygnvalI72I2LgkQ2BBNri3VTKRGLXdummuWrNDVBvZvA==", "engines": { "node": ">=16" } }, "plugins/ui/src/js/node_modules/@deephaven/golden-layout": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/golden-layout/-/golden-layout-0.66.1.tgz", - "integrity": "sha512-8qvRLdAFN3cC3Q4zLd0mGV9h4aW0k30KixoPP2zEC45eANzlHFVr407no3w5NB8oeYFJujQ2ql0ZaYPGdtjxmQ==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/golden-layout/-/golden-layout-0.67.0.tgz", + "integrity": "sha512-Yt9cVBOdjNLsFJrk0BH7Ld/Y3IzLX9gU8uzxcOZ13JyVKf8Jl9sIvZA/MvMVUbKwqTMeAcmz2AUVuP5RVLP0jw==", "dependencies": { - "@deephaven/components": "^0.66.1", + "@deephaven/components": "^0.67.0", "jquery": "^3.6.0" }, "peerDependencies": { @@ -35661,11 +35586,11 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/grid": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/grid/-/grid-0.66.0.tgz", - "integrity": "sha512-Zv3BhNJp6d2v8NFGv33OAkOJn9PhfnDktrKOR2TPXzoPbShSf0WiChAF1JDUWSdOewX3tNYZEHFJrR+aVup/gw==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/grid/-/grid-0.67.0.tgz", + "integrity": "sha512-/UQf3OjQzCqbZ4+hliUzxPs+oCCK1sdiDblhcXfQnBCFVWlnrIXOQGf+5purPAoa5pfocgnXGTsZG3TEIkfyfQ==", "dependencies": { - "@deephaven/utils": "^0.66.0", + "@deephaven/utils": "^0.67.0", "classnames": "^2.3.1", "color-convert": "^2.0.1", "event-target-shim": "^6.0.2", @@ -35683,9 +35608,9 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/icons": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/icons/-/icons-0.66.0.tgz", - "integrity": "sha512-+8evWDhQzJUpCnashLN3ybac894ikhZRYiBP+bzyVRa9Fr11gAabw9NpVqPepAr102mJXkpR/5pUIajVW0c2VQ==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/icons/-/icons-0.67.0.tgz", + "integrity": "sha512-2m4Ufnwcf+ZWnH0JigPpJbxb4QNdu7RKrjaGt0wXEzml5N8arurx1kWMktOhk6Cz25DYJDCe/RMXuHjTqH2vUQ==", "dependencies": { "@fortawesome/fontawesome-common-types": "^6.1.1" }, @@ -35695,22 +35620,22 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/iris-grid": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/iris-grid/-/iris-grid-0.66.1.tgz", - "integrity": "sha512-6h+hXjNMEw9b2tkDhhVEP0aLHMTJmXh30/Vx5b2tRvve2Uv52qZ4hh/V8cb41GznBO4f2W42Y1Pt67Bnjr0HUQ==", - "dependencies": { - "@deephaven/components": "^0.66.1", - "@deephaven/console": "^0.66.1", - "@deephaven/filters": "^0.66.0", - "@deephaven/grid": "^0.66.0", - "@deephaven/icons": "^0.66.0", - "@deephaven/jsapi-components": "^0.66.1", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/jsapi-utils": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/storage": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/iris-grid/-/iris-grid-0.67.0.tgz", + "integrity": "sha512-LMttYlLIRD4NXxBOFc/F36zEsv6C0cZWXmFmeYJdFF1w2qugQpbLubxgu5roJt4qP9ZKNm00IhQs6sVfll1zhg==", + "dependencies": { + "@deephaven/components": "^0.67.0", + "@deephaven/console": "^0.67.0", + "@deephaven/filters": "^0.67.0", + "@deephaven/grid": "^0.67.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/jsapi-components": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/jsapi-utils": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/storage": "^0.67.0", + "@deephaven/utils": "^0.67.0", "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^7.0.2", "@dnd-kit/utilities": "^3.2.2", @@ -35737,14 +35662,14 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/jsapi-bootstrap": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/jsapi-bootstrap/-/jsapi-bootstrap-0.66.1.tgz", - "integrity": "sha512-B2LdiPjF+KUc2xh5GwilFcYrkh5pzPIJ6xyj3Bs5OZuG5a6+kP989DtRR5wysezqvrK1icTGJJUOr1YsjtOLRQ==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/jsapi-bootstrap/-/jsapi-bootstrap-0.67.0.tgz", + "integrity": "sha512-vFEm6YH33TOO3kQBWCxlxJNCZllGBw2RumkhfijS6JhTRPFQMJeb7l3g71W9j128z5eKp0g5c3rnasuQ/R0/fg==", "dependencies": { - "@deephaven/components": "^0.66.1", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/react-hooks": "^0.66.0" + "@deephaven/components": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0" }, "engines": { "node": ">=16" @@ -35754,17 +35679,17 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/jsapi-components": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/jsapi-components/-/jsapi-components-0.66.1.tgz", - "integrity": "sha512-WL5g/7I5pDeo/e7h9/JMqogNNt/juK086u2N+VBm6HA/pIAeaNw/+GLYg0MQuXzjtpniX5rP8lA7tA1k2IqBbg==", - "dependencies": { - "@deephaven/components": "^0.66.1", - "@deephaven/jsapi-bootstrap": "^0.66.1", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/jsapi-utils": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/jsapi-components/-/jsapi-components-0.67.0.tgz", + "integrity": "sha512-TbdVypvpCHd6eIAnDeDnpIbyLEGq3uB8BSZXGEj1QZhgEZCNUihBKU8PUCr2KcA8gYcYROuEXKFHNvi4NVwRpw==", + "dependencies": { + "@deephaven/components": "^0.67.0", + "@deephaven/jsapi-bootstrap": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/jsapi-utils": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/utils": "^0.67.0", "@types/js-cookie": "^3.0.3", "classnames": "^2.3.2", "js-cookie": "^3.0.5", @@ -35779,22 +35704,22 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/jsapi-types": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/jsapi-types/-/jsapi-types-0.66.0.tgz", - "integrity": "sha512-8svhvxFosIZ4mQSbCYJoEoQKzup4QrNoNTZ/EbD5kYr44ZkJifmCFKks98+oSYMaThviafccPZXZnLv6FGtkeA==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/jsapi-types/-/jsapi-types-0.67.0.tgz", + "integrity": "sha512-KOI6tI7NeFSDi3RbW2u75qH5qFoH81AJREOsZqVs0nSdoHuPXqvbwy/cpuh2bJdPLFAJ5fgzcuAw4nB8HQ1MkA==", "engines": { "node": ">=16" } }, "plugins/ui/src/js/node_modules/@deephaven/jsapi-utils": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/jsapi-utils/-/jsapi-utils-0.66.0.tgz", - "integrity": "sha512-Pma3GTKadk/iFZgvQswxXQ2yailQRd/nn8mct2ylaDHSCLzcO/FbUtCFxqb4ng52Ao41JROYaZZX3e1cvx+4nQ==", - "dependencies": { - "@deephaven/filters": "^0.66.0", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/jsapi-utils/-/jsapi-utils-0.67.0.tgz", + "integrity": "sha512-4s5KLSsM8mF+9Uv8f5ftanWU25WLErn5kKCrxZWBGQgJORMPm834KGiHQeD4HMpeZ84s/gMK4rtiWxJ4u9eaHg==", + "dependencies": { + "@deephaven/filters": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/utils": "^0.67.0", "lodash.clamp": "^4.0.3", "shortid": "^2.2.16" }, @@ -35803,9 +35728,9 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/log": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/log/-/log-0.66.0.tgz", - "integrity": "sha512-bWf0JMoWYTPl870D2XNrer2jhB8gaD3lLc9Jpq6ybCO03jgMVvg6c9sPsSeCOhfaL6LDyhc2siP5tzh5Mm38bQ==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/log/-/log-0.67.0.tgz", + "integrity": "sha512-tfYUThfC3JXTBXB7bVr8IN0go3xbKxlU42XPKtQaShpRxrXgWDmLAuzHtoqCTTlunYxzXa/m8SAIaJ5qGztN6A==", "dependencies": { "event-target-shim": "^6.0.2" }, @@ -35814,17 +35739,17 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/plugin": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/plugin/-/plugin-0.66.1.tgz", - "integrity": "sha512-CvawlZhCj5eX00FD+leoystyp5nw81nBVgY3HH20wMrNGhSd31vbZ1agEfehvd9uq9RNQqQiExMbZ3DFGMjE8A==", - "dependencies": { - "@deephaven/components": "^0.66.1", - "@deephaven/golden-layout": "^0.66.1", - "@deephaven/icons": "^0.66.0", - "@deephaven/iris-grid": "^0.66.1", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/react-hooks": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/plugin/-/plugin-0.67.0.tgz", + "integrity": "sha512-DiNmjqaclXub0vPVxXWGNQpFkTiwO+m9OqUtGTqIRzggZCK3IfqHFcjzUJx3GlCkf/efLIrJMQQVWPONMudRMw==", + "dependencies": { + "@deephaven/components": "^0.67.0", + "@deephaven/golden-layout": "^0.67.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/iris-grid": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", "@fortawesome/fontawesome-common-types": "^6.1.1", "@fortawesome/react-fontawesome": "^0.2.0" }, @@ -35836,13 +35761,13 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/react-hooks": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/react-hooks/-/react-hooks-0.66.0.tgz", - "integrity": "sha512-4FfvwHmr8rrMbvl2wabRCXwWZnMwvUGbJitnxhS6eEnAOQ3O/hyJCdYYVOphauz7PqyG8d1f7UTUNAIke3EGvw==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/react-hooks/-/react-hooks-0.67.0.tgz", + "integrity": "sha512-WbjYbYhBpXvLnq7TA8ISpIQhP4ViMdv98pFiLfKCW8Sld/5DARG8AcIbYpk+Q4QSsklhr8gGQJNWlzSqMSvz2g==", "dependencies": { "@adobe/react-spectrum": "^3.34.1", - "@deephaven/log": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "@deephaven/log": "^0.67.0", + "@deephaven/utils": "^0.67.0", "lodash.debounce": "^4.0.8", "shortid": "^2.2.16" }, @@ -35854,14 +35779,14 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/redux": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/redux/-/redux-0.66.1.tgz", - "integrity": "sha512-aFLgytw7dGxfhWbMcvm5hVDGaPDPX/c2v2X+o58HtjeakaoxgJyljvp20HeZizvsVghZTDajYn4hDwUDRSb7jg==", - "dependencies": { - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/jsapi-utils": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/plugin": "^0.66.1", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/redux/-/redux-0.67.0.tgz", + "integrity": "sha512-wV/2/q4sM/DktKmOGZoyo3c/cIwuGBvIOI3TG7JYpKXUfRyWf9gVxnBwp9wgu3yO3eNYAcq9xW74qYHGtYyqow==", + "dependencies": { + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/jsapi-utils": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/plugin": "^0.67.0", "deep-equal": "^2.0.5", "redux-thunk": "2.4.1" }, @@ -35873,12 +35798,12 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/storage": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/storage/-/storage-0.66.0.tgz", - "integrity": "sha512-My1IYOsmG6rTQviQxkKaxgKThO/wGKZvSLH8pv47kccUFhgAxlw4SWIQnGgSdX1vrOlLBEoKMrx4kBMO8OWGig==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/storage/-/storage-0.67.0.tgz", + "integrity": "sha512-KA/NycHgdqsgY/WDs3srw8Bk4ln3/v17ev3Is8TPOoht+lPddzXvN145CcpyunljKTjd24eWoXXZPBVtuyW81A==", "dependencies": { - "@deephaven/filters": "^0.66.0", - "@deephaven/log": "^0.66.0", + "@deephaven/filters": "^0.67.0", + "@deephaven/log": "^0.67.0", "lodash.throttle": "^4.1.1" }, "engines": { @@ -35889,9 +35814,9 @@ } }, "plugins/ui/src/js/node_modules/@deephaven/utils": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/utils/-/utils-0.66.0.tgz", - "integrity": "sha512-4d8Aj6CHfRqjs36ogPaAydH2Ir3D4nWp7Ae9ka4cC73hUKB0pNdisTmE3jNkzfggFhFGfLvS+zrYFWl6x8TKQg==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/utils/-/utils-0.67.0.tgz", + "integrity": "sha512-bA6pU5QOYDWNZV+fSW8LgDNh/mZ0G3M4zpropcHx1w0iRoLL4xC0b6UgAWCieG9ehi76wsjIDGOo9cy3iYP6WQ==", "engines": { "node": ">=16" } @@ -39725,21 +39650,21 @@ "@deephaven/js-plugin-ui": { "version": "file:plugins/ui/src/js", "requires": { - "@adobe/react-spectrum": "^3.29.0", - "@deephaven/chart": "^0.66.1", - "@deephaven/components": "^0.66.1", - "@deephaven/dashboard": "^0.66.1", - "@deephaven/dashboard-core-plugins": "^0.66.1", - "@deephaven/icons": "^0.66.0", - "@deephaven/iris-grid": "^0.66.1", - "@deephaven/jsapi-bootstrap": "^0.66.1", - "@deephaven/jsapi-components": "^0.66.1", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/plugin": "^0.66.1", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/redux": "^0.66.1", - "@deephaven/utils": "^0.66.0", + "@adobe/react-spectrum": "^3.34.1", + "@deephaven/chart": "^0.67.0", + "@deephaven/components": "^0.67.0", + "@deephaven/dashboard": "^0.67.0", + "@deephaven/dashboard-core-plugins": "^0.67.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/iris-grid": "^0.67.0", + "@deephaven/jsapi-bootstrap": "^0.67.0", + "@deephaven/jsapi-components": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/plugin": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/redux": "^0.67.0", + "@deephaven/utils": "^0.67.0", "@fortawesome/react-fontawesome": "^0.2.0", "@react-types/shared": "^3.22.0", "@types/react": "^17.0.2", @@ -39754,40 +39679,41 @@ }, "dependencies": { "@deephaven/chart": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/chart/-/chart-0.66.1.tgz", - "integrity": "sha512-2svx9FcuDuJiH6Va+82weQUB/n2g9vjeunanNf6hN8Bgyo87NpWFI3ExK15iEaPYE+oO85FUkPOEtefDcqbpuw==", - "requires": { - "@deephaven/components": "^0.66.1", - "@deephaven/icons": "^0.66.0", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/jsapi-utils": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/chart/-/chart-0.67.0.tgz", + "integrity": "sha512-Jkq9Lh647JWRvUSuqKnyqeMWjsSDDQY+uUpcAkw9F4J2Fl+j80TcG6Pk9xyocEJ1KwvCVojNtZbDlQHHPkVIMQ==", + "requires": { + "@deephaven/components": "^0.67.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/jsapi-utils": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/utils": "^0.67.0", "deep-equal": "^2.0.5", "lodash.debounce": "^4.0.8", "lodash.set": "^4.3.2", "memoize-one": "^5.1.1", "memoizee": "^0.4.15", - "plotly.js": "^2.18.2", + "plotly.js": "^2.29.1", "prop-types": "^15.7.2", "react-plotly.js": "^2.6.0" } }, "@deephaven/components": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/components/-/components-0.66.1.tgz", - "integrity": "sha512-r2NfEI9LXkK5izL6SOQugmtuDv8Q06jOtBWD/uYcl4aE6moACZU8QoxUwMVQhmA0MMwlzZ6lJ0ZVVmxrz6an9w==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/components/-/components-0.67.0.tgz", + "integrity": "sha512-wpyBFL+/ePadhtxFu2+lG55UbWfzcwGyFtarLL7x3aVJdTTcwi5YtgUdYxHgbpmBfhG+mht2aLU59mLh2dhZ0A==", "requires": { "@adobe/react-spectrum": "^3.34.1", - "@deephaven/icons": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/utils": "^0.67.0", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/react-fontawesome": "^0.2.0", "@react-spectrum/theme-default": "^3.5.1", + "@react-types/shared": "^3.22.1", "bootstrap": "4.6.2", "classnames": "^2.3.1", "event-target-shim": "^6.0.2", @@ -39805,19 +39731,19 @@ } }, "@deephaven/console": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/console/-/console-0.66.1.tgz", - "integrity": "sha512-Jx7hr9YBv63kNJabxU4HJNEsitNXgQByU3Ilq6Rle5TFg66VdZhO0WJVlnyH+kMgU9UIEeYp0FivzCW3PB77eA==", - "requires": { - "@deephaven/chart": "^0.66.1", - "@deephaven/components": "^0.66.1", - "@deephaven/icons": "^0.66.0", - "@deephaven/jsapi-bootstrap": "^0.66.1", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/storage": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/console/-/console-0.67.0.tgz", + "integrity": "sha512-umMLDPdaAyLafXW35fr7kWk/5ZfEd44lyTgu9GDVVththSLyKj3hpIBUWpfIGekwTrb46wfZbVmRbPKEmZNa4w==", + "requires": { + "@deephaven/chart": "^0.67.0", + "@deephaven/components": "^0.67.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/jsapi-bootstrap": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/storage": "^0.67.0", + "@deephaven/utils": "^0.67.0", "@fortawesome/react-fontawesome": "^0.2.0", "classnames": "^2.3.1", "linkifyjs": "^4.1.0", @@ -39834,16 +39760,16 @@ } }, "@deephaven/dashboard": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/dashboard/-/dashboard-0.66.1.tgz", - "integrity": "sha512-YuE1d6S6Q1xYPZY2iuKSuoYD5zMrwMNTnuax+KqCsWxmclcNsB1ZDWxLdAntcByJA3G0Dc6ggSrS01QgZhcFuw==", - "requires": { - "@deephaven/components": "^0.66.1", - "@deephaven/golden-layout": "^0.66.1", - "@deephaven/log": "^0.66.0", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/redux": "^0.66.1", - "@deephaven/utils": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/dashboard/-/dashboard-0.67.0.tgz", + "integrity": "sha512-Sx16vqAxPKkF1x1xDfgKnntn7JW37mEBiV1D8qUgCHYUOM+uYiHoh1/UIU/cJfGrdx9ln+vUN21bkLDs++e1iQ==", + "requires": { + "@deephaven/components": "^0.67.0", + "@deephaven/golden-layout": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/redux": "^0.67.0", + "@deephaven/utils": "^0.67.0", "deep-equal": "^2.0.5", "lodash.ismatch": "^4.1.1", "lodash.throttle": "^4.1.1", @@ -39852,30 +39778,30 @@ } }, "@deephaven/dashboard-core-plugins": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/dashboard-core-plugins/-/dashboard-core-plugins-0.66.1.tgz", - "integrity": "sha512-tSv8cFIUoo9qObidyqzOawyHxWi9Y8UdTOxG+HduTc4PyIoZR3rOJIrGAL4okb6bCUVGKuVAqyGz9QMuyCwZYA==", - "requires": { - "@deephaven/chart": "^0.66.1", - "@deephaven/components": "^0.66.1", - "@deephaven/console": "^0.66.1", - "@deephaven/dashboard": "^0.66.1", - "@deephaven/file-explorer": "^0.66.1", - "@deephaven/filters": "^0.66.0", - "@deephaven/golden-layout": "^0.66.1", - "@deephaven/grid": "^0.66.0", - "@deephaven/icons": "^0.66.0", - "@deephaven/iris-grid": "^0.66.1", - "@deephaven/jsapi-bootstrap": "^0.66.1", - "@deephaven/jsapi-components": "^0.66.1", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/jsapi-utils": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/plugin": "^0.66.1", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/redux": "^0.66.1", - "@deephaven/storage": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/dashboard-core-plugins/-/dashboard-core-plugins-0.67.0.tgz", + "integrity": "sha512-1XrHcvLclyIZLttFO0hgCncK/jEPT02NTObI/7OHLeTKpl6OgHuzmabYFxB0QrPAWqmlPaURtaDCH/kun4t54A==", + "requires": { + "@deephaven/chart": "^0.67.0", + "@deephaven/components": "^0.67.0", + "@deephaven/console": "^0.67.0", + "@deephaven/dashboard": "^0.67.0", + "@deephaven/file-explorer": "^0.67.0", + "@deephaven/filters": "^0.67.0", + "@deephaven/golden-layout": "^0.67.0", + "@deephaven/grid": "^0.67.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/iris-grid": "^0.67.0", + "@deephaven/jsapi-bootstrap": "^0.67.0", + "@deephaven/jsapi-components": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/jsapi-utils": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/plugin": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/redux": "^0.67.0", + "@deephaven/storage": "^0.67.0", + "@deephaven/utils": "^0.67.0", "@fortawesome/react-fontawesome": "^0.2.0", "classnames": "^2.3.1", "deep-equal": "^2.0.5", @@ -39896,15 +39822,15 @@ } }, "@deephaven/file-explorer": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/file-explorer/-/file-explorer-0.66.1.tgz", - "integrity": "sha512-HS3E6bBx/u6bYMd85pZcjVrH9Z4dBufBBBzbwECczxnJUZ8rg56VyAef30Bz8aO0lfVxf6Im8S5mxWw3J/53CQ==", - "requires": { - "@deephaven/components": "^0.66.1", - "@deephaven/icons": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/storage": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/file-explorer/-/file-explorer-0.67.0.tgz", + "integrity": "sha512-Be4E+PK7lgwzGpOY3rnD2PssY/7cVBVcn7RMq9cPnWAcQZpkQg0IGCYpu+ahmRwqv3joeDiHUS7Vr1Fim73j+Q==", + "requires": { + "@deephaven/components": "^0.67.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/storage": "^0.67.0", + "@deephaven/utils": "^0.67.0", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/react-fontawesome": "^0.2.0", "classnames": "^2.3.1", @@ -39913,25 +39839,25 @@ } }, "@deephaven/filters": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/filters/-/filters-0.66.0.tgz", - "integrity": "sha512-8hMpkwMhdJdFn35zRvE/6S6R+XS9nNuc0SKz4JGGg7bsZNpuOHt8iYRkxWVu88kbgJIOqMBs+y/38TI8sl1jnA==" + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/filters/-/filters-0.67.0.tgz", + "integrity": "sha512-k/qSHXmvAzfDn/EkA+hpsB5nwB/LUYntDH9lHUDix9ygnvalI72I2LgkQ2BBNri3VTKRGLXdummuWrNDVBvZvA==" }, "@deephaven/golden-layout": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/golden-layout/-/golden-layout-0.66.1.tgz", - "integrity": "sha512-8qvRLdAFN3cC3Q4zLd0mGV9h4aW0k30KixoPP2zEC45eANzlHFVr407no3w5NB8oeYFJujQ2ql0ZaYPGdtjxmQ==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/golden-layout/-/golden-layout-0.67.0.tgz", + "integrity": "sha512-Yt9cVBOdjNLsFJrk0BH7Ld/Y3IzLX9gU8uzxcOZ13JyVKf8Jl9sIvZA/MvMVUbKwqTMeAcmz2AUVuP5RVLP0jw==", "requires": { - "@deephaven/components": "^0.66.1", + "@deephaven/components": "^0.67.0", "jquery": "^3.6.0" } }, "@deephaven/grid": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/grid/-/grid-0.66.0.tgz", - "integrity": "sha512-Zv3BhNJp6d2v8NFGv33OAkOJn9PhfnDktrKOR2TPXzoPbShSf0WiChAF1JDUWSdOewX3tNYZEHFJrR+aVup/gw==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/grid/-/grid-0.67.0.tgz", + "integrity": "sha512-/UQf3OjQzCqbZ4+hliUzxPs+oCCK1sdiDblhcXfQnBCFVWlnrIXOQGf+5purPAoa5pfocgnXGTsZG3TEIkfyfQ==", "requires": { - "@deephaven/utils": "^0.66.0", + "@deephaven/utils": "^0.67.0", "classnames": "^2.3.1", "color-convert": "^2.0.1", "event-target-shim": "^6.0.2", @@ -39943,30 +39869,30 @@ } }, "@deephaven/icons": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/icons/-/icons-0.66.0.tgz", - "integrity": "sha512-+8evWDhQzJUpCnashLN3ybac894ikhZRYiBP+bzyVRa9Fr11gAabw9NpVqPepAr102mJXkpR/5pUIajVW0c2VQ==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/icons/-/icons-0.67.0.tgz", + "integrity": "sha512-2m4Ufnwcf+ZWnH0JigPpJbxb4QNdu7RKrjaGt0wXEzml5N8arurx1kWMktOhk6Cz25DYJDCe/RMXuHjTqH2vUQ==", "requires": { "@fortawesome/fontawesome-common-types": "^6.1.1" } }, "@deephaven/iris-grid": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/iris-grid/-/iris-grid-0.66.1.tgz", - "integrity": "sha512-6h+hXjNMEw9b2tkDhhVEP0aLHMTJmXh30/Vx5b2tRvve2Uv52qZ4hh/V8cb41GznBO4f2W42Y1Pt67Bnjr0HUQ==", - "requires": { - "@deephaven/components": "^0.66.1", - "@deephaven/console": "^0.66.1", - "@deephaven/filters": "^0.66.0", - "@deephaven/grid": "^0.66.0", - "@deephaven/icons": "^0.66.0", - "@deephaven/jsapi-components": "^0.66.1", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/jsapi-utils": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/storage": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/iris-grid/-/iris-grid-0.67.0.tgz", + "integrity": "sha512-LMttYlLIRD4NXxBOFc/F36zEsv6C0cZWXmFmeYJdFF1w2qugQpbLubxgu5roJt4qP9ZKNm00IhQs6sVfll1zhg==", + "requires": { + "@deephaven/components": "^0.67.0", + "@deephaven/console": "^0.67.0", + "@deephaven/filters": "^0.67.0", + "@deephaven/grid": "^0.67.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/jsapi-components": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/jsapi-utils": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/storage": "^0.67.0", + "@deephaven/utils": "^0.67.0", "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^7.0.2", "@dnd-kit/utilities": "^3.2.2", @@ -39986,28 +39912,28 @@ } }, "@deephaven/jsapi-bootstrap": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/jsapi-bootstrap/-/jsapi-bootstrap-0.66.1.tgz", - "integrity": "sha512-B2LdiPjF+KUc2xh5GwilFcYrkh5pzPIJ6xyj3Bs5OZuG5a6+kP989DtRR5wysezqvrK1icTGJJUOr1YsjtOLRQ==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/jsapi-bootstrap/-/jsapi-bootstrap-0.67.0.tgz", + "integrity": "sha512-vFEm6YH33TOO3kQBWCxlxJNCZllGBw2RumkhfijS6JhTRPFQMJeb7l3g71W9j128z5eKp0g5c3rnasuQ/R0/fg==", "requires": { - "@deephaven/components": "^0.66.1", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/react-hooks": "^0.66.0" + "@deephaven/components": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0" } }, "@deephaven/jsapi-components": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/jsapi-components/-/jsapi-components-0.66.1.tgz", - "integrity": "sha512-WL5g/7I5pDeo/e7h9/JMqogNNt/juK086u2N+VBm6HA/pIAeaNw/+GLYg0MQuXzjtpniX5rP8lA7tA1k2IqBbg==", - "requires": { - "@deephaven/components": "^0.66.1", - "@deephaven/jsapi-bootstrap": "^0.66.1", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/jsapi-utils": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/jsapi-components/-/jsapi-components-0.67.0.tgz", + "integrity": "sha512-TbdVypvpCHd6eIAnDeDnpIbyLEGq3uB8BSZXGEj1QZhgEZCNUihBKU8PUCr2KcA8gYcYROuEXKFHNvi4NVwRpw==", + "requires": { + "@deephaven/components": "^0.67.0", + "@deephaven/jsapi-bootstrap": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/jsapi-utils": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/utils": "^0.67.0", "@types/js-cookie": "^3.0.3", "classnames": "^2.3.2", "js-cookie": "^3.0.5", @@ -40016,86 +39942,86 @@ } }, "@deephaven/jsapi-types": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/jsapi-types/-/jsapi-types-0.66.0.tgz", - "integrity": "sha512-8svhvxFosIZ4mQSbCYJoEoQKzup4QrNoNTZ/EbD5kYr44ZkJifmCFKks98+oSYMaThviafccPZXZnLv6FGtkeA==" + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/jsapi-types/-/jsapi-types-0.67.0.tgz", + "integrity": "sha512-KOI6tI7NeFSDi3RbW2u75qH5qFoH81AJREOsZqVs0nSdoHuPXqvbwy/cpuh2bJdPLFAJ5fgzcuAw4nB8HQ1MkA==" }, "@deephaven/jsapi-utils": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/jsapi-utils/-/jsapi-utils-0.66.0.tgz", - "integrity": "sha512-Pma3GTKadk/iFZgvQswxXQ2yailQRd/nn8mct2ylaDHSCLzcO/FbUtCFxqb4ng52Ao41JROYaZZX3e1cvx+4nQ==", - "requires": { - "@deephaven/filters": "^0.66.0", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/jsapi-utils/-/jsapi-utils-0.67.0.tgz", + "integrity": "sha512-4s5KLSsM8mF+9Uv8f5ftanWU25WLErn5kKCrxZWBGQgJORMPm834KGiHQeD4HMpeZ84s/gMK4rtiWxJ4u9eaHg==", + "requires": { + "@deephaven/filters": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/utils": "^0.67.0", "lodash.clamp": "^4.0.3", "shortid": "^2.2.16" } }, "@deephaven/log": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/log/-/log-0.66.0.tgz", - "integrity": "sha512-bWf0JMoWYTPl870D2XNrer2jhB8gaD3lLc9Jpq6ybCO03jgMVvg6c9sPsSeCOhfaL6LDyhc2siP5tzh5Mm38bQ==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/log/-/log-0.67.0.tgz", + "integrity": "sha512-tfYUThfC3JXTBXB7bVr8IN0go3xbKxlU42XPKtQaShpRxrXgWDmLAuzHtoqCTTlunYxzXa/m8SAIaJ5qGztN6A==", "requires": { "event-target-shim": "^6.0.2" } }, "@deephaven/plugin": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/plugin/-/plugin-0.66.1.tgz", - "integrity": "sha512-CvawlZhCj5eX00FD+leoystyp5nw81nBVgY3HH20wMrNGhSd31vbZ1agEfehvd9uq9RNQqQiExMbZ3DFGMjE8A==", - "requires": { - "@deephaven/components": "^0.66.1", - "@deephaven/golden-layout": "^0.66.1", - "@deephaven/icons": "^0.66.0", - "@deephaven/iris-grid": "^0.66.1", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/react-hooks": "^0.66.0", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/plugin/-/plugin-0.67.0.tgz", + "integrity": "sha512-DiNmjqaclXub0vPVxXWGNQpFkTiwO+m9OqUtGTqIRzggZCK3IfqHFcjzUJx3GlCkf/efLIrJMQQVWPONMudRMw==", + "requires": { + "@deephaven/components": "^0.67.0", + "@deephaven/golden-layout": "^0.67.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/iris-grid": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", "@fortawesome/fontawesome-common-types": "^6.1.1", "@fortawesome/react-fontawesome": "^0.2.0" } }, "@deephaven/react-hooks": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/react-hooks/-/react-hooks-0.66.0.tgz", - "integrity": "sha512-4FfvwHmr8rrMbvl2wabRCXwWZnMwvUGbJitnxhS6eEnAOQ3O/hyJCdYYVOphauz7PqyG8d1f7UTUNAIke3EGvw==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/react-hooks/-/react-hooks-0.67.0.tgz", + "integrity": "sha512-WbjYbYhBpXvLnq7TA8ISpIQhP4ViMdv98pFiLfKCW8Sld/5DARG8AcIbYpk+Q4QSsklhr8gGQJNWlzSqMSvz2g==", "requires": { "@adobe/react-spectrum": "^3.34.1", - "@deephaven/log": "^0.66.0", - "@deephaven/utils": "^0.66.0", + "@deephaven/log": "^0.67.0", + "@deephaven/utils": "^0.67.0", "lodash.debounce": "^4.0.8", "shortid": "^2.2.16" } }, "@deephaven/redux": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@deephaven/redux/-/redux-0.66.1.tgz", - "integrity": "sha512-aFLgytw7dGxfhWbMcvm5hVDGaPDPX/c2v2X+o58HtjeakaoxgJyljvp20HeZizvsVghZTDajYn4hDwUDRSb7jg==", - "requires": { - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/jsapi-utils": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/plugin": "^0.66.1", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/redux/-/redux-0.67.0.tgz", + "integrity": "sha512-wV/2/q4sM/DktKmOGZoyo3c/cIwuGBvIOI3TG7JYpKXUfRyWf9gVxnBwp9wgu3yO3eNYAcq9xW74qYHGtYyqow==", + "requires": { + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/jsapi-utils": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/plugin": "^0.67.0", "deep-equal": "^2.0.5", "redux-thunk": "2.4.1" } }, "@deephaven/storage": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/storage/-/storage-0.66.0.tgz", - "integrity": "sha512-My1IYOsmG6rTQviQxkKaxgKThO/wGKZvSLH8pv47kccUFhgAxlw4SWIQnGgSdX1vrOlLBEoKMrx4kBMO8OWGig==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/storage/-/storage-0.67.0.tgz", + "integrity": "sha512-KA/NycHgdqsgY/WDs3srw8Bk4ln3/v17ev3Is8TPOoht+lPddzXvN145CcpyunljKTjd24eWoXXZPBVtuyW81A==", "requires": { - "@deephaven/filters": "^0.66.0", - "@deephaven/log": "^0.66.0", + "@deephaven/filters": "^0.67.0", + "@deephaven/log": "^0.67.0", "lodash.throttle": "^4.1.1" } }, "@deephaven/utils": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@deephaven/utils/-/utils-0.66.0.tgz", - "integrity": "sha512-4d8Aj6CHfRqjs36ogPaAydH2Ir3D4nWp7Ae9ka4cC73hUKB0pNdisTmE3jNkzfggFhFGfLvS+zrYFWl6x8TKQg==" + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@deephaven/utils/-/utils-0.67.0.tgz", + "integrity": "sha512-bA6pU5QOYDWNZV+fSW8LgDNh/mZ0G3M4zpropcHx1w0iRoLL4xC0b6UgAWCieG9ehi76wsjIDGOo9cy3iYP6WQ==" }, "color-convert": { "version": "2.0.1", @@ -60677,25 +60603,6 @@ "dev": true, "peer": true }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "peer": true, - "requires": { - "balanced-match": "^1.0.0" - }, - "dependencies": { - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "peer": true - } - } - }, "cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -60720,29 +60627,14 @@ } }, "flat-cache": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.0.tgz", - "integrity": "sha512-EryKbCE/wxpxKniQlyas6PY1I9vwtF3uCBweX+N8KYTCn3Y12RTGtQAJ/bd5pl7kxUAc8v/R3Ake/N17OZiFqA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "peer": true, "requires": { "flatted": "^3.2.9", - "keyv": "^4.5.4", - "rimraf": "^5.0.5" - } - }, - "glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "peer": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "keyv": "^4.5.4" } }, "meow": { @@ -60752,16 +60644,6 @@ "dev": true, "peer": true }, - "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "peer": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, "resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -60769,16 +60651,6 @@ "dev": true, "peer": true }, - "rimraf": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", - "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", - "dev": true, - "peer": true, - "requires": { - "glob": "^10.3.7" - } - }, "signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", diff --git a/package.json b/package.json index 4f7c7f88f..45d85b0e7 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "test:ci": "run-p test:ci:*", "test:ci:unit": "jest --config jest.config.unit.cjs --ci --cacheDirectory $PWD/.jest-cache", "test:ci:lint": "jest --config jest.config.lint.cjs --ci --cacheDirectory $PWD/.jest-cache", - "update-dh-packages": "lerna run update-dh-packages --" + "update-dh-packages": "lerna run update-dh-packages" }, "devDependencies": { "@deephaven/babel-preset": "^0.40.0", diff --git a/plugins/ui/examples/README.md b/plugins/ui/examples/README.md index 9a69c1577..2675b3ce8 100644 --- a/plugins/ui/examples/README.md +++ b/plugins/ui/examples/README.md @@ -92,6 +92,45 @@ ce = checkbox_example() ![Checkbox](assets/checkbox.png) +## Picker (string values) + +The `ui.picker` component can be used to select from a list of items. Here's a basic example for selecting from a list of string values and displaying the selected key in a text field. + +```python +import deephaven.ui as ui +from deephaven.ui import use_state + + +@ui.component +def picker(): + value, set_value = use_state("") + + # Picker for selecting values + pick = ui.picker( + "Text 1", + "Text 2", + "Text 3", + label="Text", + on_selection_change=set_value, + selected_key=value, + ) + + # Show current selection in a ui.text component + text = ui.text("Selection: " + value) + + # Display picker and output in a flex column + return ui.flex( + pick, + text, + direction="column", + margin=10, + gap=10, + ) + + +p = picker() +``` + ## Form (two variables) You can have state with multiple different variables in one component. In this example, we have a [text field](https://react-spectrum.adobe.com/react-spectrum/TextField.html) and a [slider](https://react-spectrum.adobe.com/react-spectrum/Slider.html), and we display the values of both of them. diff --git a/plugins/ui/src/js/package.json b/plugins/ui/src/js/package.json index 7b43d7444..5b5631edd 100644 --- a/plugins/ui/src/js/package.json +++ b/plugins/ui/src/js/package.json @@ -41,21 +41,21 @@ "react-dom": "^17.0.2" }, "dependencies": { - "@adobe/react-spectrum": "^3.29.0", - "@deephaven/chart": "^0.66.1", - "@deephaven/components": "^0.66.1", - "@deephaven/dashboard": "^0.66.1", - "@deephaven/dashboard-core-plugins": "^0.66.1", - "@deephaven/icons": "^0.66.0", - "@deephaven/iris-grid": "^0.66.1", - "@deephaven/jsapi-bootstrap": "^0.66.1", - "@deephaven/jsapi-components": "^0.66.1", - "@deephaven/jsapi-types": "^0.66.0", - "@deephaven/log": "^0.66.0", - "@deephaven/plugin": "^0.66.1", - "@deephaven/react-hooks": "^0.66.0", - "@deephaven/redux": "^0.66.1", - "@deephaven/utils": "^0.66.0", + "@adobe/react-spectrum": "^3.34.1", + "@deephaven/chart": "^0.67.0", + "@deephaven/components": "^0.67.0", + "@deephaven/dashboard": "^0.67.0", + "@deephaven/dashboard-core-plugins": "^0.67.0", + "@deephaven/icons": "^0.67.0", + "@deephaven/iris-grid": "^0.67.0", + "@deephaven/jsapi-bootstrap": "^0.67.0", + "@deephaven/jsapi-components": "^0.67.0", + "@deephaven/jsapi-types": "^0.67.0", + "@deephaven/log": "^0.67.0", + "@deephaven/plugin": "^0.67.0", + "@deephaven/react-hooks": "^0.67.0", + "@deephaven/redux": "^0.67.0", + "@deephaven/utils": "^0.67.0", "@fortawesome/react-fontawesome": "^0.2.0", "@react-types/shared": "^3.22.0", "json-rpc-2.0": "^1.6.0", diff --git a/plugins/ui/src/js/src/elements/ElementConstants.ts b/plugins/ui/src/js/src/elements/ElementConstants.ts new file mode 100644 index 000000000..0ff501ac3 --- /dev/null +++ b/plugins/ui/src/js/src/elements/ElementConstants.ts @@ -0,0 +1,42 @@ +import { ReactHTML } from 'react'; +import type * as icons from '@deephaven/icons'; + +/** Namespaces */ +export const UI_COMPONENTS_NAMESPACE = 'deephaven.ui.components'; +export const UI_ELEMENTS_NAMESPACE = 'deephaven.ui.elements'; + +/** Table */ +export const UITABLE_ELEMENT_TYPE = `${UI_ELEMENTS_NAMESPACE}.UITable` as const; +export type UITableElementName = typeof UITABLE_ELEMENT_TYPE; + +/** Layout */ +export const PANEL_ELEMENT_NAME = `${UI_COMPONENTS_NAMESPACE}.Panel` as const; +export const ROW_ELEMENT_NAME = `${UI_COMPONENTS_NAMESPACE}.Row` as const; +export const COLUMN_ELEMENT_NAME = `${UI_COMPONENTS_NAMESPACE}.Column` as const; +export const STACK_ELEMENT_NAME = `${UI_COMPONENTS_NAMESPACE}.Stack` as const; +export const DASHBOARD_ELEMENT_NAME = + `${UI_COMPONENTS_NAMESPACE}.Dashboard` as const; + +export type PanelElementType = typeof PANEL_ELEMENT_NAME; +export type RowElementType = typeof ROW_ELEMENT_NAME; +export type ColumnElementType = typeof COLUMN_ELEMENT_NAME; +export type StackElementType = typeof STACK_ELEMENT_NAME; +export type DashboardElementType = typeof DASHBOARD_ELEMENT_NAME; + +/** Icons */ +export const ICON_ELEMENT_TYPE_PREFIX = 'deephaven.ui.icons.'; +export type IconElementName = + `${typeof ICON_ELEMENT_TYPE_PREFIX}${keyof typeof icons}`; + +/** HTML */ +export const HTML_ELEMENT_NAME_PREFIX = 'deephaven.ui.html.'; +export type HTMLElementType = + `${typeof HTML_ELEMENT_NAME_PREFIX}${keyof ReactHTML}`; + +/** Specific Components */ +export const FRAGMENT_ELEMENT_NAME = + `${UI_COMPONENTS_NAMESPACE}.Fragment` as const; +export const ITEM_ELEMENT_NAME = `${UI_COMPONENTS_NAMESPACE}.Item` as const; +export const PICKER_ELEMENT_NAME = `${UI_COMPONENTS_NAMESPACE}.Picker` as const; +export const SECTION_ELEMENT_NAME = + `${UI_COMPONENTS_NAMESPACE}.Section` as const; diff --git a/plugins/ui/src/js/src/elements/ElementUtils.ts b/plugins/ui/src/js/src/elements/ElementUtils.ts index 9fd1357a6..218e6e4c0 100644 --- a/plugins/ui/src/js/src/elements/ElementUtils.ts +++ b/plugins/ui/src/js/src/elements/ElementUtils.ts @@ -74,27 +74,3 @@ export function getElementKey(node: unknown, defaultKey: string): string { } return `${node.props?.key}`; } - -export const FRAGMENT_ELEMENT_NAME = 'deephaven.ui.components.Fragment'; - -export type FragmentElementType = typeof FRAGMENT_ELEMENT_NAME; - -/** - * Describes a fragment element that can be rendered in the UI. - * Will be placed in the current dashboard, or within a user created dashboard if specified. - */ -export type FragmentElementNode = ElementNode; - -/** - * Check if an object is a FragmentElementNode - * @param obj Object to check - * @returns True if the object is a FragmentElementNode - */ -export function isFragmentElementNode( - obj: unknown -): obj is FragmentElementNode { - return ( - isElementNode(obj) && - (obj as ElementNode)[ELEMENT_KEY] === FRAGMENT_ELEMENT_NAME - ); -} diff --git a/plugins/ui/src/js/src/elements/HTMLElementUtils.ts b/plugins/ui/src/js/src/elements/HTMLElementUtils.ts index e01124feb..d7926c48c 100644 --- a/plugins/ui/src/js/src/elements/HTMLElementUtils.ts +++ b/plugins/ui/src/js/src/elements/HTMLElementUtils.ts @@ -1,11 +1,7 @@ import { ReactHTML } from 'react'; +import { HTMLElementType, HTML_ELEMENT_NAME_PREFIX } from './ElementConstants'; import { ELEMENT_KEY, ElementNode, isElementNode } from './ElementUtils'; -export const HTML_ELEMENT_NAME_PREFIX = 'deephaven.ui.html.'; - -export type HTMLElementType = - `${typeof HTML_ELEMENT_NAME_PREFIX}${keyof ReactHTML}`; - /** * Describes an HTML element that can be rendered in the UI. * The tag used for the HTML element is the name of the element without the prefix. diff --git a/plugins/ui/src/js/src/elements/IconElementUtils.ts b/plugins/ui/src/js/src/elements/IconElementUtils.ts index 9a6fc2980..871d23a25 100644 --- a/plugins/ui/src/js/src/elements/IconElementUtils.ts +++ b/plugins/ui/src/js/src/elements/IconElementUtils.ts @@ -1,11 +1,7 @@ import * as icons from '@deephaven/icons'; +import { IconElementName, ICON_ELEMENT_TYPE_PREFIX } from './ElementConstants'; import { ELEMENT_KEY, ElementNode, isElementNode } from './ElementUtils'; -export const ICON_ELEMENT_TYPE_PREFIX = 'deephaven.ui.icons.'; - -export type IconElementName = - `${typeof ICON_ELEMENT_TYPE_PREFIX}${keyof typeof icons}`; - /** * Describes an icon element that can be rendered in the UI. * The name of the icon is the name of the element without the prefix. diff --git a/plugins/ui/src/js/src/elements/Picker.tsx b/plugins/ui/src/js/src/elements/Picker.tsx new file mode 100644 index 000000000..d19e34699 --- /dev/null +++ b/plugins/ui/src/js/src/elements/Picker.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { + Picker as DHPicker, + PickerProps as DHPickerProps, +} from '@deephaven/components'; +import { SerializedPickerEventProps, usePickerProps } from './usePickerProps'; + +function Picker(props: DHPickerProps & SerializedPickerEventProps) { + const pickerProps = usePickerProps(props); + + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} + +export default Picker; diff --git a/plugins/ui/src/js/src/elements/UITableUtils.tsx b/plugins/ui/src/js/src/elements/UITableUtils.tsx index 4bc6a2c11..1d80866bb 100644 --- a/plugins/ui/src/js/src/elements/UITableUtils.tsx +++ b/plugins/ui/src/js/src/elements/UITableUtils.tsx @@ -1,10 +1,7 @@ import type { WidgetExportedObject } from '@deephaven/jsapi-types'; import { DehydratedSort } from '@deephaven/iris-grid'; import { ELEMENT_KEY, ElementNode, isElementNode } from './ElementUtils'; - -export const UITABLE_ELEMENT_TYPE = 'deephaven.ui.elements.UITable'; - -export type UITableElementName = typeof UITABLE_ELEMENT_TYPE; +import { UITableElementName, UITABLE_ELEMENT_TYPE } from './ElementConstants'; export interface UITableProps { table: WidgetExportedObject; diff --git a/plugins/ui/src/js/src/elements/usePickerProps.ts b/plugins/ui/src/js/src/elements/usePickerProps.ts new file mode 100644 index 000000000..5261f3de6 --- /dev/null +++ b/plugins/ui/src/js/src/elements/usePickerProps.ts @@ -0,0 +1,48 @@ +import { + SerializedFocusEventCallback, + useFocusEventCallback, +} from './spectrum/useFocusEventCallback'; +import { + SerializedKeyboardEventCallback, + useKeyboardEventCallback, +} from './spectrum/useKeyboardEventCallback'; + +export interface SerializedPickerEventProps { + /** Handler that is called when the element receives focus. */ + onFocus?: SerializedFocusEventCallback; + + /** Handler that is called when the element loses focus. */ + onBlur?: SerializedFocusEventCallback; + + /** Handler that is called when a key is pressed */ + onKeyDown?: SerializedKeyboardEventCallback; + + /** Handler that is called when a key is released */ + onKeyUp?: SerializedKeyboardEventCallback; +} + +/** + * Wrap Picker props with the appropriate serialized event callbacks. + * @param props Props to wrap + * @returns Wrapped props + */ +export function usePickerProps(props: SerializedPickerEventProps & T) { + const { onFocus, onBlur, onKeyDown, onKeyUp, ...otherProps } = props; + + const serializedOnFocus = useFocusEventCallback(onFocus); + const serializedOnBlur = useFocusEventCallback(onBlur); + const serializedOnKeyDown = useKeyboardEventCallback(onKeyDown); + const serializedOnKeyUp = useKeyboardEventCallback(onKeyUp); + + return { + onFocus: serializedOnFocus, + onBlur: serializedOnBlur, + onKeyDown: serializedOnKeyDown, + onKeyUp: serializedOnKeyUp, + // The @deephaven/components `Picker` has its own normalization logic that + // handles primitive children types (string, number, boolean). It also + // handles nested children inside of `Item` and `Section` components, so + // we are intentionally not wrapping `otherProps` in `mapSpectrumProps` + ...otherProps, + }; +} diff --git a/plugins/ui/src/js/src/layout/LayoutUtils.test.tsx b/plugins/ui/src/js/src/layout/LayoutUtils.test.tsx index 14c52e889..da8535034 100644 --- a/plugins/ui/src/js/src/layout/LayoutUtils.test.tsx +++ b/plugins/ui/src/js/src/layout/LayoutUtils.test.tsx @@ -3,14 +3,10 @@ import React from 'react'; import { TestUtils } from '@deephaven/utils'; import Column from './Column'; import { - PANEL_ELEMENT_NAME, - ROW_ELEMENT_NAME, - COLUMN_ELEMENT_NAME, isPanelElementNode, isRowElementNode, isColumnElementNode, isStackElementNode, - STACK_ELEMENT_NAME, normalizeDashboardChildren, normalizeColumnChildren, normalizeRowChildren, @@ -19,6 +15,12 @@ import { import Row from './Row'; import Stack from './Stack'; import ReactPanel from './ReactPanel'; +import { + PANEL_ELEMENT_NAME, + ROW_ELEMENT_NAME, + COLUMN_ELEMENT_NAME, + STACK_ELEMENT_NAME, +} from '../elements/ElementConstants'; beforeEach(() => { TestUtils.disableConsoleOutput(); diff --git a/plugins/ui/src/js/src/layout/LayoutUtils.tsx b/plugins/ui/src/js/src/layout/LayoutUtils.tsx index 230c276d9..459565c05 100644 --- a/plugins/ui/src/js/src/layout/LayoutUtils.tsx +++ b/plugins/ui/src/js/src/layout/LayoutUtils.tsx @@ -13,18 +13,18 @@ import Column from './Column'; import Row from './Row'; import Stack from './Stack'; import ReactPanel from './ReactPanel'; - -export const PANEL_ELEMENT_NAME = 'deephaven.ui.components.Panel'; -export const ROW_ELEMENT_NAME = 'deephaven.ui.components.Row'; -export const COLUMN_ELEMENT_NAME = 'deephaven.ui.components.Column'; -export const STACK_ELEMENT_NAME = 'deephaven.ui.components.Stack'; -export const DASHBOARD_ELEMENT_NAME = 'deephaven.ui.components.Dashboard'; - -export type PanelElementType = typeof PANEL_ELEMENT_NAME; -export type RowElementType = typeof ROW_ELEMENT_NAME; -export type ColumnElementType = typeof COLUMN_ELEMENT_NAME; -export type StackElementType = typeof STACK_ELEMENT_NAME; -export type DashboardElementType = typeof DASHBOARD_ELEMENT_NAME; +import { + ColumnElementType, + COLUMN_ELEMENT_NAME, + DashboardElementType, + DASHBOARD_ELEMENT_NAME, + PanelElementType, + PANEL_ELEMENT_NAME, + RowElementType, + ROW_ELEMENT_NAME, + StackElementType, + STACK_ELEMENT_NAME, +} from '../elements/ElementConstants'; export type GoldenLayoutParent = RowOrColumn | GLStack | Root; diff --git a/plugins/ui/src/js/src/widget/DocumentHandler.test.tsx b/plugins/ui/src/js/src/widget/DocumentHandler.test.tsx index c00b3286d..5b7023f23 100644 --- a/plugins/ui/src/js/src/widget/DocumentHandler.test.tsx +++ b/plugins/ui/src/js/src/widget/DocumentHandler.test.tsx @@ -3,13 +3,13 @@ import { WidgetDescriptor } from '@deephaven/dashboard'; import { TestUtils } from '@deephaven/utils'; import { render } from '@testing-library/react'; import DocumentHandler, { DocumentHandlerProps } from './DocumentHandler'; +import { ReactPanelProps } from '../layout/LayoutUtils'; +import { MixedPanelsError, NoChildrenError } from '../errors'; +import { getComponentForElement } from './WidgetUtils'; import { DASHBOARD_ELEMENT_NAME, PANEL_ELEMENT_NAME, - ReactPanelProps, -} from '../layout/LayoutUtils'; -import { MixedPanelsError, NoChildrenError } from '../errors'; -import { getComponentForElement } from './WidgetUtils'; +} from '../elements/ElementConstants'; const mockReactPanel = jest.fn((props: ReactPanelProps) => (
ReactPanel
diff --git a/plugins/ui/src/js/src/widget/WidgetUtils.test.tsx b/plugins/ui/src/js/src/widget/WidgetUtils.test.tsx new file mode 100644 index 000000000..4d5ffdbd4 --- /dev/null +++ b/plugins/ui/src/js/src/widget/WidgetUtils.test.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { + FRAGMENT_ELEMENT_NAME, + HTML_ELEMENT_NAME_PREFIX, + ICON_ELEMENT_TYPE_PREFIX, +} from '../elements/ElementConstants'; +import { ELEMENT_KEY } from '../elements/ElementUtils'; +import HTMLElementView from '../elements/HTMLElementView'; +import IconElementView from '../elements/IconElementView'; +import { SPECTRUM_ELEMENT_TYPE_PREFIX } from '../elements/SpectrumElementUtils'; +import SpectrumElementView from '../elements/SpectrumElementView'; +import { + elementComponentMap, + getComponentForElement, + getComponentTypeForElement, +} from './WidgetUtils'; + +describe('getComponentTypeForElement', () => { + it.each( + Object.keys(elementComponentMap) as (keyof typeof elementComponentMap)[] + )( + 'should return the correct component type for a given key: %s', + elementKey => { + const actual = getComponentTypeForElement({ [ELEMENT_KEY]: elementKey }); + expect(actual).toBe(elementComponentMap[elementKey]); + } + ); +}); + +describe('getComponentForElement', () => { + it.each([ + /* eslint-disable react/jsx-key */ + [`${HTML_ELEMENT_NAME_PREFIX}div`, HTMLElementView], + [`${SPECTRUM_ELEMENT_TYPE_PREFIX}ActionButton`, SpectrumElementView], + [`${ICON_ELEMENT_TYPE_PREFIX}vsAdd`, IconElementView], + /* eslint-enable react/jsx-key */ + ] as [string, ({ element }: { element: unknown }) => JSX.Element][])( + 'should use expected element factory function: %s', + (elementKey, factory) => { + const actual = getComponentForElement({ [ELEMENT_KEY]: elementKey }); + expect(actual).toEqual( + factory({ element: { [ELEMENT_KEY]: elementKey } }) + ); + } + ); + + it.each( + Object.keys(elementComponentMap) as (keyof typeof elementComponentMap)[] + )('should spread props for element nodes', elementKey => { + const element = { + [ELEMENT_KEY]: elementKey, + props: + elementKey === FRAGMENT_ELEMENT_NAME + ? { key: 'mock.key', children: ['Some child'] } + : { + 'data-test': 'mock.value', + children: ['Some child'], + }, + }; + const actual = getComponentForElement(element); + + const Expected = elementComponentMap[elementKey] as ( + props: Record + ) => JSX.Element; + + expect(actual).toEqual( + // eslint-disable-next-line react/jsx-props-no-spreading + [0])} /> + ); + }); +}); diff --git a/plugins/ui/src/js/src/widget/WidgetUtils.tsx b/plugins/ui/src/js/src/widget/WidgetUtils.tsx index dbe4d5493..cc404b52a 100644 --- a/plugins/ui/src/js/src/widget/WidgetUtils.tsx +++ b/plugins/ui/src/js/src/widget/WidgetUtils.tsx @@ -1,10 +1,14 @@ /* eslint-disable react/jsx-props-no-spreading */ /* eslint-disable import/prefer-default-export */ -import React from 'react'; +import React, { ComponentType } from 'react'; +// Importing `Item` and `Section` compnents directly since they should not be +// wrapped due to how Spectrum collection components consume them. +import { Item, Section } from '@deephaven/components'; import { ElementNode, + ELEMENT_KEY, + isElementNode, isExportedObject, - isFragmentElementNode, } from '../elements/ElementUtils'; import HTMLElementView from '../elements/HTMLElementView'; import { isHTMLElementNode } from '../elements/HTMLElementUtils'; @@ -12,21 +16,50 @@ import { isSpectrumElementNode } from '../elements/SpectrumElementUtils'; import SpectrumElementView from '../elements/SpectrumElementView'; import { isIconElementNode } from '../elements/IconElementUtils'; import IconElementView from '../elements/IconElementView'; -import { isUITable } from '../elements/UITableUtils'; import UITable from '../elements/UITable'; import { - isColumnElementNode, - isDashboardElementNode, - isPanelElementNode, - isRowElementNode, - isStackElementNode, -} from '../layout/LayoutUtils'; + COLUMN_ELEMENT_NAME, + DASHBOARD_ELEMENT_NAME, + FRAGMENT_ELEMENT_NAME, + ITEM_ELEMENT_NAME, + PANEL_ELEMENT_NAME, + PICKER_ELEMENT_NAME, + ROW_ELEMENT_NAME, + SECTION_ELEMENT_NAME, + STACK_ELEMENT_NAME, + UITABLE_ELEMENT_TYPE, +} from '../elements/ElementConstants'; import ReactPanel from '../layout/ReactPanel'; import ObjectView from '../elements/ObjectView'; import Row from '../layout/Row'; import Stack from '../layout/Stack'; import Column from '../layout/Column'; import Dashboard from '../layout/Dashboard'; +import Picker from '../elements/Picker'; + +/* + * Map element node names to their corresponding React components + */ +export const elementComponentMap = { + [PANEL_ELEMENT_NAME]: ReactPanel, + [ROW_ELEMENT_NAME]: Row, + [COLUMN_ELEMENT_NAME]: Column, + [FRAGMENT_ELEMENT_NAME]: React.Fragment, + [STACK_ELEMENT_NAME]: Stack, + [DASHBOARD_ELEMENT_NAME]: Dashboard, + [ITEM_ELEMENT_NAME]: Item, + [PICKER_ELEMENT_NAME]: Picker, + [SECTION_ELEMENT_NAME]: Section, + [UITABLE_ELEMENT_TYPE]: UITable, +} as const; + +export function getComponentTypeForElement

>( + element: ElementNode +): ComponentType

| null { + return (elementComponentMap[ + element[ELEMENT_KEY] as keyof typeof elementComponentMap + ] ?? null) as ComponentType

| null; +} export function getComponentForElement(element: ElementNode): React.ReactNode { // Need to convert the children of the element if they are exported objects to an ObjectView @@ -59,27 +92,12 @@ export function getComponentForElement(element: ElementNode): React.ReactNode { if (isIconElementNode(newElement)) { return IconElementView({ element: newElement }); } - if (isUITable(newElement)) { - return ; - } - if (isPanelElementNode(newElement)) { - return ; - } - if (isFragmentElementNode(newElement)) { - // eslint-disable-next-line react/jsx-no-useless-fragment - return <>{newElement.props?.children}; - } - if (isRowElementNode(newElement)) { - return ; - } - if (isColumnElementNode(newElement)) { - return ; - } - if (isStackElementNode(newElement)) { - return ; - } - if (isDashboardElementNode(newElement)) { - return ; + if (isElementNode(newElement)) { + const Component = getComponentTypeForElement(newElement); + + if (Component != null) { + return ; + } } return newElement.props?.children; diff --git a/tools/update-dh-packages.mjs b/tools/update-dh-packages.mjs index cdabbf7ab..f533e0516 100644 --- a/tools/update-dh-packages.mjs +++ b/tools/update-dh-packages.mjs @@ -14,7 +14,7 @@ * `npm run update-dh-packages`. * * Or for a specific plugin via: - * `npm run update-dh-packages --scope=@deephaven/js-plugin-ui` + * `npm run update-dh-packages -- --scope=@deephaven/js-plugin-ui` */ /* eslint-disable no-console */ From 6028195fdcdaf7d379e60fa3ed824a7f4b553e85 Mon Sep 17 00:00:00 2001 From: Mike Bender Date: Tue, 5 Mar 2024 14:47:09 -0500 Subject: [PATCH 2/8] fix: Tab Panels contents should take up the full height (#340) - Fixes #175 - Tested using the snippet from within the ticket, as well as some other examples with tab_panels in vertical orientation --- .../deephaven/ui/components/spectrum/basic.py | 8 ------- .../js/src/elements/SpectrumElementUtils.ts | 2 +- .../js/src/elements/spectrum/TabPanels.tsx | 21 +++++++++++++++++++ .../ui/src/js/src/elements/spectrum/index.ts | 1 + 4 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 plugins/ui/src/js/src/elements/spectrum/TabPanels.tsx diff --git a/plugins/ui/src/deephaven/ui/components/spectrum/basic.py b/plugins/ui/src/deephaven/ui/components/spectrum/basic.py index cce9ae815..3eabd1d50 100644 --- a/plugins/ui/src/deephaven/ui/components/spectrum/basic.py +++ b/plugins/ui/src/deephaven/ui/components/spectrum/basic.py @@ -61,14 +61,6 @@ def icon_wrapper(*children, **props): return spectrum_element("Icon", *children, **props) -def item(*children, **props): - """ - Python implementation for the Adobe React Spectrum Item component. - Used with Tabs: https://react-spectrum.adobe.com/react-spectrum/Tabs.html - """ - return spectrum_element("Item", *children, **props) - - def illustrated_message(*children, **props): """ Python implementation for the Adobe React Spectrum IllustratedMessage component. diff --git a/plugins/ui/src/js/src/elements/SpectrumElementUtils.ts b/plugins/ui/src/js/src/elements/SpectrumElementUtils.ts index 9cd44b1d0..d6a546fdf 100644 --- a/plugins/ui/src/js/src/elements/SpectrumElementUtils.ts +++ b/plugins/ui/src/js/src/elements/SpectrumElementUtils.ts @@ -12,7 +12,6 @@ import { Switch, Tabs, TabList, - TabPanels, Text, ToggleButton, View, @@ -25,6 +24,7 @@ import { Form, RangeSlider, Slider, + TabPanels, TextField, } from './spectrum'; import { ELEMENT_KEY, ElementNode, isElementNode } from './ElementUtils'; diff --git a/plugins/ui/src/js/src/elements/spectrum/TabPanels.tsx b/plugins/ui/src/js/src/elements/spectrum/TabPanels.tsx new file mode 100644 index 000000000..b13310dc5 --- /dev/null +++ b/plugins/ui/src/js/src/elements/spectrum/TabPanels.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { + TabPanels as SpectrumTabPanels, + SpectrumTabPanelsProps, +} from '@adobe/react-spectrum'; + +function TabPanels(props: SpectrumTabPanelsProps) { + const { UNSAFE_style: unsafeStyle, ...otherProps } = props; + + return ( + + ); +} + +TabPanels.displayName = 'TabPanels'; + +export default TabPanels; diff --git a/plugins/ui/src/js/src/elements/spectrum/index.ts b/plugins/ui/src/js/src/elements/spectrum/index.ts index 27c96d7f3..c7b144d93 100644 --- a/plugins/ui/src/js/src/elements/spectrum/index.ts +++ b/plugins/ui/src/js/src/elements/spectrum/index.ts @@ -10,4 +10,5 @@ export { default as Flex } from './Flex'; export { default as Form } from './Form'; export { default as RangeSlider } from './RangeSlider'; export { default as Slider } from './Slider'; +export { default as TabPanels } from './TabPanels'; export { default as TextField } from './TextField'; From 6212bd5d84737f0638a813598ac3ea1af1663598 Mon Sep 17 00:00:00 2001 From: Vlad Babich Date: Tue, 5 Mar 2024 13:41:58 -0700 Subject: [PATCH 3/8] feat: Export plotly-express as a dashboard plugin (#329) Tested with DHE V+ (1.20231218.176) Core 0.32.1 Closes #308 BREAKING CHANGE: - `widget` type in the `PanelEvent.OPEN` event arguments has changed from `VariableDefinition` to `VariableDescriptor` --- .../src/js/src/DashboardPlugin.tsx | 77 +++++++++++++++++++ .../src/js/src/PlotlyExpressChartUtils.ts | 11 ++- plugins/plotly-express/src/js/src/index.ts | 2 + 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 plugins/plotly-express/src/js/src/DashboardPlugin.tsx diff --git a/plugins/plotly-express/src/js/src/DashboardPlugin.tsx b/plugins/plotly-express/src/js/src/DashboardPlugin.tsx new file mode 100644 index 000000000..53b335620 --- /dev/null +++ b/plugins/plotly-express/src/js/src/DashboardPlugin.tsx @@ -0,0 +1,77 @@ +import { useCallback, DragEvent, useEffect } from 'react'; +import shortid from 'shortid'; +import { + DashboardPluginComponentProps, + LayoutUtils, + PanelEvent, + useListener, +} from '@deephaven/dashboard'; +import type { VariableDescriptor } from '@deephaven/jsapi-types'; +import PlotlyExpressChartPanel from './PlotlyExpressChartPanel.js'; +import type { PlotlyChartWidget } from './PlotlyExpressChartUtils.js'; + +export function DashboardPlugin( + props: DashboardPluginComponentProps +): JSX.Element | null { + const { id, layout, registerComponent } = props; + + const handlePanelOpen = useCallback( + async ({ + dragEvent, + fetch, + metadata = {}, + panelId = shortid.generate(), + widget, + }: { + dragEvent?: DragEvent; + fetch: () => Promise; + metadata?: Record; + panelId?: string; + widget: VariableDescriptor; + }) => { + const { type, name } = widget; + if (type !== 'deephaven.plot.express.DeephavenFigure') { + return; + } + + const config = { + type: 'react-component' as const, + component: 'PlotlyPanel', + props: { + localDashboardId: id, + id: panelId, + metadata: { + ...metadata, + ...widget, + figure: name, + }, + fetch, + }, + title: name, + id: panelId, + }; + + const { root } = layout; + LayoutUtils.openComponent({ root, config, dragEvent }); + }, + [id, layout] + ); + + useEffect( + function registerComponentsAndReturnCleanup() { + const cleanups = [ + registerComponent('PlotlyPanel', PlotlyExpressChartPanel), + ]; + return () => { + cleanups.forEach(cleanup => cleanup()); + }; + }, + [registerComponent] + ); + + useListener(layout.eventHub, PanelEvent.OPEN, handlePanelOpen); + + return null; +} + +export default DashboardPlugin; diff --git a/plugins/plotly-express/src/js/src/PlotlyExpressChartUtils.ts b/plugins/plotly-express/src/js/src/PlotlyExpressChartUtils.ts index c2b193f98..c91c66f82 100644 --- a/plugins/plotly-express/src/js/src/PlotlyExpressChartUtils.ts +++ b/plugins/plotly-express/src/js/src/PlotlyExpressChartUtils.ts @@ -1,5 +1,14 @@ import type { Data, PlotlyDataLayoutConfig } from 'plotly.js'; -import type { Widget } from '@deephaven/jsapi-types'; +import type { Table, Widget } from '@deephaven/jsapi-types'; + +export interface PlotlyChartWidget { + getDataAsBase64(): string; + exportedObjects: { fetch(): Promise }[]; + addEventListener( + type: string, + fn: (event: CustomEvent) => () => void + ): void; +} export interface PlotlyChartWidgetData { type: string; diff --git a/plugins/plotly-express/src/js/src/index.ts b/plugins/plotly-express/src/js/src/index.ts index 9d83e0884..312a2bc95 100644 --- a/plugins/plotly-express/src/js/src/index.ts +++ b/plugins/plotly-express/src/js/src/index.ts @@ -1,5 +1,7 @@ import { PlotlyExpressPlugin } from './PlotlyExpressPlugin.js'; +// Export legacy dashboard plugin as named export for backwards compatibility +export * from './DashboardPlugin.js'; export * from './PlotlyExpressChartModel.js'; export * from './PlotlyExpressChartUtils.js'; From c32396b58e2b59fef50a8969d7480bdd111ec515 Mon Sep 17 00:00:00 2001 From: Mike Bender Date: Wed, 6 Mar 2024 08:47:16 -0500 Subject: [PATCH 4/8] chore(version): plotly-express-v0.5.0 --- plugins/plotly-express/CHANGELOG.md | 8 ++++++++ plugins/plotly-express/setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/plotly-express/CHANGELOG.md b/plugins/plotly-express/CHANGELOG.md index a7cd9f240..68d2af0a6 100644 --- a/plugins/plotly-express/CHANGELOG.md +++ b/plugins/plotly-express/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## plotly-express-v0.5.0 - 2024-03-06 +#### Build system +- Update dh ui packages to ^0.66.1 (#330) - (9433a98) - bmingles +#### Features +- Export plotly-express as a dashboard plugin (#329) - (6212bd5) - vbabich + +- - - + ## plotly-express-v0.4.1 - 2024-02-28 #### Bug Fixes - Scatter plots rendering at the wrong location (#324) - (dfe5c48) - mofojed diff --git a/plugins/plotly-express/setup.cfg b/plugins/plotly-express/setup.cfg index 2deaa712f..16ccd8b2f 100644 --- a/plugins/plotly-express/setup.cfg +++ b/plugins/plotly-express/setup.cfg @@ -3,7 +3,7 @@ name = deephaven-plugin-plotly-express description = Deephaven Chart Plugin long_description = file: README.md long_description_content_type = text/markdown -version = 0.4.1.dev0 +version = 0.5.0 url = https://github.com/deephaven/deephaven-plugins project_urls = Source Code = https://github.com/deephaven/deephaven-plugins From a970aa54901950224ba0aa651b7f8fb8ec4b758a Mon Sep 17 00:00:00 2001 From: Mike Bender Date: Wed, 6 Mar 2024 08:47:16 -0500 Subject: [PATCH 5/8] chore(version): update plotly-express version to 0.5.0 --- plugins/plotly-express/setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/plotly-express/setup.cfg b/plugins/plotly-express/setup.cfg index 16ccd8b2f..9b213314b 100644 --- a/plugins/plotly-express/setup.cfg +++ b/plugins/plotly-express/setup.cfg @@ -3,7 +3,7 @@ name = deephaven-plugin-plotly-express description = Deephaven Chart Plugin long_description = file: README.md long_description_content_type = text/markdown -version = 0.5.0 +version = 0.5.0.dev0 url = https://github.com/deephaven/deephaven-plugins project_urls = Source Code = https://github.com/deephaven/deephaven-plugins From 6a504091d0f54e62d3ac0f9988632307a679d06d Mon Sep 17 00:00:00 2001 From: Mike Bender Date: Wed, 6 Mar 2024 08:53:03 -0500 Subject: [PATCH 6/8] chore(version): Manually bump plotly-express - This is only needed for Enterprise --- package-lock.json | 2 +- plugins/plotly-express/src/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3ece149cb..61e9e0c0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35032,7 +35032,7 @@ }, "plugins/plotly-express/src/js": { "name": "@deephaven/js-plugin-plotly-express", - "version": "0.4.0", + "version": "0.5.0", "license": "Apache-2.0", "dependencies": { "@deephaven/chart": "0.64.0", diff --git a/plugins/plotly-express/src/js/package.json b/plugins/plotly-express/src/js/package.json index b262d8aa9..aa6f92ce7 100644 --- a/plugins/plotly-express/src/js/package.json +++ b/plugins/plotly-express/src/js/package.json @@ -1,6 +1,6 @@ { "name": "@deephaven/js-plugin-plotly-express", - "version": "0.4.0", + "version": "0.5.0", "description": "Deephaven plotly express plugin", "keywords": [ "Deephaven", From 6a635b9917afd2e0c688934f556aa4051805994a Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 6 Mar 2024 12:46:47 -0600 Subject: [PATCH 7/8] feat: Add utilities package (#331) Adds new utilities and packaging packages. These can be used for cross-plugin functionality. The first versions have functions that make it simpler to register js plugins. Part of the work for #316 and #194 After this is merged I will upload the initial versions to pypi, then use that for the follow-up PR to fix the issues themselves. --- .github/workflows/modified-plugin.yml | 4 + .github/workflows/release-python-package.yml | 8 + plugins/packaging/.gitignore | 8 + plugins/packaging/LICENSE | 202 ++++++++++++++++++ plugins/packaging/README.md | 25 +++ plugins/packaging/pyproject.toml | 3 + plugins/packaging/setup.cfg | 29 +++ .../deephaven/plugin/packaging/__init__.py | 1 + .../src/deephaven/plugin/packaging/utils.py | 43 ++++ plugins/utilities/.gitignore | 8 + plugins/utilities/LICENSE | 202 ++++++++++++++++++ plugins/utilities/README.md | 27 +++ plugins/utilities/pyproject.toml | 3 + plugins/utilities/setup.cfg | 31 +++ .../deephaven/plugin/utilities/__init__.py | 2 + .../utilities/dhe_safe_callback_wrapper.py | 59 +++++ .../src/deephaven/plugin/utilities/utils.py | 109 ++++++++++ tools/update_version.sh | 6 + 18 files changed, 770 insertions(+) create mode 100644 plugins/packaging/.gitignore create mode 100644 plugins/packaging/LICENSE create mode 100644 plugins/packaging/README.md create mode 100644 plugins/packaging/pyproject.toml create mode 100644 plugins/packaging/setup.cfg create mode 100644 plugins/packaging/src/deephaven/plugin/packaging/__init__.py create mode 100644 plugins/packaging/src/deephaven/plugin/packaging/utils.py create mode 100644 plugins/utilities/.gitignore create mode 100644 plugins/utilities/LICENSE create mode 100644 plugins/utilities/README.md create mode 100644 plugins/utilities/pyproject.toml create mode 100644 plugins/utilities/setup.cfg create mode 100644 plugins/utilities/src/deephaven/plugin/utilities/__init__.py create mode 100644 plugins/utilities/src/deephaven/plugin/utilities/dhe_safe_callback_wrapper.py create mode 100644 plugins/utilities/src/deephaven/plugin/utilities/utils.py diff --git a/.github/workflows/modified-plugin.yml b/.github/workflows/modified-plugin.yml index 4c4837347..eebae4032 100644 --- a/.github/workflows/modified-plugin.yml +++ b/.github/workflows/modified-plugin.yml @@ -11,6 +11,8 @@ on: - 'matplotlib-v*' - 'json-v*' - 'ui-v*' + - 'utilities-v*' + - 'packaging-v*' jobs: changes: @@ -32,6 +34,8 @@ jobs: matplotlib: plugins/matplotlib/** json: plugins/json/** ui: plugins/ui/** + utilities: plugins/utilities/** + packaging: plugins/packaging/** # Test all python packages that have been modified individually test-python: diff --git a/.github/workflows/release-python-package.yml b/.github/workflows/release-python-package.yml index 5ffa014a2..c8045590a 100644 --- a/.github/workflows/release-python-package.yml +++ b/.github/workflows/release-python-package.yml @@ -28,10 +28,18 @@ jobs: node-version: '18.x' registry-url: 'https://registry.npmjs.org' + - name: Check file existence + id: check_files + uses: andstor/file-existence-action@v3 + with: + files: "plugins/${{ inputs.package }}/src/js/package.json" + - name: Install npm dependencies + if: steps.check_files.outputs.files_exists == 'true' run: npm ci - name: Build npm packages + if: steps.check_files.outputs.files_exists == 'true' run: npm run build -- --scope "@deephaven/js-plugin-${{ inputs.package }}" - name: Set up Python diff --git a/plugins/packaging/.gitignore b/plugins/packaging/.gitignore new file mode 100644 index 000000000..3356dc1ce --- /dev/null +++ b/plugins/packaging/.gitignore @@ -0,0 +1,8 @@ +build/ +dist/ +.venv/ +/venv +*.egg-info/ +.idea +.DS_store +__pycache__/ \ No newline at end of file diff --git a/plugins/packaging/LICENSE b/plugins/packaging/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/plugins/packaging/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/packaging/README.md b/plugins/packaging/README.md new file mode 100644 index 000000000..6f83ea754 --- /dev/null +++ b/plugins/packaging/README.md @@ -0,0 +1,25 @@ +# Deephaven Plugin Packaging + +This is a Python package that stores cross-plugin utilities for packaging Deephaven plugins. +This package is used by the Deephaven plugin build process to create wheels for plugins. +If the functions need to be available at runtime, they should be added to `utilities` instead. +This is not a plugin on its own. + +## Build + +To create your build / development environment (skip the first two lines if you already have a venv): + +```sh +python -m venv .venv +source .venv/bin/activate +pip install --upgrade pip setuptools +pip install build +``` + +To build: + +```sh +python -m build --wheel +``` + +The wheel is stored in `dist/`. diff --git a/plugins/packaging/pyproject.toml b/plugins/packaging/pyproject.toml new file mode 100644 index 000000000..62df2b006 --- /dev/null +++ b/plugins/packaging/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=43.0.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/plugins/packaging/setup.cfg b/plugins/packaging/setup.cfg new file mode 100644 index 000000000..1d2e14b16 --- /dev/null +++ b/plugins/packaging/setup.cfg @@ -0,0 +1,29 @@ +[metadata] +name = deephaven-plugin-packaging +description = Deephaven Plugin Packaging +long_description = file: README.md +long_description_content_type = text/markdown +version = 0.0.1.dev0 +url = https://github.com/deephaven/deephaven-plugins +project_urls = + Source Code = https://github.com/deephaven/deephaven-plugins + Bug Tracker = https://github.com/deephaven/deephaven-plugins/issues +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: Apache Software License + Operating System :: OS Independent + Environment :: Plugins + Development Status :: 3 - Alpha +keywords = deephaven, plugin, packaging +author = Joe Numainville +author_email = josephnumainville@deephaven.io +platforms = any + +[options] +package_dir= + =src +packages=find_namespace: +include_package_data = True + +[options.packages.find] +where=src diff --git a/plugins/packaging/src/deephaven/plugin/packaging/__init__.py b/plugins/packaging/src/deephaven/plugin/packaging/__init__.py new file mode 100644 index 000000000..16281fe0b --- /dev/null +++ b/plugins/packaging/src/deephaven/plugin/packaging/__init__.py @@ -0,0 +1 @@ +from .utils import * diff --git a/plugins/packaging/src/deephaven/plugin/packaging/utils.py b/plugins/packaging/src/deephaven/plugin/packaging/utils.py new file mode 100644 index 000000000..cfb1aa66d --- /dev/null +++ b/plugins/packaging/src/deephaven/plugin/packaging/utils.py @@ -0,0 +1,43 @@ +import shutil +import os +import subprocess + +__all__ = ["package_js"] + + +def package_js(js_dir: str, dest_dir: str) -> None: + """ + Package the built JS files at the given JS directory and unpack them into the destination directory. + + Args: + js_dir: + The directory containing the JS files + dest_dir: + The directory to unpack the JS files into + """ + dist_dir = os.path.join(js_dir, "dist") + build_dir = os.path.join(js_dir, "build") + package_dir = os.path.join(build_dir, "package") + + # copy the bundle to the directory + # the path may not exist (e.g. when running tests) + # so it is not strictly necessary to copy the bundle + if os.path.exists(dist_dir): + # ignore errors as the directory may not exist + shutil.rmtree(build_dir, ignore_errors=True) + shutil.rmtree(dest_dir, ignore_errors=True) + + os.makedirs(build_dir, exist_ok=True) + + # pack and unpack into the js directory + subprocess.run( + ["npm", "pack", "--pack-destination", "build"], cwd=js_dir, check=True + ) + # it is assumed that there is only one tarball in the directory + files = os.listdir(build_dir) + for file in files: + subprocess.run(["tar", "-xzf", file], cwd=build_dir, check=True) + os.remove(os.path.join(build_dir, file)) + + # move the package directory to the expected package location + shutil.move(package_dir, dest_dir) diff --git a/plugins/utilities/.gitignore b/plugins/utilities/.gitignore new file mode 100644 index 000000000..3356dc1ce --- /dev/null +++ b/plugins/utilities/.gitignore @@ -0,0 +1,8 @@ +build/ +dist/ +.venv/ +/venv +*.egg-info/ +.idea +.DS_store +__pycache__/ \ No newline at end of file diff --git a/plugins/utilities/LICENSE b/plugins/utilities/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/plugins/utilities/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/utilities/README.md b/plugins/utilities/README.md new file mode 100644 index 000000000..48f8ac1c0 --- /dev/null +++ b/plugins/utilities/README.md @@ -0,0 +1,27 @@ +# Deephaven Plugin Utilities + +This is a Python package that stores cross-plugin utilities for building Deephaven plugins. +If the functions are only used during the build process, they should be added to `packaging` instead. +This is not a plugin on its own. + +## Build + +To create your build / development environment (skip the first two lines if you already have a venv): + +```sh +python -m venv .venv +source .venv/bin/activate +pip install --upgrade pip setuptools +pip install build deephaven-plugin +``` + +To build: + +```sh +python -m build --wheel +``` + +The wheel is stored in `dist/`. + +To test within [deephaven-core](https://github.com/deephaven/deephaven-core), note where this wheel is stored (using `pwd`, for example). +Then, follow the directions in the top-level README.md to install the wheel into your Deephaven environment. diff --git a/plugins/utilities/pyproject.toml b/plugins/utilities/pyproject.toml new file mode 100644 index 000000000..62df2b006 --- /dev/null +++ b/plugins/utilities/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=43.0.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/plugins/utilities/setup.cfg b/plugins/utilities/setup.cfg new file mode 100644 index 000000000..78b1efcd4 --- /dev/null +++ b/plugins/utilities/setup.cfg @@ -0,0 +1,31 @@ +[metadata] +name = deephaven-plugin-utilities +description = Deephaven Plugin Utilities +long_description = file: README.md +long_description_content_type = text/markdown +version = 0.0.1.dev0 +url = https://github.com/deephaven/deephaven-plugins +project_urls = + Source Code = https://github.com/deephaven/deephaven-plugins + Bug Tracker = https://github.com/deephaven/deephaven-plugins/issues +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: Apache Software License + Operating System :: OS Independent + Environment :: Plugins + Development Status :: 3 - Alpha +keywords = deephaven, plugin, utilities +author = Joe Numainville +author_email = josephnumainville@deephaven.io +platforms = any + +[options] +package_dir= + =src +packages=find_namespace: +install_requires = + deephaven-plugin>=0.6.0 +include_package_data = True + +[options.packages.find] +where=src diff --git a/plugins/utilities/src/deephaven/plugin/utilities/__init__.py b/plugins/utilities/src/deephaven/plugin/utilities/__init__.py new file mode 100644 index 000000000..1b2cca4e7 --- /dev/null +++ b/plugins/utilities/src/deephaven/plugin/utilities/__init__.py @@ -0,0 +1,2 @@ +from .utils import * +from .dhe_safe_callback_wrapper import * diff --git a/plugins/utilities/src/deephaven/plugin/utilities/dhe_safe_callback_wrapper.py b/plugins/utilities/src/deephaven/plugin/utilities/dhe_safe_callback_wrapper.py new file mode 100644 index 000000000..21c9034e8 --- /dev/null +++ b/plugins/utilities/src/deephaven/plugin/utilities/dhe_safe_callback_wrapper.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from typing import Type +import logging +from deephaven.plugin import Callback, Plugin +from deephaven.plugin.js import JsPlugin +from .utils import is_enterprise_environment + +logger = logging.getLogger(__name__) + +__all__ = ["DheSafeCallbackWrapper"] + + +class DheSafeCallbackWrapper(Callback): + """ + + A wrapper around the Callback class that provides a safe way to register plugins. + + """ + + def __init__(self, callback: Callback): + self._callback = callback + + def register(self, plugin: Plugin | Type[Plugin]) -> None: + """ + Register a plugin with the provided callback + + Args: + plugin: The plugin to register + + """ + if isinstance(plugin, JsPlugin) or ( + isinstance(plugin, type) and issubclass(plugin, JsPlugin) + ): + self._register_js(plugin) + else: + self._callback.register(plugin) + + def _register_js(self, js_plugin: JsPlugin | Type[JsPlugin]) -> None: + """ + Attempt to register a JS plugin. + If failed and enterprise is detected, a debug message will be logged. + If failed and enterprise is not detected, an exception will be raised. + + Args: + js_plugin: + The JS plugin to register + """ + try: + self._callback.register(js_plugin) + except RuntimeError as e: + if is_enterprise_environment(): + logger.debug( + f"Failed to register {js_plugin} embedded in Python plugin. Skipping." + ) + else: + raise RuntimeError( + f"Failed to register {js_plugin} embedded in Python plugin: {e}" + ) diff --git a/plugins/utilities/src/deephaven/plugin/utilities/utils.py b/plugins/utilities/src/deephaven/plugin/utilities/utils.py new file mode 100644 index 000000000..62bbe0a0f --- /dev/null +++ b/plugins/utilities/src/deephaven/plugin/utilities/utils.py @@ -0,0 +1,109 @@ +import abc +import logging +from functools import partial +from typing import Callable, ContextManager +import importlib.resources +import json +import pathlib +import sys +from deephaven.plugin.js import JsPlugin + +logger = logging.getLogger(__name__) + +__all__ = ["is_enterprise_environment", "create_js_plugin"] + + +def is_enterprise_environment() -> bool: + """ + Check if the environment is an enterprise environment. + + Returns: + True if the environment is an enterprise environment, False otherwise + """ + # TODO: better implementation after https://deephaven.atlassian.net/browse/DH-16573 + return any("coreplus" in path or "dnd" in path for path in sys.path) + + +def _create_from_npm_package_json( + path_provider: Callable[[], ContextManager[pathlib.Path]], plugin_class: abc.ABCMeta +) -> JsPlugin: + """ + Create a JsPlugin from an npm package.json file. + + Args: + path_provider: + A function that returns a context manager that provides a pathlib.Path to the package.json file + plugin_class: + The class to create. It must be a subclass of JsPlugin. + """ + with path_provider() as tmp_js_path: + js_path = tmp_js_path + if not js_path.exists(): + raise Exception( + f"Package is not installed in a normal python filesystem, '{js_path}' does not exist" + ) + with (js_path / "package.json").open("rb") as f: + package_json = json.load(f) + return plugin_class( + package_json["name"], + package_json["version"], + package_json["main"], + js_path, + ) + + +def _resource_js_path_provider( + package_namespace: str, js_name: str +) -> Callable[[], ContextManager[pathlib.Path]]: + """ + Get the path to a resource in a package. + + Args: + package_namespace: + The package namespace + js_name: + The resource name + """ + return partial(_resource_js_path, package_namespace, js_name) + + +def _resource_js_path( + package_namespace: str, js_name: str +) -> ContextManager[pathlib.Path]: + """ + Get the path to a resource in a package. + + Args: + package_namespace: + The package namespace + js_name: + The resource name + """ + + if sys.version_info < (3, 9): + return importlib.resources.path(package_namespace, js_name) + else: + return importlib.resources.as_file( + importlib.resources.files(package_namespace).joinpath(js_name) + ) + + +def create_js_plugin( + package_namespace: str, js_name: str, plugin_class: abc.ABCMeta +) -> JsPlugin: + """ + Create a JsPlugin from an npm package.json file. + + Args: + package_namespace: + The package namespace + js_name: + The resource name + plugin_class: + The class to create. It must be a subclass of JsPlugin. + + Returns: + The created JsPlugin + """ + js_path = _resource_js_path_provider(package_namespace, js_name) + return _create_from_npm_package_json(js_path, plugin_class) diff --git a/tools/update_version.sh b/tools/update_version.sh index 3892a1873..0eee3ab85 100755 --- a/tools/update_version.sh +++ b/tools/update_version.sh @@ -139,6 +139,12 @@ case "$package" in update_file ui/src/js/package.json '"version": "' '",' update_file ui/src/deephaven/ui/__init__.py '__version__ = "' '"' "$extra" ;; + utilities) + update_file utilities/setup.cfg 'version = ' '' "$extra" + ;; + packaging) + update_file packaging/setup.cfg 'version = ' '' "$extra" + ;; *) { log_error "Unhandled plugin $package. You will need to add wiring in $SCRIPT_NAME" From 5bef1a46dd2d1d58bd17aa17c10a48f0867c9d78 Mon Sep 17 00:00:00 2001 From: Steven Wu Date: Wed, 6 Mar 2024 14:37:08 -0500 Subject: [PATCH 8/8] test: add e2e (#314) - Adds #274 - Create E2E tests and add to a GitHub Actions workflow - Both the plugins and the Playwright are run through Docker, which can be run both locally and on CI As for cache, caching Docker layers in GitHub Actions takes longer because of loading cache and images. --------- Co-authored-by: Mike Bender --- .dockerignore | 8 +- .github/workflows/e2e.yml | 43 +++++++ .gitignore | 8 +- Dockerfile | 2 +- README.md | 8 ++ docker-compose.yml | 28 +++++ docker/config/deephaven.prop | 1 + package-lock.json | 107 +++++++++++++++++- package.json | 7 +- playwright-docker.config.ts | 13 +++ playwright.config.ts | 94 +++++++++++++++ tests/Dockerfile | 9 ++ tests/app.d/express.py | 11 ++ tests/app.d/matplotlib.py | 5 + tests/app.d/tests.app | 8 ++ tests/app.d/ui.py | 20 ++++ tests/express.spec.ts | 8 ++ .../Express-loads-1-chromium-linux.png | Bin 0 -> 7521 bytes .../Express-loads-1-firefox-linux.png | Bin 0 -> 17489 bytes .../Express-loads-1-webkit-linux.png | Bin 0 -> 6704 bytes tests/matplotlib.spec.ts | 10 ++ .../Matplotlib-loads-1-chromium-linux.png | Bin 0 -> 14529 bytes .../Matplotlib-loads-1-firefox-linux.png | Bin 0 -> 29277 bytes .../Matplotlib-loads-1-webkit-linux.png | Bin 0 -> 12040 bytes tests/ui.spec.ts | 8 ++ .../UI-loads-1-chromium-linux.png | Bin 0 -> 7332 bytes .../UI-loads-1-firefox-linux.png | Bin 0 -> 14344 bytes .../UI-loads-1-webkit-linux.png | Bin 0 -> 6594 bytes tests/utils.ts | 57 ++++++++++ tools/run_docker.sh | 13 +++ tsconfig.json | 3 +- 31 files changed, 460 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/e2e.yml create mode 100644 playwright-docker.config.ts create mode 100644 playwright.config.ts create mode 100644 tests/Dockerfile create mode 100644 tests/app.d/express.py create mode 100644 tests/app.d/matplotlib.py create mode 100644 tests/app.d/tests.app create mode 100644 tests/app.d/ui.py create mode 100644 tests/express.spec.ts create mode 100644 tests/express.spec.ts-snapshots/Express-loads-1-chromium-linux.png create mode 100644 tests/express.spec.ts-snapshots/Express-loads-1-firefox-linux.png create mode 100644 tests/express.spec.ts-snapshots/Express-loads-1-webkit-linux.png create mode 100644 tests/matplotlib.spec.ts create mode 100644 tests/matplotlib.spec.ts-snapshots/Matplotlib-loads-1-chromium-linux.png create mode 100644 tests/matplotlib.spec.ts-snapshots/Matplotlib-loads-1-firefox-linux.png create mode 100644 tests/matplotlib.spec.ts-snapshots/Matplotlib-loads-1-webkit-linux.png create mode 100644 tests/ui.spec.ts create mode 100644 tests/ui.spec.ts-snapshots/UI-loads-1-chromium-linux.png create mode 100644 tests/ui.spec.ts-snapshots/UI-loads-1-firefox-linux.png create mode 100644 tests/ui.spec.ts-snapshots/UI-loads-1-webkit-linux.png create mode 100644 tests/utils.ts create mode 100755 tools/run_docker.sh diff --git a/.dockerignore b/.dockerignore index 937948ab8..aa00b07c7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -41,4 +41,10 @@ playwright/.cache/ **/.gitignore # Tests are copied to the docker container, as it modifies them -tests/ \ No newline at end of file +tests/ + +# Playwright +test-results/ +playwright-report/ +blob-report/ +playwright/.cache/ diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..26a098adc --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,43 @@ +name: End-to-end Tests + +on: + push: + branches: + - main + - 'release/**' + pull_request: + branches: + - main + - 'release/**' + +jobs: + e2e-test: + runs-on: ubuntu-22.04 + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run tests + run: "./tools/run_docker.sh e2e-tests" + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 90 + + - name: Dump server logs + if: failure() + run: docker logs deephaven-plugins > /tmp/server-log.txt + + - name: Upload server logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: server-logs + path: /tmp/server-log.txt \ No newline at end of file diff --git a/.gitignore b/.gitignore index 812a2add3..ae0a65d79 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,12 @@ docker-compose.override.yml # Ignore temporary files created during a release releases/ +# Playwright +test-results/ +playwright-report/ +blob-report/ +playwright/.cache/ + # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* -replay_pid* \ No newline at end of file +replay_pid* diff --git a/Dockerfile b/Dockerfile index 3d437a8df..f428390dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,4 +55,4 @@ COPY --link docker/config/deephaven.prop /opt/deephaven/config/deephaven.prop COPY --link docker/data /data # Set the environment variable to enable the JS plugins embedded in Python -ENV DEEPHAVEN_ENABLE_PY_JS=true \ No newline at end of file +ENV DEEPHAVEN_ENABLE_PY_JS=true diff --git a/README.md b/README.md index bba85aea3..6f49f9c9a 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,14 @@ To bypass the pre-commit hook, you can commit with the `--no-verify` flag, for e git commit --no-verify -m "commit message"` ``` +### Running end-to-end tests + +We use [Playwright](https://playwright.dev/) for end-to-end tests. We test against Chrome, Firefox, and Webkit (Safari). Snapshots from E2E tests are only run against Linux so they can be validated in CI. + +You should be able to pass arguments to these commands as if you were running Playwright via CLI directly. For example, to test only `matplotlib.spec.ts` you could run `npm run e2e:docker -- ./tests/matplotlib.spec.ts`, or to test only `matplotlib.spec.ts` in Firefox, you could run `npm run e2e:docker -- --project firefox ./tests/matplotlib.spec.ts`. See [Playwright CLI](https://playwright.dev/docs/test-cli) for more details. + +It is highly recommended to use `npm run e2e:docker` (instead of `npm run e2e`) as CI also uses the same environment. You can also use `npm run e2e:update-snapshots` to regenerate snapshots in said environment. + ### Running Python tests The above steps will also set up `tox` to run tests for the python plugins that support it. diff --git a/docker-compose.yml b/docker-compose.yml index 307de2637..f78e91799 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,5 +7,33 @@ services: pull: true ports: - '${DEEPHAVEN_PORT:-10000}:10000' + expose: + - 10000 volumes: - ./docker/data/:/data + - ./tests/app.d:/app.d + environment: + - START_OPTS=-Xmx4g -DAuthHandlers=io.deephaven.auth.AnonymousAuthenticationHandler -Ddeephaven.console.type=python -Ddeephaven.application.dir=./app.d + + e2e-tests: + build: + dockerfile: ./tests/Dockerfile + ports: + - '9323:9323' + ipc: host + volumes: + - ./tests:/work/tests + - ./test-results:/work/test-results + - ./playwright-report:/work/playwright-report + entrypoint: "npx playwright test --config=playwright-docker.config.ts" + depends_on: + deephaven-plugins: + condition: service_healthy + + update-snapshots: + extends: + service: e2e-tests + entrypoint: 'npx playwright test --config=playwright-docker.config.ts --update-snapshots' + depends_on: + deephaven-plugins: + condition: service_healthy diff --git a/docker/config/deephaven.prop b/docker/config/deephaven.prop index f415db0c6..114ab0959 100644 --- a/docker/config/deephaven.prop +++ b/docker/config/deephaven.prop @@ -3,6 +3,7 @@ includefiles=dh-defaults.prop deephaven.console.type=python # Add all plugins that you want installed here +deephaven.jsPlugins.@deephaven/js-plugin-matplotlib=/opt/deephaven/config/plugins/plugins/matplotlib/src/js deephaven.jsPlugins.@deephaven/js-plugin-ui=/opt/deephaven/config/plugins/plugins/ui/src/js # Anonymous authentication so we don't need to put in a password diff --git a/package-lock.json b/package-lock.json index 61e9e0c0d..efcea6308 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,11 +13,13 @@ "@deephaven/eslint-config": "^0.40.0", "@deephaven/prettier-config": "^0.40.0", "@deephaven/tsconfig": "^0.40.0", + "@playwright/test": "^1.41.2", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^12.1.3", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.4.3", "@types/jest": "^29.2.5", + "@types/node": "^20.11.17", "@types/prop-types": "^15.7.10", "@types/shortid": "^0.0.29", "eslint": "^8.37.0", @@ -7863,6 +7865,21 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", + "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==", + "dev": true, + "dependencies": { + "playwright": "1.41.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@plotly/d3": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.8.1.tgz", @@ -11901,9 +11918,9 @@ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "node_modules/@types/node": { - "version": "20.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.1.tgz", - "integrity": "sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==", + "version": "20.11.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", + "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -28791,6 +28808,50 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz", + "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==", + "dev": true, + "dependencies": { + "playwright-core": "1.41.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz", + "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/plotly.js": { "version": "2.29.1", "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-2.29.1.tgz", @@ -42901,6 +42962,15 @@ "dev": true, "optional": true }, + "@playwright/test": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", + "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==", + "dev": true, + "requires": { + "playwright": "1.41.2" + } + }, "@plotly/d3": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.8.1.tgz", @@ -46029,9 +46099,9 @@ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "@types/node": { - "version": "20.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.1.tgz", - "integrity": "sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==", + "version": "20.11.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", + "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", "dev": true, "requires": { "undici-types": "~5.26.4" @@ -58416,6 +58486,31 @@ } } }, + "playwright": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz", + "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.41.2" + }, + "dependencies": { + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + } + } + }, + "playwright-core": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz", + "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==", + "dev": true + }, "plotly.js": { "version": "2.29.1", "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-2.29.1.tgz", diff --git a/package.json b/package.json index 45d85b0e7..e656220ed 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "./plugins/*/src/js/" ], "scripts": { - "docker": "docker compose up --build", + "docker": "docker compose up deephaven-plugins --build", "start": "run-p \"start:packages -- {@}\" serve:plugins --", "build": "lerna run build --stream", "serve:plugins": "vite", @@ -15,6 +15,9 @@ "test:ci": "run-p test:ci:*", "test:ci:unit": "jest --config jest.config.unit.cjs --ci --cacheDirectory $PWD/.jest-cache", "test:ci:lint": "jest --config jest.config.lint.cjs --ci --cacheDirectory $PWD/.jest-cache", + "e2e": "playwright run", + "e2e:docker": "./tools/run_docker.sh e2e-tests", + "e2e:update-snapshots": "./tools/run_docker.sh update-snapshots", "update-dh-packages": "lerna run update-dh-packages" }, "devDependencies": { @@ -22,11 +25,13 @@ "@deephaven/eslint-config": "^0.40.0", "@deephaven/prettier-config": "^0.40.0", "@deephaven/tsconfig": "^0.40.0", + "@playwright/test": "^1.41.2", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^12.1.3", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.4.3", "@types/jest": "^29.2.5", + "@types/node": "^20.11.17", "@types/prop-types": "^15.7.10", "@types/shortid": "^0.0.29", "eslint": "^8.37.0", diff --git a/playwright-docker.config.ts b/playwright-docker.config.ts new file mode 100644 index 000000000..1151d0fe5 --- /dev/null +++ b/playwright-docker.config.ts @@ -0,0 +1,13 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import DefaultConfig from './playwright.config'; + +const config: PlaywrightTestConfig = { + ...DefaultConfig, + use: { + ...DefaultConfig.use, + baseURL: 'http://deephaven-plugins:10000/ide/', + }, + reporter: process.env.CI ? [['github'], ['html']] : DefaultConfig.reporter, +}; + +export default config; diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..abdb97233 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,94 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + + +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 120 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 15000, + }, + /* Default to run each suite in parallel, suites and optional opt out */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 1 : 0, + /* Default to 50% of cores, don't want too many as core or web will become bottleneck */ + workers: undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + /* Use host 0.0.0.0 so it can be forwarded from within docker */ + reporter: [['html', { host: '0.0.0.0', port: 9323 }]], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + + /* Navigation timeout for how long it takes to navigate to a page */ + navigationTimeout: 60 * 1000, + + /* Base URL to use in actions like `await page.goto('')`. */ + baseURL: 'http://localhost:10000/ide/', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + /* Retain videos on failure for easier debugging */ + video: 'retain-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'], + // https://playwright.dev/docs/test-use-options#more-browser-and-context-options + launchOptions: { + // https://playwright.dev/docs/api/class-browsertype#browser-type-launch-option-firefox-user-prefs + firefoxUserPrefs: { + // By default, headless Firefox runs as though no pointers capabilities + // are available. + // https://github.com/microsoft/playwright/issues/7769#issuecomment-966098074 + // + // This impacts React Spectrum which uses an '(any-pointer: fine)' + // media query to determine font size. It also causes certain chart + // elements to always be visible that should only be visible on + // hover. + // + // Available values for pointer capabilities: + // NO_POINTER = 0x00; + // COARSE_POINTER = 0x01; + // FINE_POINTER = 0x02; + // HOVER_CAPABLE_POINTER = 0x04; + // + // Setting to 0x02 | 0x04 says the system supports a mouse + 'ui.primaryPointerCapabilities': 0x02 | 0x04, + 'ui.allPointerCapabilities': 0x02 | 0x04, + }, + }, + }, + }, + + { + name: 'webkit', + use: { + ...devices['Desktop Safari'], + }, + }, + ], +}; + +export default config; diff --git a/tests/Dockerfile b/tests/Dockerfile new file mode 100644 index 000000000..00596c498 --- /dev/null +++ b/tests/Dockerfile @@ -0,0 +1,9 @@ +# syntax=docker/dockerfile:1 +# Dockerfile for updating the snapshots. +# Expects to be run from the root of the web-client-ui repo +FROM mcr.microsoft.com/playwright:v1.41.2-jammy AS playwright +WORKDIR /work/ + +RUN npm install @playwright/test@1.41.2 +COPY playwright.config.ts . +COPY playwright-docker.config.ts . diff --git a/tests/app.d/express.py b/tests/app.d/express.py new file mode 100644 index 000000000..217055b4e --- /dev/null +++ b/tests/app.d/express.py @@ -0,0 +1,11 @@ +from deephaven.column import int_col, string_col +from deephaven import new_table +import deephaven.plot.express as dx + +express_source = new_table( + [ + string_col("Categories", ["A", "B", "C"]), + int_col("Values", [1, 3, 5]), + ] +) +express_fig = dx.bar(table=express_source, x="Categories", y="Values") diff --git a/tests/app.d/matplotlib.py b/tests/app.d/matplotlib.py new file mode 100644 index 000000000..8617ed23d --- /dev/null +++ b/tests/app.d/matplotlib.py @@ -0,0 +1,5 @@ +import matplotlib.pyplot as plt + +matplotlib_fig = plt.figure() +ax = matplotlib_fig.subplots() +ax.plot([1, 2, 3, 4], [4, 2, 6, 7]) diff --git a/tests/app.d/tests.app b/tests/app.d/tests.app new file mode 100644 index 000000000..43694c37f --- /dev/null +++ b/tests/app.d/tests.app @@ -0,0 +1,8 @@ +type=script +scriptType=python +enabled=true +id=web.test +name=Plugins Test Application +file_0=express.py +file_1=matplotlib.py +file_2=ui.py diff --git a/tests/app.d/ui.py b/tests/app.d/ui.py new file mode 100644 index 000000000..30eb89f1d --- /dev/null +++ b/tests/app.d/ui.py @@ -0,0 +1,20 @@ +import deephaven.ui as ui +from deephaven.ui import use_state + + +@ui.component +def my_component(): + count, set_count = use_state(0) + text, set_text = use_state("hello") + + return ui.flex( + ui.action_button( + f"You pressed me {count} times", on_press=lambda _: set_count(count + 1) + ), + ui.text_field(value=text, on_change=set_text), + ui.text(f"You typed {text}"), + direction="column", + ) + + +ui_component = my_component() diff --git a/tests/express.spec.ts b/tests/express.spec.ts new file mode 100644 index 000000000..6859fdca8 --- /dev/null +++ b/tests/express.spec.ts @@ -0,0 +1,8 @@ +import { expect, test } from '@playwright/test'; +import { openPanel, gotoPage } from './utils'; + +test('Express loads', async ({ page }) => { + await gotoPage(page, ''); + await openPanel(page, 'express_fig', '.js-plotly-plot'); + await expect(page.locator('.iris-chart-panel')).toHaveScreenshot(); +}); diff --git a/tests/express.spec.ts-snapshots/Express-loads-1-chromium-linux.png b/tests/express.spec.ts-snapshots/Express-loads-1-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..11a9903c23b42a92e223505e384a479cb1c44a06 GIT binary patch literal 7521 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 literal 0 HcmV?d00001 diff --git a/tests/express.spec.ts-snapshots/Express-loads-1-webkit-linux.png b/tests/express.spec.ts-snapshots/Express-loads-1-webkit-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..e60915a1b4f75b5273fa8cf8c58f8ed357b17632 GIT binary patch literal 6704 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 { + await gotoPage(page, ''); + await openPanel(page, 'matplotlib_fig', '.matplotlib-view'); + await expect( + page.getByRole('img', { name: 'Matplotlib render' }) + ).toHaveScreenshot(); +}); diff --git a/tests/matplotlib.spec.ts-snapshots/Matplotlib-loads-1-chromium-linux.png b/tests/matplotlib.spec.ts-snapshots/Matplotlib-loads-1-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..55cb9766a9dc893de926f7229c2e4c2a11470608 GIT binary patch literal 14529 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 literal 0 HcmV?d00001 diff --git a/tests/matplotlib.spec.ts-snapshots/Matplotlib-loads-1-firefox-linux.png b/tests/matplotlib.spec.ts-snapshots/Matplotlib-loads-1-firefox-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..128337c7653a33b20e92fbc116368608ba8de8f3 GIT binary patch literal 29277 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 G<9`9}-&EWH literal 0 HcmV?d00001 diff --git a/tests/matplotlib.spec.ts-snapshots/Matplotlib-loads-1-webkit-linux.png b/tests/matplotlib.spec.ts-snapshots/Matplotlib-loads-1-webkit-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..3f0bfee7727ca65be4e308ce63333932f241e3e3 GIT binary patch literal 12040 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 literal 0 HcmV?d00001 diff --git a/tests/ui.spec.ts b/tests/ui.spec.ts new file mode 100644 index 000000000..ddc2897ff --- /dev/null +++ b/tests/ui.spec.ts @@ -0,0 +1,8 @@ +import { expect, test } from '@playwright/test'; +import { openPanel, gotoPage } from './utils'; + +test('UI loads', async ({ page }) => { + await gotoPage(page, ''); + await openPanel(page, 'ui_component', '.ui-portal-panel'); + await expect(page.locator('.ui-portal-panel')).toHaveScreenshot(); +}); diff --git a/tests/ui.spec.ts-snapshots/UI-loads-1-chromium-linux.png b/tests/ui.spec.ts-snapshots/UI-loads-1-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..3886c4dc64eb51923e3f9a89205d100f4ea544b0 GIT binary patch literal 7332 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 literal 0 HcmV?d00001 diff --git a/tests/ui.spec.ts-snapshots/UI-loads-1-firefox-linux.png b/tests/ui.spec.ts-snapshots/UI-loads-1-firefox-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..2f9c74f200608433c5e61264e8d3d6f46dee28b4 GIT binary patch literal 14344 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 { + await test.step(`Go to page (${url})`, async () => { + await page.goto(url, options); + await expect( + page.getByRole('progressbar', { name: 'Loading...', exact: true }) + ).not.toBeVisible(); + }); +} + +/** + * Opens a panel by clicking on the Panels button and then the panel button + * @param page The page + * @param name The name of the panel + * @param panelLocator The locator for the panel, passed to `page.locator` + */ +export async function openPanel( + page: Page, + name: string, + panelLocator = '.dh-panel' +) { + await test.step(`Open panel (${name})`, async () => { + const panelCount = await page.locator(panelLocator).count(); + + // open app panels menu + const appPanels = page.getByRole('button', { + name: 'Panels', + exact: true, + }); + await expect(appPanels).toBeEnabled(); + await appPanels.click(); + + // open panel + const targetPanel = page.getByRole('button', { name, exact: true }); + expect(targetPanel).toBeEnabled(); + await targetPanel.click(); + + // check for panel to be loaded + await expect(page.locator(panelLocator)).toHaveCount(panelCount + 1); + await expect(page.locator('.loading-spinner')).toHaveCount(0); + }); +} diff --git a/tools/run_docker.sh b/tools/run_docker.sh new file mode 100755 index 000000000..6d37d8f8f --- /dev/null +++ b/tools/run_docker.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Set pwd to this directory +pushd "$(dirname "$0")" + +# Start the containers +docker compose run --service-ports --rm --build "$@" +exit_code=$? +docker compose down + +# Reset pwd +popd +exit $exit_code diff --git a/tsconfig.json b/tsconfig.json index 6a66086f6..cd20a0503 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,8 @@ "plugins/**/**.ts", "plugins/**/**.tsx", "plugins/**/**.js", - "plugins/**/**.jsx" + "plugins/**/**.jsx", + "tests/**/**.ts" ], "exclude": ["plugins/*/src/js/dist/**/*"], "watchOptions": {