diff --git a/__mocks__/dh-core.js b/__mocks__/dh-core.js
index 680e8e5a88..d28c4b22e7 100644
--- a/__mocks__/dh-core.js
+++ b/__mocks__/dh-core.js
@@ -1259,6 +1259,8 @@ class CoreClient {
}
}
+CoreClient.LOGIN_TYPE_ANONYMOUS = 'MOCK_LOGIN_ANONYNOUS';
+
class FileContents {
static text(...text) {
return new FileContents(text.join(''));
diff --git a/jest.config.base.cjs b/jest.config.base.cjs
index 265320c81e..bcaa86a08f 100644
--- a/jest.config.base.cjs
+++ b/jest.config.base.cjs
@@ -18,6 +18,7 @@ module.exports = {
__dirname,
'./__mocks__/fileMock.js'
),
+ '^fira$': 'identity-obj-proxy',
'^monaco-editor$': path.join(
__dirname,
'node_modules',
diff --git a/jest.setup.ts b/jest.setup.ts
index 3ddbfe4d17..c38b2c3cda 100644
--- a/jest.setup.ts
+++ b/jest.setup.ts
@@ -29,3 +29,9 @@ Object.defineProperty(window, 'matchMedia', {
dispatchEvent: jest.fn(),
})),
});
+
+Object.defineProperty(document, 'fonts', {
+ value: {
+ ready: Promise.resolve(),
+ },
+});
diff --git a/package-lock.json b/package-lock.json
index a85b83adaa..ce5b6093ea 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"./packages/*"
],
"dependencies": {
+ "@deephaven/app-utils": "file:packages/app-utils",
"@deephaven/babel-preset": "file:packages/babel-preset",
"@deephaven/chart": "file:packages/chart",
"@deephaven/code-studio": "file:packages/code-studio",
@@ -1924,6 +1925,14 @@
"node": ">=0.1.90"
}
},
+ "node_modules/@deephaven/app-utils": {
+ "resolved": "packages/app-utils",
+ "link": true
+ },
+ "node_modules/@deephaven/auth-plugins": {
+ "resolved": "packages/auth-plugins",
+ "link": true
+ },
"node_modules/@deephaven/babel-preset": {
"resolved": "packages/babel-preset",
"link": true
@@ -5705,20 +5714,17 @@
}
},
"node_modules/@paciolan/remote-component": {
- "version": "2.7.2",
- "license": "MIT",
+ "version": "2.13.0",
+ "resolved": "https://registry.npmjs.org/@paciolan/remote-component/-/remote-component-2.13.0.tgz",
+ "integrity": "sha512-VESHSqjYf3kpDmqpc1Fx9e5Ccnyc1fbCVrBGSp5yRw1+6oRwzoFlDGc27u8Af3vDl1RJD/4aldaeMZ8f2Btojg==",
"dependencies": {
- "@paciolan/remote-module-loader": "^2.4.0"
+ "@paciolan/remote-module-loader": "^3.0.2"
},
"peerDependencies": {
"react": ">=16.9",
"react-dom": ">=16.9"
}
},
- "node_modules/@paciolan/remote-component/node_modules/@paciolan/remote-module-loader": {
- "version": "2.6.2",
- "license": "MIT"
- },
"node_modules/@paciolan/remote-module-loader": {
"version": "3.0.2",
"license": "MIT"
@@ -25681,6 +25687,103 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "packages/app-utils": {
+ "name": "@deephaven/app-utils",
+ "version": "0.35.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@deephaven/auth-plugins": "file:../auth-plugins",
+ "@deephaven/components": "file:../components",
+ "@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
+ "@deephaven/jsapi-types": "file:../jsapi-types",
+ "@deephaven/log": "file:../log",
+ "@paciolan/remote-component": "2.13.0",
+ "@paciolan/remote-module-loader": "^3.0.2",
+ "fira": "mozilla/fira#4.202"
+ },
+ "devDependencies": {
+ "@deephaven/redux": "file:../redux",
+ "react": "^17.x",
+ "react-dom": "^17.x",
+ "react-redux": "^7.x",
+ "redux": "^4.x"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "@deephaven/redux": "file:../redux",
+ "react": "^17.x",
+ "react-dom": "^17.x",
+ "react-redux": "^7.x",
+ "redux": "^4.x"
+ }
+ },
+ "packages/auth-core-plugins": {
+ "name": "@deephaven/auth-core-plugins",
+ "version": "0.32.0",
+ "extraneous": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@deephaven/auth-plugin": "file:../auth-plugin",
+ "@deephaven/components": "file:../components",
+ "@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
+ "@deephaven/jsapi-types": "file:../jsapi-types",
+ "@deephaven/jsapi-utils": "file:../jsapi-utils",
+ "@deephaven/log": "file:../log"
+ },
+ "devDependencies": {
+ "@deephaven/tsconfig": "file:../tsconfig",
+ "@types/react": "^17.0.2"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "react": "^17.x"
+ }
+ },
+ "packages/auth-plugin": {
+ "name": "@deephaven/auth-plugin",
+ "version": "0.32.0",
+ "extraneous": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@deephaven/jsapi-types": "file:../jsapi-types"
+ },
+ "devDependencies": {
+ "@deephaven/tsconfig": "file:../tsconfig",
+ "@types/react": "^17.0.2"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "react": "^17.x"
+ }
+ },
+ "packages/auth-plugins": {
+ "name": "@deephaven/auth-plugins",
+ "version": "0.32.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@deephaven/components": "file:../components",
+ "@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
+ "@deephaven/jsapi-types": "file:../jsapi-types",
+ "@deephaven/jsapi-utils": "file:../jsapi-utils",
+ "@deephaven/log": "file:../log"
+ },
+ "devDependencies": {
+ "@deephaven/tsconfig": "file:../tsconfig",
+ "@types/react": "^17.0.2"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "react": "^17.x"
+ }
+ },
"packages/babel-preset": {
"name": "@deephaven/babel-preset",
"version": "0.36.0",
@@ -25731,6 +25834,8 @@
"version": "0.36.0",
"license": "Apache-2.0",
"dependencies": {
+ "@deephaven/app-utils": "file:../app-utils",
+ "@deephaven/auth-plugins": "file:../auth-plugins",
"@deephaven/chart": "file:../chart",
"@deephaven/components": "file:../components",
"@deephaven/console": "file:../console",
@@ -25743,6 +25848,7 @@
"@deephaven/icons": "file:../icons",
"@deephaven/iris-grid": "file:../iris-grid",
"@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
+ "@deephaven/jsapi-components": "file:../jsapi-components",
"@deephaven/jsapi-shim": "file:../jsapi-shim",
"@deephaven/jsapi-types": "file:../jsapi-types",
"@deephaven/jsapi-utils": "file:../jsapi-utils",
@@ -25754,11 +25860,8 @@
"@deephaven/utils": "file:../utils",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/react-fontawesome": "^0.2.0",
- "@paciolan/remote-component": "2.7.2",
- "@paciolan/remote-module-loader": "^3.0.2",
"classnames": "^2.3.1",
"event-target-shim": "^6.0.2",
- "fira": "mozilla/fira#4.202",
"jszip": "3.2.2",
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
@@ -26020,7 +26123,6 @@
"@deephaven/golden-layout": "file:../golden-layout",
"@deephaven/log": "file:../log",
"@deephaven/react-hooks": "file:../react-hooks",
- "@deephaven/redux": "file:../redux",
"@deephaven/utils": "file:../utils",
"deep-equal": "^2.0.5",
"lodash.ismatch": "^4.1.1",
@@ -26030,6 +26132,7 @@
},
"devDependencies": {
"@deephaven/mocks": "file:../mocks",
+ "@deephaven/redux": "file:../redux",
"@deephaven/tsconfig": "file:../tsconfig",
"@types/lodash.ismatch": "^4.4.0"
},
@@ -26037,6 +26140,7 @@
"node": ">=16"
},
"peerDependencies": {
+ "@deephaven/redux": "file:../redux",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react-redux": "^7.2.4"
@@ -26110,6 +26214,7 @@
"version": "0.36.0",
"license": "Apache-2.0",
"dependencies": {
+ "@deephaven/app-utils": "file:../app-utils",
"@deephaven/chart": "file:../chart",
"@deephaven/components": "file:../components",
"@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
@@ -26249,6 +26354,7 @@
"version": "0.36.0",
"license": "Apache-2.0",
"dependencies": {
+ "@deephaven/app-utils": "file:../app-utils",
"@deephaven/components": "file:../components",
"@deephaven/iris-grid": "file:../iris-grid",
"@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
@@ -26480,7 +26586,7 @@
"node": ">=16"
},
"peerDependencies": {
- "react": "^17.0.0"
+ "react": "^17.x"
}
},
"packages/icons": {
@@ -26552,10 +26658,12 @@
"dependencies": {
"@deephaven/components": "file:../components",
"@deephaven/jsapi-types": "file:../jsapi-types",
+ "@deephaven/jsapi-utils": "file:../jsapi-utils",
"@deephaven/log": "file:../log"
},
"devDependencies": {
- "@deephaven/tsconfig": "file:../tsconfig"
+ "@deephaven/tsconfig": "file:../tsconfig",
+ "react": "^17.x"
},
"engines": {
"node": ">=16"
@@ -26630,6 +26738,7 @@
"dependencies": {
"@deephaven/filters": "file:../filters",
"@deephaven/jsapi-shim": "file:../jsapi-shim",
+ "@deephaven/jsapi-types": "file:../jsapi-types",
"@deephaven/log": "file:../log",
"@deephaven/utils": "file:../utils",
"@react-stately/data": "^3.9.1",
@@ -26665,6 +26774,34 @@
"jest": "29.x"
}
},
+ "packages/plugin-utils": {
+ "name": "@deephaven/plugin-utils",
+ "version": "0.35.0",
+ "extraneous": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@deephaven/auth-core-plugins": "file:../auth-core-plugins",
+ "@deephaven/auth-plugin": "file:../auth-plugin",
+ "@deephaven/log": "file:../log",
+ "@paciolan/remote-component": "2.13.0",
+ "@paciolan/remote-module-loader": "^3.0.2"
+ },
+ "devDependencies": {
+ "react": "^17.x",
+ "react-dom": "^17.x",
+ "react-redux": "^7.x",
+ "redux": "^4.x"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "react": "^17.x",
+ "react-dom": "^17.x",
+ "react-redux": "^7.x",
+ "redux": "^4.x"
+ }
+ },
"packages/pouch-storage": {
"name": "@deephaven/pouch-storage",
"version": "0.36.0",
@@ -26686,7 +26823,7 @@
"node": ">=16"
},
"peerDependencies": {
- "react": "^17.0.0"
+ "react": "^17.x"
}
},
"packages/prettier-config": {
@@ -26712,7 +26849,7 @@
"node": ">=16"
},
"peerDependencies": {
- "react": "^17.0.0"
+ "react": "^17.x"
}
},
"packages/redux": {
@@ -26751,7 +26888,7 @@
"node": ">=16"
},
"peerDependencies": {
- "react": "^17.0.0"
+ "react": "^17.x"
}
},
"packages/stylelint-config": {
@@ -27949,6 +28086,36 @@
"version": "1.5.0",
"dev": true
},
+ "@deephaven/app-utils": {
+ "version": "file:packages/app-utils",
+ "requires": {
+ "@deephaven/auth-plugins": "file:../auth-plugins",
+ "@deephaven/components": "file:../components",
+ "@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
+ "@deephaven/jsapi-types": "file:../jsapi-types",
+ "@deephaven/log": "file:../log",
+ "@deephaven/redux": "file:../redux",
+ "@paciolan/remote-component": "2.13.0",
+ "@paciolan/remote-module-loader": "^3.0.2",
+ "fira": "mozilla/fira#4.202",
+ "react": "^17.x",
+ "react-dom": "^17.x",
+ "react-redux": "^7.x",
+ "redux": "^4.x"
+ }
+ },
+ "@deephaven/auth-plugins": {
+ "version": "file:packages/auth-plugins",
+ "requires": {
+ "@deephaven/components": "file:../components",
+ "@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
+ "@deephaven/jsapi-types": "file:../jsapi-types",
+ "@deephaven/jsapi-utils": "file:../jsapi-utils",
+ "@deephaven/log": "file:../log",
+ "@deephaven/tsconfig": "file:../tsconfig",
+ "@types/react": "^17.0.2"
+ }
+ },
"@deephaven/babel-preset": {
"version": "file:packages/babel-preset",
"requires": {
@@ -27985,6 +28152,8 @@
"@deephaven/code-studio": {
"version": "file:packages/code-studio",
"requires": {
+ "@deephaven/app-utils": "file:../app-utils",
+ "@deephaven/auth-plugins": "file:../auth-plugins",
"@deephaven/chart": "file:../chart",
"@deephaven/components": "file:../components",
"@deephaven/console": "file:../console",
@@ -27998,6 +28167,7 @@
"@deephaven/icons": "file:../icons",
"@deephaven/iris-grid": "file:../iris-grid",
"@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
+ "@deephaven/jsapi-components": "file:../jsapi-components",
"@deephaven/jsapi-shim": "file:../jsapi-shim",
"@deephaven/jsapi-types": "file:../jsapi-types",
"@deephaven/jsapi-utils": "file:../jsapi-utils",
@@ -28013,13 +28183,10 @@
"@deephaven/utils": "file:../utils",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/react-fontawesome": "^0.2.0",
- "@paciolan/remote-component": "2.7.2",
- "@paciolan/remote-module-loader": "^3.0.2",
"@vitejs/plugin-react-swc": "^3.2.0",
"autoprefixer": "^10.4.8",
"classnames": "^2.3.1",
"event-target-shim": "^6.0.2",
- "fira": "mozilla/fira#4.202",
"jszip": "3.2.2",
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
@@ -28243,6 +28410,7 @@
"@deephaven/embed-chart": {
"version": "file:packages/embed-chart",
"requires": {
+ "@deephaven/app-utils": "file:../app-utils",
"@deephaven/chart": "file:../chart",
"@deephaven/components": "file:../components",
"@deephaven/eslint-config": "file:../eslint-config",
@@ -28327,6 +28495,7 @@
"@deephaven/embed-grid": {
"version": "file:packages/embed-grid",
"requires": {
+ "@deephaven/app-utils": "file:../app-utils",
"@deephaven/components": "file:../components",
"@deephaven/eslint-config": "file:../eslint-config",
"@deephaven/iris-grid": "file:../iris-grid",
@@ -28512,8 +28681,10 @@
"requires": {
"@deephaven/components": "file:../components",
"@deephaven/jsapi-types": "file:../jsapi-types",
+ "@deephaven/jsapi-utils": "file:../jsapi-utils",
"@deephaven/log": "file:../log",
- "@deephaven/tsconfig": "file:../tsconfig"
+ "@deephaven/tsconfig": "file:../tsconfig",
+ "react": "^17.x"
}
},
"@deephaven/jsapi-components": {
@@ -28563,6 +28734,7 @@
"requires": {
"@deephaven/filters": "file:../filters",
"@deephaven/jsapi-shim": "file:../jsapi-shim",
+ "@deephaven/jsapi-types": "file:../jsapi-types",
"@deephaven/log": "file:../log",
"@deephaven/tsconfig": "file:../tsconfig",
"@deephaven/utils": "file:../utils",
@@ -31310,14 +31482,11 @@
}
},
"@paciolan/remote-component": {
- "version": "2.7.2",
+ "version": "2.13.0",
+ "resolved": "https://registry.npmjs.org/@paciolan/remote-component/-/remote-component-2.13.0.tgz",
+ "integrity": "sha512-VESHSqjYf3kpDmqpc1Fx9e5Ccnyc1fbCVrBGSp5yRw1+6oRwzoFlDGc27u8Af3vDl1RJD/4aldaeMZ8f2Btojg==",
"requires": {
- "@paciolan/remote-module-loader": "^2.4.0"
- },
- "dependencies": {
- "@paciolan/remote-module-loader": {
- "version": "2.6.2"
- }
+ "@paciolan/remote-module-loader": "^3.0.2"
}
},
"@paciolan/remote-module-loader": {
diff --git a/package.json b/package.json
index 8e558578ea..87fa6bdd4f 100644
--- a/package.json
+++ b/package.json
@@ -168,6 +168,7 @@
"@deephaven/jsapi-utils": "file:packages/jsapi-utils",
"@deephaven/log": "file:packages/log",
"@deephaven/mocks": "file:packages/mocks",
+ "@deephaven/app-utils": "file:packages/app-utils",
"@deephaven/prettier-config": "file:packages/prettier-config",
"@deephaven/react-hooks": "file:packages/react-hooks",
"@deephaven/redux": "file:packages/redux",
diff --git a/packages/app-utils/.gitignore b/packages/app-utils/.gitignore
new file mode 100644
index 0000000000..742a2cf9d2
--- /dev/null
+++ b/packages/app-utils/.gitignore
@@ -0,0 +1,31 @@
+# See https://help.github.com/ignore-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+
+# testing
+/coverage
+
+# production
+/build
+/dist
+
+# misc
+.vscode
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+.project
+.settings/
+.eslintcache
+.stylelintcache
+
+/public/vs
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+src/**/*.css
diff --git a/packages/app-utils/README.md b/packages/app-utils/README.md
new file mode 100644
index 0000000000..0bfe2fe0b0
--- /dev/null
+++ b/packages/app-utils/README.md
@@ -0,0 +1,9 @@
+# @deephaven/app-utils
+
+A library with some utility functions used by Deephaven applications.
+
+## Install
+
+```bash
+npm install --save @deephaven/app-utils
+```
diff --git a/packages/app-utils/jest.config.cjs b/packages/app-utils/jest.config.cjs
new file mode 100644
index 0000000000..365815a412
--- /dev/null
+++ b/packages/app-utils/jest.config.cjs
@@ -0,0 +1,7 @@
+const baseConfig = require('../../jest.config.base.cjs');
+const packageJson = require('./package');
+
+module.exports = {
+ ...baseConfig,
+ displayName: packageJson.name,
+};
diff --git a/packages/app-utils/package.json b/packages/app-utils/package.json
new file mode 100644
index 0000000000..a3dca821af
--- /dev/null
+++ b/packages/app-utils/package.json
@@ -0,0 +1,57 @@
+{
+ "name": "@deephaven/app-utils",
+ "version": "0.35.0",
+ "description": "Deephaven App Utils",
+ "author": "Deephaven Data Labs LLC",
+ "license": "Apache-2.0",
+ "type": "module",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/deephaven/web-client-ui.git",
+ "directory": "packages/app-utils"
+ },
+ "source": "src/index.js",
+ "main": "dist/index.js",
+ "module": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "engines": {
+ "node": ">=16"
+ },
+ "scripts": {
+ "build": "cross-env NODE_ENV=production run-p build:*",
+ "build:babel": "babel ./src --out-dir ./dist --extensions \".ts,.tsx,.js,.jsx\" --source-maps --root-mode upward"
+ },
+ "devDependencies": {
+ "@deephaven/redux": "file:../redux",
+ "react": "^17.x",
+ "react-dom": "^17.x",
+ "react-redux": "^7.x",
+ "redux": "^4.x"
+ },
+ "dependencies": {
+ "@deephaven/auth-plugins": "file:../auth-plugins",
+ "@deephaven/components": "file:../components",
+ "@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
+ "@deephaven/jsapi-types": "file:../jsapi-types",
+ "@deephaven/log": "file:../log",
+ "@paciolan/remote-component": "2.13.0",
+ "@paciolan/remote-module-loader": "^3.0.2",
+ "fira": "mozilla/fira#4.202"
+ },
+ "peerDependencies": {
+ "@deephaven/redux": "file:../redux",
+ "react": "^17.x",
+ "react-dom": "^17.x",
+ "react-redux": "^7.x",
+ "redux": "^4.x"
+ },
+ "files": [
+ "dist"
+ ],
+ "sideEffects": [
+ "*.css"
+ ],
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/app-utils/src/components/AppBootstrap.test.tsx b/packages/app-utils/src/components/AppBootstrap.test.tsx
new file mode 100644
index 0000000000..394bd927d0
--- /dev/null
+++ b/packages/app-utils/src/components/AppBootstrap.test.tsx
@@ -0,0 +1,151 @@
+import React from 'react';
+import { AUTH_HANDLER_TYPE_ANONYMOUS } from '@deephaven/auth-plugins';
+import { ApiContext } from '@deephaven/jsapi-bootstrap';
+import {
+ CoreClient,
+ IdeConnection,
+ dh as DhType,
+} from '@deephaven/jsapi-types';
+import { TestUtils } from '@deephaven/utils';
+import { act, render, screen } from '@testing-library/react';
+import AppBootstrap from './AppBootstrap';
+
+const API_URL = 'http://mockserver.net:8111';
+const PLUGINS_URL = 'http://mockserver.net:8111/plugins';
+
+const mockPluginsPromise = Promise.resolve([]);
+jest.mock('../plugins', () => ({
+ ...jest.requireActual('../plugins'),
+ loadModulePlugins: jest.fn(() => mockPluginsPromise),
+}));
+
+const mockChildText = 'Mock Child';
+const mockChild =
{mockChildText}
;
+
+function expectMockChild() {
+ return expect(screen.queryByText(mockChildText));
+}
+
+it('should throw if api has not been bootstrapped', () => {
+ expect(() =>
+ render(
+
+ {mockChild}
+
+ )
+ ).toThrow();
+ expectMockChild().toBeNull();
+});
+
+it('should display an error if no login plugin matches the provided auth handlers', async () => {
+ const authConfigValues: [string, string][] = [
+ ['AuthHandlers', `MockAuthHandler`],
+ ];
+ const mockGetAuthConfigValues = jest.fn(() =>
+ Promise.resolve(authConfigValues)
+ );
+ const mockLogin = jest.fn(() => Promise.resolve());
+ const client = TestUtils.createMockProxy({
+ getAuthConfigValues: mockGetAuthConfigValues,
+ login: mockLogin,
+ });
+ const api = TestUtils.createMockProxy({
+ CoreClient: (jest
+ .fn()
+ .mockImplementation(() => client) as unknown) as CoreClient,
+ });
+
+ render(
+
+
+ {mockChild}
+
+
+ );
+ expectMockChild().toBeNull();
+ expect(mockGetAuthConfigValues).toHaveBeenCalled();
+
+ await act(async () => {
+ await mockPluginsPromise;
+ });
+
+ expectMockChild().toBeNull();
+ expect(mockLogin).not.toHaveBeenCalled();
+ expect(
+ screen.queryByText(
+ 'Error: No login plugins found, please register a login plugin for auth handlers: MockAuthHandler'
+ )
+ ).not.toBeNull();
+});
+
+it('should log in automatically when the anonymous handler is supported', async () => {
+ const authConfigValues: [string, string][] = [
+ ['AuthHandlers', `${AUTH_HANDLER_TYPE_ANONYMOUS}`],
+ ];
+ const mockGetAuthConfigValues = jest.fn(() =>
+ Promise.resolve(authConfigValues)
+ );
+ let mockLoginResolve;
+ const mockLogin = jest.fn(
+ () =>
+ new Promise(resolve => {
+ mockLoginResolve = resolve;
+ })
+ );
+ let mockConnectionResolve;
+ const mockGetAsConnection = jest.fn(
+ () =>
+ new Promise(resolve => {
+ mockConnectionResolve = resolve;
+ })
+ );
+ const mockConnection = TestUtils.createMockProxy({});
+ const client = TestUtils.createMockProxy({
+ getAuthConfigValues: mockGetAuthConfigValues,
+ login: mockLogin,
+ getAsIdeConnection: mockGetAsConnection,
+ });
+ const api = TestUtils.createMockProxy({
+ CoreClient: (jest
+ .fn()
+ .mockImplementation(() => client) as unknown) as CoreClient,
+ });
+
+ render(
+
+
+ {mockChild}
+
+
+ );
+
+ expectMockChild().toBeNull();
+ expect(mockLogin).not.toHaveBeenCalled();
+
+ // Wait for plugins to load
+ await act(async () => {
+ await mockPluginsPromise;
+ });
+
+ expectMockChild().toBeNull();
+ expect(mockLogin).toHaveBeenCalled();
+ expect(screen.queryByTestId('auth-anonymous-loading')).not.toBeNull();
+
+ // Wait for login to complete
+ await act(async () => {
+ mockLoginResolve();
+ });
+
+ expect(screen.queryByTestId('auth-anonymous-loading')).toBeNull();
+ expect(screen.queryByTestId('connection-bootstrap-loading')).not.toBeNull();
+ expect(screen.queryByText(mockChildText)).toBeNull();
+
+ // Wait for IdeConnection to resolve
+ await act(async () => {
+ mockConnectionResolve(mockConnection);
+ });
+
+ expect(screen.queryByTestId('auth-anonymous-loading')).toBeNull();
+ expect(screen.queryByTestId('connection-bootstrap-loading')).toBeNull();
+ expectMockChild().not.toBeNull();
+});
diff --git a/packages/app-utils/src/components/AppBootstrap.tsx b/packages/app-utils/src/components/AppBootstrap.tsx
new file mode 100644
index 0000000000..8317303f80
--- /dev/null
+++ b/packages/app-utils/src/components/AppBootstrap.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import '@deephaven/components/scss/BaseStyleSheet.scss';
+import FontBootstrap from './FontBootstrap';
+import ClientBootstrap from './ClientBootstrap';
+import PluginsBootstrap from './PluginsBootstrap';
+import AuthBootstrap from './AuthBootstrap';
+import ConnectionBootstrap from './ConnectionBootstrap';
+import { getBaseUrl, getConnectOptions } from '../utils';
+import FontsLoaded from './FontsLoaded';
+
+export type AppBootstrapProps = {
+ /** URL of the API to load. */
+ apiUrl: string;
+
+ /** URL of the plugins to load. */
+ pluginsUrl: string;
+
+ /** Font class names to load. */
+ fontClassNames?: string[];
+
+ /**
+ * The children to render wrapped when everything is loaded and authenticated.
+ */
+ children: React.ReactNode;
+};
+
+/**
+ * AppBootstrap component. Handles loading the fonts, client, and authentication.
+ * Will display the children when everything is loaded and authenticated.
+ */
+export function AppBootstrap({
+ apiUrl,
+ fontClassNames,
+ pluginsUrl,
+ children,
+}: AppBootstrapProps) {
+ const serverUrl = getBaseUrl(apiUrl).origin;
+ const clientOptions = getConnectOptions();
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
+
+export default AppBootstrap;
diff --git a/packages/app-utils/src/components/AuthBootstrap.tsx b/packages/app-utils/src/components/AuthBootstrap.tsx
new file mode 100644
index 0000000000..54104e1945
--- /dev/null
+++ b/packages/app-utils/src/components/AuthBootstrap.tsx
@@ -0,0 +1,114 @@
+import React, {
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+import {
+ AuthConfigMap,
+ AuthPluginAnonymous,
+ AuthPluginParent,
+ AuthPluginPsk,
+} from '@deephaven/auth-plugins';
+import { LoadingOverlay } from '@deephaven/components';
+import { PluginsContext } from './PluginsBootstrap';
+import { getAuthPluginComponent } from '../plugins';
+import useClient from './useClient';
+
+export type AuthBootstrapProps = {
+ /**
+ * The children to render after authentication is completed.
+ */
+ children: React.ReactNode;
+};
+
+/** Core auth plugins that are always loaded */
+const CORE_AUTH_PLUGINS = new Map([
+ ['@deephaven/auth-plugins.AuthPluginPsk', AuthPluginPsk],
+ ['@deephaven/auth-plugins.AuthPluginParent', AuthPluginParent],
+ ['@deephaven/auth-plugins.AuthPluginAnonymous', AuthPluginAnonymous],
+]);
+
+/**
+ * AuthBootstrap component. Handles displaying the auth plugin and authenticating.
+ */
+export function AuthBootstrap({ children }: AuthBootstrapProps) {
+ const client = useClient();
+ // `useContext` instead of `usePlugins` so that we don't have to wait for the plugins to load
+ // We want to load the auth config values in parallel with the plugins
+ const plugins = useContext(PluginsContext);
+ const [authConfig, setAuthConfig] = useState();
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
+ const [error, setError] = useState();
+
+ useEffect(
+ function initAuthConfigValues() {
+ let isCancelled = false;
+ async function loadAuthConfigValues() {
+ try {
+ const newAuthConfigValues = await client.getAuthConfigValues();
+ if (!isCancelled) {
+ setAuthConfig(new Map(newAuthConfigValues));
+ }
+ } catch (e) {
+ if (!isCancelled) {
+ setError(e);
+ }
+ }
+ }
+ loadAuthConfigValues();
+ return () => {
+ isCancelled = true;
+ };
+ },
+ [client]
+ );
+
+ const AuthComponent = useMemo(() => {
+ if (plugins == null || authConfig == null) {
+ return undefined;
+ }
+
+ try {
+ return getAuthPluginComponent(plugins, authConfig, CORE_AUTH_PLUGINS);
+ } catch (e) {
+ setError(e);
+ }
+ }, [authConfig, plugins]);
+
+ const handleLoginSuccess = useCallback(() => {
+ setIsLoggedIn(true);
+ }, []);
+
+ const handleLoginFailure = useCallback((e: unknown) => {
+ setIsLoggedIn(false);
+ setError(e);
+ }, []);
+
+ const isLoading = AuthComponent == null || authConfig == null;
+
+ if (isLoading || error != null) {
+ return (
+
+ );
+ }
+ if (!isLoggedIn) {
+ return (
+
+ );
+ }
+
+ // eslint-disable-next-line react/jsx-no-useless-fragment
+ return <>{children}>;
+}
+
+export default AuthBootstrap;
diff --git a/packages/app-utils/src/components/ClientBootstrap.tsx b/packages/app-utils/src/components/ClientBootstrap.tsx
new file mode 100644
index 0000000000..e64a83dbd3
--- /dev/null
+++ b/packages/app-utils/src/components/ClientBootstrap.tsx
@@ -0,0 +1,50 @@
+import React, { createContext, useEffect, useState } from 'react';
+import { useApi } from '@deephaven/jsapi-bootstrap';
+import { ConnectOptions, CoreClient } from '@deephaven/jsapi-types';
+
+export const ClientContext = createContext(null);
+
+export type ClientBootstrapProps = {
+ /** URL of the server to connect to */
+ serverUrl: string;
+
+ /** Connection options to pass to CoreClient when connecting */
+ options?: ConnectOptions;
+
+ /**
+ * The children to render wrapped with the ClientContext.
+ * Will not render children until the client is created.
+ */
+ children: React.ReactNode;
+};
+
+/**
+ * ClientBootstrap component. Handles creating the client.
+ */
+export function ClientBootstrap({
+ serverUrl,
+ options,
+ children,
+}: ClientBootstrapProps) {
+ const api = useApi();
+ const [client, setClient] = useState();
+ useEffect(
+ function initClient() {
+ const newClient = new api.CoreClient(serverUrl, options);
+ setClient(newClient);
+ return () => {
+ newClient.disconnect();
+ };
+ },
+ [api, options, serverUrl]
+ );
+
+ if (client == null) {
+ return null;
+ }
+ return (
+ {children}
+ );
+}
+
+export default ClientBootstrap;
diff --git a/packages/app-utils/src/components/ConnectionBootstrap.tsx b/packages/app-utils/src/components/ConnectionBootstrap.tsx
new file mode 100644
index 0000000000..32ea58a3a1
--- /dev/null
+++ b/packages/app-utils/src/components/ConnectionBootstrap.tsx
@@ -0,0 +1,89 @@
+import React, { createContext, useEffect, useState } from 'react';
+import { LoadingOverlay } from '@deephaven/components';
+import { useApi } from '@deephaven/jsapi-bootstrap';
+import { IdeConnection } from '@deephaven/jsapi-types';
+import Log from '@deephaven/log';
+import useClient from './useClient';
+
+const log = Log.module('@deephaven/jsapi-components.ConnectionBootstrap');
+
+export const ConnectionContext = createContext(null);
+
+export type ConnectionBootstrapProps = {
+ /**
+ * The children to render wrapped with the ConnectionContext.
+ * Will not render children until the connection is created.
+ */
+ children: React.ReactNode;
+};
+
+/**
+ * ConnectionBootstrap component. Handles initializing the connection.
+ */
+export function ConnectionBootstrap({ children }: ConnectionBootstrapProps) {
+ const api = useApi();
+ const client = useClient();
+ const [error, setError] = useState();
+ const [connection, setConnection] = useState();
+ useEffect(
+ function initConnection() {
+ let isCancelled = false;
+ async function loadConnection() {
+ try {
+ const newConnection = await client.getAsIdeConnection();
+ if (isCancelled) {
+ return;
+ }
+ setConnection(newConnection);
+ } catch (e) {
+ if (isCancelled) {
+ return;
+ }
+ setError(e);
+ }
+ }
+ loadConnection();
+ return () => {
+ isCancelled = true;
+ };
+ },
+ [api, client]
+ );
+
+ useEffect(
+ function listenForShutdown() {
+ if (connection == null) return;
+
+ function handleShutdown(event: CustomEvent) {
+ const { detail } = event;
+ log.info('Shutdown', `${JSON.stringify(detail)}`);
+ setError(`Server shutdown: ${detail ?? 'Unknown reason'}`);
+ }
+
+ const removerFn = connection.addEventListener(
+ api.IdeConnection.EVENT_SHUTDOWN,
+ handleShutdown
+ );
+ return removerFn;
+ },
+ [api, connection]
+ );
+
+ if (connection == null || error != null) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+export default ConnectionBootstrap;
diff --git a/packages/app-utils/src/components/FontBootstrap.tsx b/packages/app-utils/src/components/FontBootstrap.tsx
new file mode 100644
index 0000000000..fa0f3d1616
--- /dev/null
+++ b/packages/app-utils/src/components/FontBootstrap.tsx
@@ -0,0 +1,62 @@
+import React, { createContext, useEffect, useState } from 'react';
+import 'fira';
+
+export const FontsLoadedContext = createContext(false);
+
+export type FontBootstrapProps = {
+ /**
+ * Class names of the font elements to pre load
+ */
+ fontClassNames?: string[];
+
+ /**
+ * The children to render wrapped with the FontsLoadedContext.
+ * Note that it renders the children even if the fonts aren't loaded yet.
+ */
+ children: React.ReactNode;
+};
+
+/**
+ * FontBootstrap component. Handles preloading fonts.
+ */
+export function FontBootstrap({
+ fontClassNames = ['fira-sans-regular', 'fira-sans-bold', 'fira-mono'],
+ children,
+}: FontBootstrapProps) {
+ const [isLoaded, setIsLoaded] = useState(false);
+ useEffect(function initFonts() {
+ document.fonts.ready.then(() => {
+ setIsLoaded(true);
+ });
+ }, []);
+
+ return (
+ <>
+
+ {children}
+
+ {/*
+ Need to preload any monaco and Deephaven grid fonts.
+ We hide text with all the fonts we need on the root app.jsx page
+ Load the Fira Mono font so that Monaco calculates word wrapping properly.
+ This element doesn't need to be visible, just load the font and stay hidden.
+ https://github.com/microsoft/vscode/issues/88689
+ Can be replaced with a rel="preload" when firefox adds support
+ https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content
+ */}
+
+ {/* trigger loading of fonts needed by monaco and iris grid */}
+ {fontClassNames.map(className => (
+
+ preload
+
+ ))}
+
+ >
+ );
+}
+
+export default FontBootstrap;
diff --git a/packages/app-utils/src/components/FontsLoaded.tsx b/packages/app-utils/src/components/FontsLoaded.tsx
new file mode 100644
index 0000000000..9fd3fef3fb
--- /dev/null
+++ b/packages/app-utils/src/components/FontsLoaded.tsx
@@ -0,0 +1,21 @@
+import React, { useContext } from 'react';
+import { LoadingOverlay } from '@deephaven/components';
+import { FontsLoadedContext } from './FontBootstrap';
+
+export type FontsLoadedProps = {
+ /** Children to show when the fonts have completed loading */
+ children: React.ReactNode;
+};
+
+export function FontsLoaded({ children }: FontsLoadedProps) {
+ const isFontsLoaded = useContext(FontsLoadedContext);
+
+ if (!isFontsLoaded) {
+ return ;
+ }
+
+ // eslint-disable-next-line react/jsx-no-useless-fragment
+ return <>{children}>;
+}
+
+export default FontsLoaded;
diff --git a/packages/app-utils/src/components/PluginsBootstrap.tsx b/packages/app-utils/src/components/PluginsBootstrap.tsx
new file mode 100644
index 0000000000..7714a2c3a5
--- /dev/null
+++ b/packages/app-utils/src/components/PluginsBootstrap.tsx
@@ -0,0 +1,51 @@
+import React, { createContext, useEffect, useState } from 'react';
+import { PluginModuleMap, loadModulePlugins } from '../plugins';
+
+export const PluginsContext = createContext(null);
+
+export type PluginsBootstrapProps = {
+ /**
+ * Base URL of the plugins to load.
+ */
+ pluginsUrl: string;
+
+ /**
+ * The children to render wrapped with the PluginsContext.
+ * Note that it renders the children even if the plugins aren't loaded yet.
+ */
+ children: React.ReactNode;
+};
+
+/**
+ * PluginsBootstrap component. Handles loading the plugins.
+ */
+export function PluginsBootstrap({
+ pluginsUrl,
+ children,
+}: PluginsBootstrapProps) {
+ const [plugins, setPlugins] = useState(null);
+ useEffect(
+ function initPlugins() {
+ let isCancelled = false;
+ async function loadPlugins() {
+ const pluginModules = await loadModulePlugins(pluginsUrl);
+ if (!isCancelled) {
+ setPlugins(pluginModules);
+ }
+ }
+ loadPlugins();
+ return () => {
+ isCancelled = true;
+ };
+ },
+ [pluginsUrl]
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+export default PluginsBootstrap;
diff --git a/packages/app-utils/src/components/index.ts b/packages/app-utils/src/components/index.ts
new file mode 100644
index 0000000000..ab84abca89
--- /dev/null
+++ b/packages/app-utils/src/components/index.ts
@@ -0,0 +1,10 @@
+export * from './AppBootstrap';
+export * from './AuthBootstrap';
+export * from './ClientBootstrap';
+export * from './ConnectionBootstrap';
+export * from './FontBootstrap';
+export * from './FontsLoaded';
+export * from './PluginsBootstrap';
+export * from './usePlugins';
+export * from './useClient';
+export * from './useConnection';
diff --git a/packages/app-utils/src/components/useClient.ts b/packages/app-utils/src/components/useClient.ts
new file mode 100644
index 0000000000..47310b440a
--- /dev/null
+++ b/packages/app-utils/src/components/useClient.ts
@@ -0,0 +1,14 @@
+import { useContext } from 'react';
+import { ClientContext } from './ClientBootstrap';
+
+export function useClient() {
+ const client = useContext(ClientContext);
+ if (client == null) {
+ throw new Error(
+ 'No Client available in useClient. Was code wrapped in ClientBootstrap or ClientContext.Provider?'
+ );
+ }
+ return client;
+}
+
+export default useClient;
diff --git a/packages/app-utils/src/components/useConnection.ts b/packages/app-utils/src/components/useConnection.ts
new file mode 100644
index 0000000000..76f432d8bf
--- /dev/null
+++ b/packages/app-utils/src/components/useConnection.ts
@@ -0,0 +1,14 @@
+import { useContext } from 'react';
+import { ConnectionContext } from './ConnectionBootstrap';
+
+export function useConnection() {
+ const connection = useContext(ConnectionContext);
+ if (connection == null) {
+ throw new Error(
+ 'No IdeConnection available in useConnection. Was code wrapped in ConnectionBootstrap or ConnectionContext.Provider?'
+ );
+ }
+ return connection;
+}
+
+export default useConnection;
diff --git a/packages/app-utils/src/components/usePlugins.ts b/packages/app-utils/src/components/usePlugins.ts
new file mode 100644
index 0000000000..7f3e78389f
--- /dev/null
+++ b/packages/app-utils/src/components/usePlugins.ts
@@ -0,0 +1,14 @@
+import { useContext } from 'react';
+import { PluginsContext } from './PluginsBootstrap';
+
+export function usePlugins() {
+ const plugins = useContext(PluginsContext);
+ if (plugins == null) {
+ throw new Error(
+ 'No Plugins available in usePlugins. Was code wrapped in PluginsBootstrap or PluginsContext.Provider?'
+ );
+ }
+ return plugins;
+}
+
+export default usePlugins;
diff --git a/packages/app-utils/src/index.ts b/packages/app-utils/src/index.ts
new file mode 100644
index 0000000000..44a7957644
--- /dev/null
+++ b/packages/app-utils/src/index.ts
@@ -0,0 +1,3 @@
+export * from './components';
+export * from './plugins';
+export * from './utils';
diff --git a/packages/app-utils/src/plugins/PluginUtils.tsx b/packages/app-utils/src/plugins/PluginUtils.tsx
new file mode 100644
index 0000000000..7ebb289a72
--- /dev/null
+++ b/packages/app-utils/src/plugins/PluginUtils.tsx
@@ -0,0 +1,180 @@
+import React, { ForwardRefExoticComponent } from 'react';
+import {
+ AuthPlugin,
+ AuthPluginComponent,
+ isAuthPlugin,
+} from '@deephaven/auth-plugins';
+import Log from '@deephaven/log';
+import RemoteComponent from './RemoteComponent';
+import loadRemoteModule from './loadRemoteModule';
+
+const log = Log.module('@deephaven/app-utils.PluginUtils');
+
+// A PluginModule. This interface should have new fields added to it from different levels of plugins.
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface PluginModule {}
+
+export type PluginModuleMap = Map;
+
+/**
+ * Load a component plugin from the server.
+ * @param baseURL Base URL of the plugin server
+ * @param pluginName Name of the component plugin to load
+ * @returns A lazily loaded JSX.Element from the plugin
+ */
+export function loadComponentPlugin(
+ baseURL: URL,
+ pluginName: string
+): ForwardRefExoticComponent> {
+ const pluginUrl = new URL(`${pluginName}.js`, baseURL);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const Plugin: any = React.forwardRef((props, ref) => (
+ {
+ if (err != null && err !== '') {
+ const errorMessage = `Error loading plugin ${pluginName} from ${pluginUrl} due to ${err}`;
+ log.error(errorMessage);
+ return {`${errorMessage}`}
;
+ }
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return ;
+ }}
+ />
+ ));
+ Plugin.pluginName = pluginName;
+ Plugin.displayName = 'Plugin';
+ return Plugin;
+}
+
+/**
+ * Imports a commonjs plugin module from the provided URL
+ * @param pluginUrl The URL of the plugin to load
+ * @returns The loaded module
+ */
+export async function loadModulePlugin(pluginUrl: string): Promise {
+ const myModule = await loadRemoteModule(pluginUrl);
+ return myModule;
+}
+
+/**
+ * Loads a JSON file and returns the JSON object
+ * @param jsonUrl The URL of the JSON file to load
+ * @returns The JSON object of the manifest file
+ */
+export async function loadJson(
+ jsonUrl: string
+): Promise<{ plugins: { name: string; main: string }[] }> {
+ const res = await fetch(jsonUrl);
+ if (!res.ok) {
+ throw new Error(res.statusText);
+ }
+ try {
+ return await res.json();
+ } catch {
+ throw new Error('Could not be parsed as JSON');
+ }
+}
+
+/**
+ * Load all plugin modules available.
+ * @param modulePluginsUrl The base URL of the module plugins to load
+ * @returns A map from the name of the plugin to the plugin module that was loaded
+ */
+export async function loadModulePlugins(
+ modulePluginsUrl: string
+): Promise {
+ log.debug('Loading plugins...');
+ try {
+ const manifest = await loadJson(`${modulePluginsUrl}/manifest.json`);
+
+ if (!Array.isArray(manifest.plugins)) {
+ throw new Error('Plugin manifest JSON does not contain plugins array');
+ }
+
+ log.debug('Plugin manifest loaded:', manifest);
+ const pluginPromises: Promise[] = [];
+ for (let i = 0; i < manifest.plugins.length; i += 1) {
+ const { name, main } = manifest.plugins[i];
+ const pluginMainUrl = `${modulePluginsUrl}/${name}/${main}`;
+ pluginPromises.push(loadModulePlugin(pluginMainUrl));
+ }
+ const pluginModules = await Promise.allSettled(pluginPromises);
+
+ const pluginMap: PluginModuleMap = new Map();
+ for (let i = 0; i < pluginModules.length; i += 1) {
+ const module = pluginModules[i];
+ const { name } = manifest.plugins[i];
+ if (module.status === 'fulfilled') {
+ pluginMap.set(name, module.value as PluginModule);
+ } else {
+ log.error(`Unable to load plugin ${name}`, module.reason);
+ }
+ }
+ log.info('Plugins loaded:', pluginMap);
+
+ return pluginMap;
+ } catch (e) {
+ log.error('Unable to load plugins:', e);
+ return new Map();
+ }
+}
+
+export function getAuthHandlers(
+ authConfigValues: Map
+): string[] {
+ return authConfigValues.get('AuthHandlers')?.split(',') ?? [];
+}
+
+/**
+ * Get the auth plugin component from the plugin map and current configuration
+ * Throws if no auth plugin is available
+ *
+ * @param pluginMap Map of plugins loaded from the server
+ * @param authConfigValues Auth config values from the server
+ * @param corePlugins Map of core auth plugins to include in the list. They are added after the loaded plugins
+ * @returns The auth plugin component to render
+ */
+export function getAuthPluginComponent(
+ pluginMap: PluginModuleMap,
+ authConfigValues: Map,
+ corePlugins?: Map
+): AuthPluginComponent {
+ const authHandlers = getAuthHandlers(authConfigValues);
+ // Filter out all the plugins that are auth plugins, and then map them to [pluginName, AuthPlugin] pairs
+ // Uses some pretty disgusting casting, because TypeScript wants to treat it as an (string | AuthPlugin)[] array instead
+ const authPlugins = ([
+ ...pluginMap.entries(),
+ ].filter(([, plugin]: [string, { AuthPlugin?: AuthPlugin }]) =>
+ isAuthPlugin(plugin.AuthPlugin)
+ ) as [string, { AuthPlugin: AuthPlugin }][]).map(([name, plugin]) => [
+ name,
+ plugin.AuthPlugin,
+ ]) as [string, AuthPlugin][];
+
+ // Add all the core plugins in priority
+ authPlugins.push(...(corePlugins ?? []));
+
+ // Filter the available auth plugins
+
+ const availableAuthPlugins = authPlugins.filter(([name, authPlugin]) =>
+ authPlugin.isAvailable(authHandlers)
+ );
+
+ if (availableAuthPlugins.length === 0) {
+ throw new Error(
+ `No login plugins found, please register a login plugin for auth handlers: ${authHandlers}`
+ );
+ } else if (availableAuthPlugins.length > 1) {
+ log.warn(
+ 'More than one login plugin available, will use the first one: ',
+ availableAuthPlugins.map(([name]) => name).join(', ')
+ );
+ }
+
+ const [loginPluginName, NewLoginPlugin] = availableAuthPlugins[0];
+ log.info('Using LoginPlugin', loginPluginName);
+
+ return NewLoginPlugin.Component;
+}
diff --git a/packages/app-utils/src/plugins/RemoteComponent.ts b/packages/app-utils/src/plugins/RemoteComponent.ts
new file mode 100644
index 0000000000..91a6dd3be1
--- /dev/null
+++ b/packages/app-utils/src/plugins/RemoteComponent.ts
@@ -0,0 +1,10 @@
+import {
+ createRemoteComponent,
+ createRequires,
+} from '@paciolan/remote-component';
+import { resolve } from './remote-component.config';
+
+const requires = createRequires(() => resolve);
+
+export const RemoteComponent = createRemoteComponent({ requires });
+export default RemoteComponent;
diff --git a/packages/code-studio/src/plugins/index.ts b/packages/app-utils/src/plugins/index.ts
similarity index 53%
rename from packages/code-studio/src/plugins/index.ts
rename to packages/app-utils/src/plugins/index.ts
index 76d2fd18c7..4f006ac18e 100644
--- a/packages/code-studio/src/plugins/index.ts
+++ b/packages/app-utils/src/plugins/index.ts
@@ -1,2 +1,2 @@
-export { default as PluginUtils } from './PluginUtils';
+export * from './PluginUtils';
export { default as RemoteComponent } from './RemoteComponent';
diff --git a/packages/code-studio/src/plugins/loadRemoteModule.ts b/packages/app-utils/src/plugins/loadRemoteModule.ts
similarity index 86%
rename from packages/code-studio/src/plugins/loadRemoteModule.ts
rename to packages/app-utils/src/plugins/loadRemoteModule.ts
index 5e20e2833a..85744d307e 100644
--- a/packages/code-studio/src/plugins/loadRemoteModule.ts
+++ b/packages/app-utils/src/plugins/loadRemoteModule.ts
@@ -1,7 +1,7 @@
import createLoadRemoteModule, {
createRequires,
} from '@paciolan/remote-module-loader';
-import { resolve } from '../remote-component.config';
+import { resolve } from './remote-component.config';
const requires = createRequires(resolve);
diff --git a/packages/code-studio/src/remote-component.config.js b/packages/app-utils/src/plugins/remote-component.config.ts
similarity index 100%
rename from packages/code-studio/src/remote-component.config.js
rename to packages/app-utils/src/plugins/remote-component.config.ts
diff --git a/packages/app-utils/src/utils/ConnectUtils.ts b/packages/app-utils/src/utils/ConnectUtils.ts
new file mode 100644
index 0000000000..ed030af674
--- /dev/null
+++ b/packages/app-utils/src/utils/ConnectUtils.ts
@@ -0,0 +1,26 @@
+import { ConnectOptions } from '@deephaven/jsapi-types';
+
+/**
+ * Get the base URL of the API
+ * @param apiUrl API URL
+ * @returns URL for the base of the API
+ */
+export function getBaseUrl(apiUrl: string): URL {
+ return new URL(apiUrl, `${window.location}`);
+}
+
+/**
+ * Get the Envoy prefix header value
+ * @returns Envoy prefix header value
+ */
+export function getEnvoyPrefix(): string | null {
+ const searchParams = new URLSearchParams(window.location.search);
+ return searchParams.get('envoyPrefix');
+}
+
+export function getConnectOptions(): ConnectOptions {
+ const envoyPrefix = getEnvoyPrefix();
+ return envoyPrefix != null
+ ? { headers: { 'envoy-prefix': envoyPrefix } }
+ : { headers: {} };
+}
diff --git a/packages/app-utils/src/utils/index.ts b/packages/app-utils/src/utils/index.ts
new file mode 100644
index 0000000000..0cbc0d265c
--- /dev/null
+++ b/packages/app-utils/src/utils/index.ts
@@ -0,0 +1 @@
+export * from './ConnectUtils';
diff --git a/packages/app-utils/tsconfig.json b/packages/app-utils/tsconfig.json
new file mode 100644
index 0000000000..548436955e
--- /dev/null
+++ b/packages/app-utils/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "src/",
+ "outDir": "dist/"
+ },
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx"],
+ "exclude": ["node_modules", "src/**/*.test.*", "src/**/__mocks__/*"],
+ "references": [
+ { "path": "../auth-plugins" },
+ { "path": "../jsapi-bootstrap" },
+ { "path": "../jsapi-types" },
+ { "path": "../jsapi-utils" },
+ { "path": "../log" },
+ { "path": "../redux" }
+ ]
+}
diff --git a/packages/auth-plugins/.gitignore b/packages/auth-plugins/.gitignore
new file mode 100644
index 0000000000..742a2cf9d2
--- /dev/null
+++ b/packages/auth-plugins/.gitignore
@@ -0,0 +1,31 @@
+# See https://help.github.com/ignore-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+
+# testing
+/coverage
+
+# production
+/build
+/dist
+
+# misc
+.vscode
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+.project
+.settings/
+.eslintcache
+.stylelintcache
+
+/public/vs
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+src/**/*.css
diff --git a/packages/auth-plugins/README.md b/packages/auth-plugins/README.md
new file mode 100644
index 0000000000..7bd3df47db
--- /dev/null
+++ b/packages/auth-plugins/README.md
@@ -0,0 +1,13 @@
+# @deephaven/auth-plugins
+
+Authentication plugins for Deephaven. Used by [AuthBootstrap](../app-utils/src/components/AuthBootstrap.tsx) to provide default authentication if no custom plugins are loaded. For mode details on custom plugins, see [deephaven-js-plugins repository](https://github.com/deephaven/deephaven-js-plugins).
+
+## Install
+
+```bash
+npm install --save @deephaven/auth-plugins
+```
+
+# Legal Notices
+
+Deephaven Data Labs and any contributors grant you a license to the content of this repository under the Apache 2.0 License, see the [LICENSE](../../LICENSE) file.
diff --git a/packages/auth-plugins/jest.config.cjs b/packages/auth-plugins/jest.config.cjs
new file mode 100644
index 0000000000..6ee7d9fc30
--- /dev/null
+++ b/packages/auth-plugins/jest.config.cjs
@@ -0,0 +1,8 @@
+const baseConfig = require('../../jest.config.base.cjs');
+const packageJson = require('./package');
+
+module.exports = {
+ ...baseConfig,
+ displayName: packageJson.name,
+ resetMocks: false,
+};
diff --git a/packages/auth-plugins/package.json b/packages/auth-plugins/package.json
new file mode 100644
index 0000000000..f984f667c7
--- /dev/null
+++ b/packages/auth-plugins/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "@deephaven/auth-plugins",
+ "version": "0.32.0",
+ "description": "Deephaven Auth Plugins",
+ "keywords": [
+ "Deephaven",
+ "plugin",
+ "deephaven-js-plugin",
+ "auth",
+ "authentication",
+ "anonymous",
+ "parent",
+ "psk",
+ "Pre-shared key"
+ ],
+ "author": "Deephaven Data Labs LLC",
+ "license": "Apache-2.0",
+ "type": "module",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/deephaven/web-client-ui.git",
+ "directory": "packages/auth-plugins"
+ },
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "source": "src/index.ts",
+ "engines": {
+ "node": ">=16"
+ },
+ "scripts": {
+ "build": "cross-env NODE_ENV=production run-p build:*",
+ "build:babel": "babel ./src --out-dir ./dist --extensions \".ts,.tsx,.js,.jsx\" --source-maps --root-mode upward"
+ },
+ "dependencies": {
+ "@deephaven/components": "file:../components",
+ "@deephaven/log": "file:../log",
+ "@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
+ "@deephaven/jsapi-types": "file:../jsapi-types",
+ "@deephaven/jsapi-utils": "file:../jsapi-utils"
+ },
+ "devDependencies": {
+ "@deephaven/tsconfig": "file:../tsconfig",
+ "@types/react": "^17.0.2"
+ },
+ "peerDependencies": {
+ "react": "^17.x"
+ },
+ "files": [
+ "dist"
+ ],
+ "sideEffects": false,
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/auth-plugins/src/AuthHandlerTypes.ts b/packages/auth-plugins/src/AuthHandlerTypes.ts
new file mode 100644
index 0000000000..d02bbef89f
--- /dev/null
+++ b/packages/auth-plugins/src/AuthHandlerTypes.ts
@@ -0,0 +1,5 @@
+export const AUTH_HANDLER_TYPE_ANONYMOUS =
+ 'io.deephaven.auth.AnonymousAuthenticationHandler';
+
+export const AUTH_HANDLER_TYPE_PSK =
+ 'io.deephaven.authentication.psk.PskAuthenticationHandler';
diff --git a/packages/auth-plugins/src/AuthPlugin.ts b/packages/auth-plugins/src/AuthPlugin.ts
new file mode 100644
index 0000000000..87d4bcf58c
--- /dev/null
+++ b/packages/auth-plugins/src/AuthPlugin.ts
@@ -0,0 +1,46 @@
+import React from 'react';
+import { CoreClient } from '@deephaven/jsapi-types';
+
+/**
+ * Map from auth config keys to their values
+ * E.g. Map { AuthHandlers → "io.deephaven.auth.AnonymousAuthenticationHandler" }
+ */
+export type AuthConfigMap = Map;
+
+/**
+ * Props for the auth plugin component to render
+ */
+export type AuthPluginProps = {
+ /** Map from config keys to their values */
+ authConfigValues: AuthConfigMap;
+
+ /** Client to check auth configuration on */
+ client: CoreClient;
+
+ /** Called when authentication is sucessful */
+ onSuccess(): void;
+
+ /** Called when authentication fails */
+ onFailure(error: unknown): void;
+};
+
+export type AuthPluginComponent = React.FunctionComponent;
+
+/**
+ * Whether the auth plugin is available given the current configuration
+ */
+export type AuthPluginIsAvailableFunction = (authHandlers: string[]) => boolean;
+
+export type AuthPlugin = {
+ Component: AuthPluginComponent;
+ isAvailable: AuthPluginIsAvailableFunction;
+};
+
+export function isAuthPlugin(plugin?: unknown): plugin is AuthPlugin {
+ if (plugin == null) return false;
+ const authPlugin = plugin as AuthPlugin;
+ return (
+ authPlugin.Component !== undefined &&
+ typeof authPlugin.isAvailable === 'function'
+ );
+}
diff --git a/packages/auth-plugins/src/AuthPluginAnonymous.test.tsx b/packages/auth-plugins/src/AuthPluginAnonymous.test.tsx
new file mode 100644
index 0000000000..7dfab722b0
--- /dev/null
+++ b/packages/auth-plugins/src/AuthPluginAnonymous.test.tsx
@@ -0,0 +1,105 @@
+import React from 'react';
+import { act, render, screen } from '@testing-library/react';
+import { ApiContext } from '@deephaven/jsapi-bootstrap';
+import { dh } from '@deephaven/jsapi-shim';
+import AuthPluginAnonymous from './AuthPluginAnonymous';
+import { AUTH_HANDLER_TYPE_ANONYMOUS as AUTH_TYPE } from './AuthHandlerTypes';
+
+function makeCoreClient() {
+ return new dh.CoreClient('wss://test.mockurl.example.com');
+}
+
+describe('availability tests', () => {
+ it.each([
+ [[AUTH_TYPE], true],
+ [['another.type', AUTH_TYPE], true],
+ [[], false],
+ [
+ ['not-anonymous', `${AUTH_TYPE}.withsuffix`, `prefix.${AUTH_TYPE}`],
+ false,
+ ],
+ ])(
+ 'returns availability based on auth handlers: %s',
+ (authHandlers, result) => {
+ expect(AuthPluginAnonymous.isAvailable(authHandlers)).toBe(result);
+ }
+ );
+});
+
+function expectLoading() {
+ expect(screen.queryByTestId('auth-anonymous-loading')).not.toBeNull();
+}
+
+describe('component tests', () => {
+ const authConfigValues = new Map();
+ it('attempts to login on mount, calls success', async () => {
+ const loginPromise = Promise.resolve();
+ const onSuccess = jest.fn();
+ const onFailure = jest.fn();
+ const mockLogin = jest.fn(() => loginPromise);
+ const client = makeCoreClient();
+ client.login = mockLogin;
+ render(
+
+
+
+ );
+ expectLoading();
+ expect(onSuccess).not.toHaveBeenCalled();
+ expect(onFailure).not.toHaveBeenCalled();
+ expect(mockLogin).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: dh.CoreClient.LOGIN_TYPE_ANONYMOUS,
+ })
+ );
+
+ await loginPromise;
+
+ expect(onSuccess).toHaveBeenCalled();
+ expect(onFailure).not.toHaveBeenCalled();
+ });
+
+ it('attempts to login on mount, calls failure if login fails', async () => {
+ const error = 'Mock test error';
+ const loginPromise = Promise.reject(error);
+ const onSuccess = jest.fn();
+ const onFailure = jest.fn();
+ const mockLogin = jest.fn(() => loginPromise);
+ const client = makeCoreClient();
+ client.login = mockLogin;
+ render(
+
+
+
+ );
+ expectLoading();
+ expect(onSuccess).not.toHaveBeenCalled();
+ expect(onFailure).not.toHaveBeenCalled();
+ expect(mockLogin).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: dh.CoreClient.LOGIN_TYPE_ANONYMOUS,
+ })
+ );
+
+ await act(async () => {
+ try {
+ await loginPromise;
+ } catch (e) {
+ // We know it fails
+ }
+ });
+
+ expect(onSuccess).not.toHaveBeenCalled();
+ expect(onFailure).toHaveBeenCalledWith(error);
+ });
+});
diff --git a/packages/auth-plugins/src/AuthPluginAnonymous.tsx b/packages/auth-plugins/src/AuthPluginAnonymous.tsx
new file mode 100644
index 0000000000..a55d732080
--- /dev/null
+++ b/packages/auth-plugins/src/AuthPluginAnonymous.tsx
@@ -0,0 +1,64 @@
+import React, { useEffect, useState } from 'react';
+import { useApi } from '@deephaven/jsapi-bootstrap';
+import Log from '@deephaven/log';
+import { LoadingOverlay } from '@deephaven/components';
+import { AUTH_HANDLER_TYPE_ANONYMOUS } from './AuthHandlerTypes';
+import { AuthPlugin, AuthPluginProps } from './AuthPlugin';
+
+const log = Log.module('AuthPluginAnonymous');
+
+/**
+ * AuthPlugin that tries to login anonymously. Fails if anonymous login fails
+ */
+function Component({
+ client,
+ onSuccess,
+ onFailure,
+}: AuthPluginProps): JSX.Element {
+ const [error, setError] = useState();
+ const dh = useApi();
+
+ useEffect(() => {
+ let isCanceled = false;
+ async function login() {
+ try {
+ log.info('Logging in...');
+ await client.login({ type: dh.CoreClient.LOGIN_TYPE_ANONYMOUS });
+ if (isCanceled) {
+ log.info('Previous login result canceled');
+ return;
+ }
+ log.info('Logged in successfully.');
+ onSuccess();
+ } catch (e) {
+ if (isCanceled) {
+ log.info('Previous login failure canceled');
+ return;
+ }
+ log.error('Unable to login:', e);
+ setError(e);
+ onFailure(e);
+ }
+ }
+ login();
+ return () => {
+ isCanceled = true;
+ };
+ }, [client, dh, onFailure, onSuccess]);
+ return (
+
+ );
+}
+
+const AuthPluginAnonymous: AuthPlugin = {
+ Component,
+ isAvailable: authHandlers =>
+ authHandlers.includes(AUTH_HANDLER_TYPE_ANONYMOUS),
+};
+
+export default AuthPluginAnonymous;
diff --git a/packages/auth-plugins/src/AuthPluginParent.test.tsx b/packages/auth-plugins/src/AuthPluginParent.test.tsx
new file mode 100644
index 0000000000..39e13e5f87
--- /dev/null
+++ b/packages/auth-plugins/src/AuthPluginParent.test.tsx
@@ -0,0 +1,120 @@
+import React from 'react';
+import { act, render, screen } from '@testing-library/react';
+import { dh } from '@deephaven/jsapi-shim';
+import { LoginOptions } from '@deephaven/jsapi-types';
+import AuthPluginParent from './AuthPluginParent';
+
+let mockParentResponse: Promise;
+jest.mock('@deephaven/jsapi-utils', () => ({
+ LOGIN_OPTIONS_REQUEST: 'mock-login-options-request',
+ requestParentResponse: jest.fn(() => mockParentResponse),
+}));
+
+function makeCoreClient() {
+ return new dh.CoreClient('wss://test.mockurl.example.com');
+}
+
+describe('availability tests', () => {
+ const authHandlers = [];
+ it('is available when window opener is set', () => {
+ window.opener = { postMessage: jest.fn() };
+ window.history.pushState(
+ {},
+ 'Test Title',
+ `/test.html?authProvider=parent`
+ );
+ expect(AuthPluginParent.isAvailable(authHandlers)).toBe(true);
+ });
+ it('is not available when window opener not set', () => {
+ delete window.opener;
+ expect(AuthPluginParent.isAvailable(authHandlers)).toBe(false);
+ });
+});
+
+function expectLoading() {
+ expect(screen.queryByTestId('auth-parent-loading')).not.toBeNull();
+}
+
+describe('component tests', () => {
+ const authConfigValues = new Map();
+
+ it('logs in when parent window provides login credentials', async () => {
+ let mockResolve;
+ mockParentResponse = new Promise(resolve => {
+ mockResolve = resolve;
+ });
+ const loginOptions = { token: 'mockParentToken' };
+ const loginPromise = Promise.resolve();
+ const onSuccess = jest.fn();
+ const onFailure = jest.fn();
+ const mockLogin = jest.fn(() => loginPromise);
+ const client = makeCoreClient();
+ client.login = mockLogin;
+ render(
+
+ );
+ expectLoading();
+ expect(onSuccess).not.toHaveBeenCalled();
+ expect(onFailure).not.toHaveBeenCalled();
+ expect(mockLogin).not.toHaveBeenCalled();
+
+ mockResolve(loginOptions);
+
+ await mockParentResponse;
+ expect(onSuccess).not.toHaveBeenCalled();
+ expect(onFailure).not.toHaveBeenCalled();
+ expect(mockLogin).toHaveBeenCalledWith(loginOptions);
+
+ await loginPromise;
+ expect(onSuccess).toHaveBeenCalled();
+ expect(onFailure).not.toHaveBeenCalled();
+ });
+
+ it('reports failure if login credentials are invalid', async () => {
+ let mockResolve;
+ mockParentResponse = new Promise(resolve => {
+ mockResolve = resolve;
+ });
+
+ const error = 'mock test Invalid login credentials';
+ const loginOptions = { token: 'mockParentToken' };
+ const loginPromise = Promise.reject(error);
+ const onSuccess = jest.fn();
+ const onFailure = jest.fn();
+ const mockLogin = jest.fn(() => loginPromise);
+ const client = makeCoreClient();
+ client.login = mockLogin;
+ render(
+
+ );
+ expectLoading();
+ expect(onSuccess).not.toHaveBeenCalled();
+ expect(onFailure).not.toHaveBeenCalled();
+ expect(mockLogin).not.toHaveBeenCalled();
+
+ // Send a message with the login details
+ mockResolve(loginOptions);
+ await mockParentResponse;
+ expect(mockLogin).toHaveBeenCalledWith(loginOptions);
+
+ await act(async () => {
+ try {
+ await loginPromise;
+ } catch (e) {
+ // expecting promise to fail
+ }
+ });
+ expect(onSuccess).not.toHaveBeenCalled();
+ expect(onFailure).toHaveBeenCalledWith(error);
+ });
+});
diff --git a/packages/auth-plugins/src/AuthPluginParent.tsx b/packages/auth-plugins/src/AuthPluginParent.tsx
new file mode 100644
index 0000000000..661123baf8
--- /dev/null
+++ b/packages/auth-plugins/src/AuthPluginParent.tsx
@@ -0,0 +1,61 @@
+import React, { useEffect, useState } from 'react';
+import { LoginOptions } from '@deephaven/jsapi-types';
+import {
+ LOGIN_OPTIONS_REQUEST,
+ requestParentResponse,
+} from '@deephaven/jsapi-utils';
+import Log from '@deephaven/log';
+import { LoadingOverlay } from '@deephaven/components';
+import { AuthPlugin, AuthPluginProps } from './AuthPlugin';
+
+const log = Log.module('AuthPluginParent');
+
+function getWindowAuthProvider(): string {
+ return new URLSearchParams(window.location.search).get('authProvider') ?? '';
+}
+
+/**
+ * AuthPlugin that tries to delegate to the parent window for authentication. Fails if there is no parent window.
+ */
+function Component({
+ client,
+ onSuccess,
+ onFailure,
+}: AuthPluginProps): JSX.Element {
+ const [error, setError] = useState();
+ useEffect(() => {
+ async function login() {
+ try {
+ log.info('Logging in by delegating to parent window...');
+ const loginOptions = await requestParentResponse(
+ LOGIN_OPTIONS_REQUEST
+ );
+
+ await client.login(loginOptions);
+ log.info('Logged in successfully.');
+ onSuccess();
+ } catch (e) {
+ log.error('Unable to login:', e);
+ setError(e);
+ onFailure(e);
+ }
+ }
+ login();
+ }, [client, onFailure, onSuccess]);
+ return (
+
+ );
+}
+
+const AuthPluginParent: AuthPlugin = {
+ Component,
+ isAvailable: () =>
+ window.opener != null && getWindowAuthProvider() === 'parent',
+};
+
+export default AuthPluginParent;
diff --git a/packages/auth-plugins/src/AuthPluginPsk.test.tsx b/packages/auth-plugins/src/AuthPluginPsk.test.tsx
new file mode 100644
index 0000000000..90a335a757
--- /dev/null
+++ b/packages/auth-plugins/src/AuthPluginPsk.test.tsx
@@ -0,0 +1,123 @@
+import React from 'react';
+import { act, render, screen } from '@testing-library/react';
+import { dh } from '@deephaven/jsapi-shim';
+import AuthPluginPsk from './AuthPluginPsk';
+import { AUTH_HANDLER_TYPE_PSK as AUTH_TYPE } from './AuthHandlerTypes';
+
+function makeCoreClient() {
+ return new dh.CoreClient('wss://test.mockurl.example.com');
+}
+
+describe('availability tests', () => {
+ it.each([
+ [[AUTH_TYPE], true],
+ [['another.type', AUTH_TYPE], true],
+ [[], false],
+ [
+ ['not-anonymous', `${AUTH_TYPE}.withsuffix`, `prefix.${AUTH_TYPE}`],
+ false,
+ ],
+ ])(
+ 'returns availability based on auth handlers: %s',
+ (authHandlers, result) => {
+ expect(AuthPluginPsk.isAvailable(authHandlers)).toBe(result);
+ }
+ );
+});
+
+function expectLoading() {
+ expect(screen.queryByTestId('auth-psk-loading')).not.toBeNull();
+}
+
+describe('component tests', () => {
+ const authConfigValues = new Map();
+ it('fails if no psk is provided on query URL string', () => {
+ const onSuccess = jest.fn();
+ const onFailure = jest.fn();
+ const mockLogin = jest.fn();
+ const client = makeCoreClient();
+ client.login = mockLogin;
+ render(
+
+ );
+ expect(onSuccess).not.toHaveBeenCalled();
+ expect(onFailure).toHaveBeenCalled();
+ expect(mockLogin).not.toHaveBeenCalled();
+ });
+
+ it('uses the psk provided on the query URL string', async () => {
+ const mockToken = 'mock-token';
+ window.history.pushState({}, 'Test Title', `/test.html?psk=${mockToken}`);
+ const loginPromise = Promise.resolve();
+ const onSuccess = jest.fn();
+ const onFailure = jest.fn();
+ const mockLogin = jest.fn(() => loginPromise);
+ const client = makeCoreClient();
+ client.login = mockLogin;
+ render(
+
+ );
+ expectLoading();
+ expect(onSuccess).not.toHaveBeenCalled();
+ expect(onFailure).not.toHaveBeenCalled();
+ expect(mockLogin).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: AUTH_TYPE,
+ token: mockToken,
+ })
+ );
+ await loginPromise;
+
+ expect(onSuccess).toHaveBeenCalled();
+ expect(onFailure).not.toHaveBeenCalled();
+ });
+
+ it('reports failure if the psk provided on the query URL string fails login', async () => {
+ const mockToken = 'mock-token';
+ window.history.pushState({}, 'Test Title', `/test.html?psk=${mockToken}`);
+ const loginError = 'Invalid token';
+ const loginPromise = Promise.reject(loginError);
+ const onSuccess = jest.fn();
+ const onFailure = jest.fn();
+ const mockLogin = jest.fn(() => loginPromise);
+ const client = makeCoreClient();
+ client.login = mockLogin;
+ render(
+
+ );
+ expectLoading();
+ expect(onSuccess).not.toHaveBeenCalled();
+ expect(onFailure).not.toHaveBeenCalled();
+ expect(mockLogin).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: AUTH_TYPE,
+ token: mockToken,
+ })
+ );
+ await act(async () => {
+ try {
+ await loginPromise;
+ } catch (e) {
+ // We know it fails
+ }
+ });
+
+ expect(onSuccess).not.toHaveBeenCalled();
+ expect(onFailure).toHaveBeenCalledWith(loginError);
+ });
+});
diff --git a/packages/auth-plugins/src/AuthPluginPsk.tsx b/packages/auth-plugins/src/AuthPluginPsk.tsx
new file mode 100644
index 0000000000..7d59512618
--- /dev/null
+++ b/packages/auth-plugins/src/AuthPluginPsk.tsx
@@ -0,0 +1,72 @@
+import React, { useEffect, useState } from 'react';
+import Log from '@deephaven/log';
+import { LoadingOverlay } from '@deephaven/components';
+import { AuthPlugin, AuthPluginProps } from './AuthPlugin';
+
+const log = Log.module('AuthPluginPsk');
+
+const AUTH_TYPE = 'io.deephaven.authentication.psk.PskAuthenticationHandler';
+
+function getWindowToken(): string {
+ return new URLSearchParams(window.location.search).get('psk') ?? '';
+}
+
+/**
+ * AuthPlugin that tries to login using a pre-shared key.
+ * Add the `psk=` parameter to your URL string to set the token.
+ */
+function Component({
+ client,
+ onSuccess,
+ onFailure,
+}: AuthPluginProps): JSX.Element {
+ const [error, setError] = useState();
+ const [token] = useState(() => getWindowToken());
+ useEffect(() => {
+ let isCanceled = false;
+ async function login() {
+ try {
+ if (!token) {
+ throw new Error(
+ 'No Pre-shared key token found. Add `psk=` parameter to your URL'
+ );
+ }
+ log.info('Logging in with found token...');
+ await client.login({ type: AUTH_TYPE, token });
+ if (isCanceled) {
+ log.info('Previous login result canceled');
+ return;
+ }
+ log.info('Logged in successfully.');
+ onSuccess();
+ } catch (e) {
+ if (isCanceled) {
+ log.info('Previous login failure canceled');
+ return;
+ }
+ log.error('Unable to login:', e);
+ setError(e);
+ onFailure(e);
+ }
+ }
+ login();
+ return () => {
+ isCanceled = true;
+ };
+ }, [client, onFailure, onSuccess, token]);
+ return (
+
+ );
+}
+
+const AuthPluginPsk: AuthPlugin = {
+ Component,
+ isAvailable: authHandlers => authHandlers.includes(AUTH_TYPE),
+};
+
+export default AuthPluginPsk;
diff --git a/packages/auth-plugins/src/index.ts b/packages/auth-plugins/src/index.ts
new file mode 100644
index 0000000000..b7d6400a9d
--- /dev/null
+++ b/packages/auth-plugins/src/index.ts
@@ -0,0 +1,5 @@
+export * from './AuthPlugin';
+export * from './AuthHandlerTypes';
+export { default as AuthPluginAnonymous } from './AuthPluginAnonymous';
+export { default as AuthPluginParent } from './AuthPluginParent';
+export { default as AuthPluginPsk } from './AuthPluginPsk';
diff --git a/packages/auth-plugins/tsconfig.json b/packages/auth-plugins/tsconfig.json
new file mode 100644
index 0000000000..ce3aa0d789
--- /dev/null
+++ b/packages/auth-plugins/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "src/",
+ "outDir": "dist/"
+ },
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx"],
+ "exclude": ["node_modules", "src/**/*.test.*", "src/**/__mocks__/*"],
+ "references": [
+ { "path": "../components" },
+ { "path": "../jsapi-bootstrap" },
+ { "path": "../jsapi-types" },
+ { "path": "../jsapi-utils" },
+ { "path": "../log" }
+ ]
+}
diff --git a/packages/code-studio/.env b/packages/code-studio/.env
index c5692ca509..abb75f3905 100644
--- a/packages/code-studio/.env
+++ b/packages/code-studio/.env
@@ -3,7 +3,6 @@
BASE_URL=/ide/
VITE_CORE_API_URL=/jsapi
VITE_CORE_API_NAME=dh-core.js
-VITE_OPEN_API_NAME=dh-internal.js
VITE_PLUGIN_URL=/ide/plugins/
VITE_PROXY_URL=http://localhost:10000
VITE_NOTEBOOKS_URL=/notebooks
diff --git a/packages/code-studio/package.json b/packages/code-studio/package.json
index 23f2e56799..ca224a11ed 100644
--- a/packages/code-studio/package.json
+++ b/packages/code-studio/package.json
@@ -11,6 +11,8 @@
"directory": "packages/code-studio"
},
"dependencies": {
+ "@deephaven/app-utils": "file:../app-utils",
+ "@deephaven/auth-plugins": "file:../auth-plugins",
"@deephaven/chart": "file:../chart",
"@deephaven/components": "file:../components",
"@deephaven/console": "file:../console",
@@ -23,6 +25,7 @@
"@deephaven/icons": "file:../icons",
"@deephaven/iris-grid": "file:../iris-grid",
"@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
+ "@deephaven/jsapi-components": "file:../jsapi-components",
"@deephaven/jsapi-shim": "file:../jsapi-shim",
"@deephaven/jsapi-types": "file:../jsapi-types",
"@deephaven/jsapi-utils": "file:../jsapi-utils",
@@ -34,11 +37,8 @@
"@deephaven/utils": "file:../utils",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/react-fontawesome": "^0.2.0",
- "@paciolan/remote-component": "2.7.2",
- "@paciolan/remote-module-loader": "^3.0.2",
"classnames": "^2.3.1",
"event-target-shim": "^6.0.2",
- "fira": "mozilla/fira#4.202",
"jszip": "3.2.2",
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
diff --git a/packages/code-studio/src/AppRoot.tsx b/packages/code-studio/src/AppRoot.tsx
index 8326059435..516a3146d1 100644
--- a/packages/code-studio/src/AppRoot.tsx
+++ b/packages/code-studio/src/AppRoot.tsx
@@ -5,10 +5,8 @@ import { store } from '@deephaven/redux';
import MonacoWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import AppRouter from './main/AppRouter';
import DownloadServiceWorkerUtils from './DownloadServiceWorkerUtils';
-import { unregister } from './serviceWorker';
export function AppRoot() {
- unregister();
DownloadServiceWorkerUtils.registerOnLoaded();
MonacoUtils.init({ getWorker: () => new MonacoWorker() });
diff --git a/packages/code-studio/src/index.tsx b/packages/code-studio/src/index.tsx
index a6015db335..4f98f1be7b 100644
--- a/packages/code-studio/src/index.tsx
+++ b/packages/code-studio/src/index.tsx
@@ -1,6 +1,5 @@
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom';
-import 'fira';
import '@deephaven/components/scss/BaseStyleSheet.scss';
import { LoadingOverlay } from '@deephaven/components';
import { ApiBootstrap } from '@deephaven/jsapi-bootstrap';
@@ -8,9 +7,16 @@ import logInit from './log/LogInit';
logInit();
+// Lazy load components for code splitting and also to avoid importing the jsapi-shim before API is bootstrapped.
// eslint-disable-next-line react-refresh/only-export-components
const AppRoot = React.lazy(() => import('./AppRoot'));
+// eslint-disable-next-line react-refresh/only-export-components
+const AppBootstrap = React.lazy(async () => {
+ const module = await import('@deephaven/app-utils');
+ return { default: module.AppBootstrap };
+});
+
ReactDOM.render(
}>
-
+
+
+
,
document.getElementById('root')
diff --git a/packages/code-studio/src/main/AppInit.tsx b/packages/code-studio/src/main/AppInit.tsx
index b85d551882..44cae37fac 100644
--- a/packages/code-studio/src/main/AppInit.tsx
+++ b/packages/code-studio/src/main/AppInit.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
@@ -11,22 +11,22 @@ import {
setDashboardData as setDashboardDataAction,
} from '@deephaven/dashboard';
import {
- SessionDetails,
- SessionWrapper,
setDashboardConnection as setDashboardConnectionAction,
setDashboardSessionWrapper as setDashboardSessionWrapperAction,
ToolType,
} from '@deephaven/dashboard-core-plugins';
import { FileStorage } from '@deephaven/file-explorer';
-import dh, { IdeConnection } from '@deephaven/jsapi-shim';
+import { IdeConnection } from '@deephaven/jsapi-types';
import {
DecimalColumnFormatter,
+ getSessionDetails,
IntegerColumnFormatter,
+ loadSessionWrapper,
+ SessionWrapper,
} from '@deephaven/jsapi-utils';
import Log from '@deephaven/log';
import { PouchCommandHistoryStorage } from '@deephaven/pouch-storage';
import {
- DeephavenPluginModuleMap,
getWorkspace,
getWorkspaceStorage,
RootState,
@@ -42,90 +42,18 @@ import {
Workspace,
WorkspaceStorage,
ServerConfigValues,
- DeephavenPluginModule,
+ DeephavenPluginModuleMap,
} from '@deephaven/redux';
+import { useClient, useConnection, usePlugins } from '@deephaven/app-utils';
import { setLayoutStorage as setLayoutStorageAction } from '../redux/actions';
import App from './App';
import LocalWorkspaceStorage from '../storage/LocalWorkspaceStorage';
-import {
- AUTH_TYPE,
- createConnection,
- createCoreClient,
- createSessionWrapper,
- getAuthType,
- getEnvoyPrefix,
- getLoginOptions,
- getSessionDetails,
-} from './SessionUtils';
-import { PluginUtils } from '../plugins';
import LayoutStorage from '../storage/LayoutStorage';
-import { isNoConsolesError } from './NoConsolesError';
import GrpcLayoutStorage from '../storage/grpc/GrpcLayoutStorage';
import GrpcFileStorage from '../storage/grpc/GrpcFileStorage';
const log = Log.module('AppInit');
-/**
- * Load all plugin modules available.
- * @returns A map from the name of the plugin to the plugin module that was loaded
- */
-async function loadPlugins(): Promise {
- log.debug('Loading plugins...');
- try {
- const manifest = await PluginUtils.loadJson(
- `${import.meta.env.VITE_MODULE_PLUGINS_URL}/manifest.json`
- );
-
- if (!Array.isArray(manifest.plugins)) {
- throw new Error('Plugin manifest JSON does not contain plugins array');
- }
-
- log.debug('Plugin manifest loaded:', manifest);
- const pluginPromises: Promise[] = [];
- for (let i = 0; i < manifest.plugins.length; i += 1) {
- const { name, main } = manifest.plugins[i];
- const pluginMainUrl = `${
- import.meta.env.VITE_MODULE_PLUGINS_URL
- }/${name}/${main}`;
- pluginPromises.push(PluginUtils.loadModulePlugin(pluginMainUrl));
- }
- const pluginModules = await Promise.allSettled(pluginPromises);
-
- const pluginMap: DeephavenPluginModuleMap = new Map();
- for (let i = 0; i < pluginModules.length; i += 1) {
- const module = pluginModules[i];
- const { name } = manifest.plugins[i];
- if (module.status === 'fulfilled') {
- pluginMap.set(name, module.value as DeephavenPluginModule);
- } else {
- log.error(`Unable to load plugin ${name}`, module.reason);
- }
- }
- log.info('Plugins loaded:', pluginMap);
-
- return pluginMap;
- } catch (e) {
- log.error('Unable to load plugins:', e);
- return new Map();
- }
-}
-
-async function loadSessionWrapper(
- connection: IdeConnection,
- sessionDetails: SessionDetails
-): Promise {
- let sessionWrapper: SessionWrapper | undefined;
- try {
- sessionWrapper = await createSessionWrapper(connection, sessionDetails);
- } catch (e) {
- // Consoles may be disabled on the server, but we should still be able to start up and open existing objects
- if (!isNoConsolesError(e)) {
- throw e;
- }
- }
- return sessionWrapper;
-}
-
interface AppInitProps {
workspace: Workspace;
workspaceStorage: WorkspaceStorage;
@@ -167,216 +95,159 @@ function AppInit(props: AppInitProps) {
setServerConfigValues,
} = props;
+ const client = useClient();
+ const connection = useConnection();
+ const plugins = usePlugins();
// General error means the app is dead and is unlikely to recover
const [error, setError] = useState();
- // Disconnect error may be temporary, so just show an error overlaid on the app
- const [disconnectError, setDisconnectError] = useState();
- const [isFontLoading, setIsFontLoading] = useState(true);
-
- const initClient = useCallback(async () => {
- try {
- log.info(
- 'Initializing Web UI',
- import.meta.env.npm_package_version,
- navigator.userAgent
- );
-
- const envoyPrefix = getEnvoyPrefix();
- const options =
- envoyPrefix != null
- ? { headers: { 'envoy-prefix': envoyPrefix } }
- : undefined;
- const coreClient = createCoreClient(options);
- const authType = getAuthType();
- log.info(`Login using auth type ${authType}...`);
- const [loginOptions, sessionDetails] = await Promise.all([
- getLoginOptions(authType),
- getSessionDetails(authType),
- ]);
- await coreClient.login(loginOptions);
-
- const newPlugins = await loadPlugins();
- const connection = await (authType === AUTH_TYPE.ANONYMOUS &&
- coreClient.getAsIdeConnection == null
- ? // Fall back to the old API for anonymous auth if the new API is not supported
- createConnection()
- : coreClient.getAsIdeConnection());
- connection.addEventListener(dh.IdeConnection.EVENT_SHUTDOWN, event => {
- const { detail } = event;
- log.info('Shutdown', `${JSON.stringify(detail)}`);
- setError(`Server shutdown: ${detail ?? 'Unknown reason'}`);
- setDisconnectError(null);
- });
-
- const sessionWrapper = await loadSessionWrapper(
- connection,
- sessionDetails
- );
- const name = 'user';
-
- const storageService = coreClient.getStorageService();
- const layoutStorage = new GrpcLayoutStorage(
- storageService,
- import.meta.env.VITE_LAYOUTS_URL ?? ''
- );
- const fileStorage = new GrpcFileStorage(
- storageService,
- import.meta.env.VITE_NOTEBOOKS_URL ?? ''
- );
-
- const workspaceStorage = new LocalWorkspaceStorage(layoutStorage);
- const commandHistoryStorage = new PouchCommandHistoryStorage();
-
- const loadedWorkspace = await workspaceStorage.load({
- isConsoleAvailable: sessionWrapper !== undefined,
- });
- const { data } = loadedWorkspace;
-
- // Fill in settings that have not yet been set
- const { settings } = data;
- if (settings.defaultDecimalFormatOptions === undefined) {
- settings.defaultDecimalFormatOptions = {
- defaultFormatString: DecimalColumnFormatter.DEFAULT_FORMAT_STRING,
- };
- }
- if (settings.defaultIntegerFormatOptions === undefined) {
- settings.defaultIntegerFormatOptions = {
- defaultFormatString: IntegerColumnFormatter.DEFAULT_FORMAT_STRING,
- };
- }
-
- if (settings.truncateNumbersWithPound === undefined) {
- settings.truncateNumbersWithPound = false;
- }
-
- // Set any shortcuts that user has overridden on this platform
- const { shortcutOverrides = {} } = settings;
- const isMac = Shortcut.isMacPlatform;
- const platformOverrides = isMac
- ? shortcutOverrides.mac ?? {}
- : shortcutOverrides.windows ?? {};
-
- Object.entries(platformOverrides).forEach(([id, keyState]) => {
- ShortcutRegistry.get(id)?.setKeyState(keyState);
- });
-
- const dashboardData = {
- filterSets: data.filterSets,
- links: data.links,
- };
-
- const configs = await coreClient.getServerConfigValues();
- const serverConfig = new Map(configs);
-
- const user: User = {
- name,
- operateAs: name,
- groups: [],
- permissions: {
- isACLEditor: false,
- isSuperUser: false,
- isQueryViewOnly: false,
- isNonInteractive: false,
- canUsePanels: true,
- canCreateDashboard: true,
- canCreateCodeStudio: true,
- canCreateQueryMonitor: true,
- canCopy: !(
- serverConfig.get('internal.webClient.appInit.canCopy') === 'false'
- ),
- canDownloadCsv: !(
- serverConfig.get('internal.webClient.appInit.canDownloadCsv') ===
- 'false'
- ),
- },
- };
-
- setActiveTool(ToolType.DEFAULT);
- setServerConfigValues(serverConfig);
- setCommandHistoryStorage(commandHistoryStorage);
- setDashboardData(DEFAULT_DASHBOARD_ID, dashboardData);
- setFileStorage(fileStorage);
- setLayoutStorage(layoutStorage);
- setDashboardConnection(DEFAULT_DASHBOARD_ID, connection);
- if (sessionWrapper !== undefined) {
- setDashboardSessionWrapper(DEFAULT_DASHBOARD_ID, sessionWrapper);
- }
- setPlugins(newPlugins);
- setUser(user);
- setWorkspaceStorage(workspaceStorage);
- setWorkspace(loadedWorkspace);
- } catch (e: unknown) {
- log.error(e);
- setError(e);
- }
- }, [
- setActiveTool,
- setCommandHistoryStorage,
- setDashboardData,
- setFileStorage,
- setLayoutStorage,
- setDashboardConnection,
- setDashboardSessionWrapper,
- setPlugins,
- setUser,
- setWorkspace,
- setWorkspaceStorage,
- setServerConfigValues,
- ]);
-
- const initFonts = useCallback(() => {
- if (document.fonts != null) {
- document.fonts.ready.then(() => {
- setIsFontLoading(false);
- });
- } else {
- // If document.fonts isn't supported, just best guess assume they're loaded
- setIsFontLoading(false);
- }
- }, []);
+ useEffect(
+ function setReduxPlugins() {
+ setPlugins(plugins);
+ },
+ [plugins, setPlugins]
+ );
useEffect(
- function initClientAndFonts() {
- initClient();
- initFonts();
+ function initApp() {
+ async function loadApp() {
+ try {
+ const sessionDetails = await getSessionDetails();
+ const sessionWrapper = await loadSessionWrapper(
+ connection,
+ sessionDetails
+ );
+ const name = 'user';
+
+ const storageService = client.getStorageService();
+ const layoutStorage = new GrpcLayoutStorage(
+ storageService,
+ import.meta.env.VITE_LAYOUTS_URL ?? ''
+ );
+ const fileStorage = new GrpcFileStorage(
+ storageService,
+ import.meta.env.VITE_NOTEBOOKS_URL ?? ''
+ );
+
+ const workspaceStorage = new LocalWorkspaceStorage(layoutStorage);
+ const commandHistoryStorage = new PouchCommandHistoryStorage();
+
+ const loadedWorkspace = await workspaceStorage.load({
+ isConsoleAvailable: sessionWrapper !== undefined,
+ });
+ const { data } = loadedWorkspace;
+
+ // Fill in settings that have not yet been set
+ const { settings } = data;
+ if (settings.defaultDecimalFormatOptions === undefined) {
+ settings.defaultDecimalFormatOptions = {
+ defaultFormatString: DecimalColumnFormatter.DEFAULT_FORMAT_STRING,
+ };
+ }
+
+ if (settings.defaultIntegerFormatOptions === undefined) {
+ settings.defaultIntegerFormatOptions = {
+ defaultFormatString: IntegerColumnFormatter.DEFAULT_FORMAT_STRING,
+ };
+ }
+
+ if (settings.truncateNumbersWithPound === undefined) {
+ settings.truncateNumbersWithPound = false;
+ }
+
+ // Set any shortcuts that user has overridden on this platform
+ const { shortcutOverrides = {} } = settings;
+ const isMac = Shortcut.isMacPlatform;
+ const platformOverrides = isMac
+ ? shortcutOverrides.mac ?? {}
+ : shortcutOverrides.windows ?? {};
+
+ Object.entries(platformOverrides).forEach(([id, keyState]) => {
+ ShortcutRegistry.get(id)?.setKeyState(keyState);
+ });
+
+ const dashboardData = {
+ filterSets: data.filterSets,
+ links: data.links,
+ };
+
+ const configs = await client.getServerConfigValues();
+ const serverConfig = new Map(configs);
+
+ const user: User = {
+ name,
+ operateAs: name,
+ groups: [],
+ permissions: {
+ isACLEditor: false,
+ isSuperUser: false,
+ isQueryViewOnly: false,
+ isNonInteractive: false,
+ canUsePanels: true,
+ canCreateDashboard: true,
+ canCreateCodeStudio: true,
+ canCreateQueryMonitor: true,
+ canCopy: !(
+ serverConfig.get('internal.webClient.appInit.canCopy') ===
+ 'false'
+ ),
+ canDownloadCsv: !(
+ serverConfig.get(
+ 'internal.webClient.appInit.canDownloadCsv'
+ ) === 'false'
+ ),
+ },
+ };
+
+ setActiveTool(ToolType.DEFAULT);
+ setServerConfigValues(serverConfig);
+ setCommandHistoryStorage(commandHistoryStorage);
+ setDashboardData(DEFAULT_DASHBOARD_ID, dashboardData);
+ setFileStorage(fileStorage);
+ setLayoutStorage(layoutStorage);
+ setDashboardConnection(DEFAULT_DASHBOARD_ID, connection);
+ if (sessionWrapper !== undefined) {
+ setDashboardSessionWrapper(DEFAULT_DASHBOARD_ID, sessionWrapper);
+ }
+ setUser(user);
+ setWorkspaceStorage(workspaceStorage);
+ setWorkspace(loadedWorkspace);
+ } catch (e) {
+ log.error(e);
+ setError(e);
+ }
+ }
+ loadApp();
},
- [initClient, initFonts]
+ [
+ client,
+ connection,
+ setActiveTool,
+ setCommandHistoryStorage,
+ setDashboardData,
+ setFileStorage,
+ setLayoutStorage,
+ setDashboardConnection,
+ setDashboardSessionWrapper,
+ setUser,
+ setWorkspace,
+ setWorkspaceStorage,
+ setServerConfigValues,
+ ]
);
- const isLoading = (workspace == null && error == null) || isFontLoading;
+ const isLoading = workspace == null && error == null;
const isLoaded = !isLoading && error == null;
- const errorMessage =
- error != null || disconnectError != null
- ? `${error ?? disconnectError}`
- : null;
+ const errorMessage = error != null ? `${error}` : null;
return (
<>
{isLoaded && }
- {/*
- Need to preload any monaco and Deephaven grid fonts.
- We hide text with all the fonts we need on the root app.jsx page
- Load the Fira Mono font so that Monaco calculates word wrapping properly.
- This element doesn't need to be visible, just load the font and stay hidden.
- https://github.com/microsoft/vscode/issues/88689
- Can be replaced with a rel="preload" when firefox adds support
- https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content
- */}
-
- {/* trigger loading of fonts needed by monaco and iris grid */}
-
preload
-
preload
-
preload
-
>
);
}
diff --git a/packages/code-studio/src/main/AppMainContainer.tsx b/packages/code-studio/src/main/AppMainContainer.tsx
index 3893588ff9..b90e37209d 100644
--- a/packages/code-studio/src/main/AppMainContainer.tsx
+++ b/packages/code-studio/src/main/AppMainContainer.tsx
@@ -60,7 +60,6 @@ import {
PandasPanelProps,
IrisGridPanelProps,
ColumnSelectionValidator,
- SessionConfig,
getDashboardConnection,
} from '@deephaven/dashboard-core-plugins';
import {
@@ -75,7 +74,9 @@ import dh, {
VariableDefinition,
VariableTypeUnion,
} from '@deephaven/jsapi-shim';
+import { SessionConfig } from '@deephaven/jsapi-utils';
import Log from '@deephaven/log';
+import { getBaseUrl, loadComponentPlugin } from '@deephaven/app-utils';
import {
getActiveTool,
getWorkspace,
@@ -84,11 +85,11 @@ import {
updateWorkspaceData as updateWorkspaceDataAction,
getPlugins,
Workspace,
- DeephavenPluginModuleMap,
WorkspaceData,
RootState,
UserPermissions,
ServerConfigValues,
+ DeephavenPluginModuleMap,
} from '@deephaven/redux';
import { PromiseUtils } from '@deephaven/utils';
import GoldenLayout from '@deephaven/golden-layout';
@@ -108,7 +109,6 @@ import {
import EmptyDashboard from './EmptyDashboard';
import UserLayoutUtils from './UserLayoutUtils';
import DownloadServiceWorkerUtils from '../DownloadServiceWorkerUtils';
-import { PluginUtils } from '../plugins';
import LayoutStorage from '../storage/LayoutStorage';
const log = Log.module('AppMainContainer');
@@ -660,8 +660,8 @@ export class AppMainContainer extends Component<
TablePlugin: ForwardRefExoticComponent>;
}).TablePlugin;
}
-
- return PluginUtils.loadComponentPlugin(pluginName);
+ const baseURL = getBaseUrl(import.meta.env.VITE_COMPONENT_PLUGINS_URL);
+ return loadComponentPlugin(baseURL, pluginName);
}
startListeningForDisconnect() {
@@ -918,10 +918,7 @@ export class AppMainContainer extends Component<
diff --git a/packages/code-studio/src/main/SessionUtils.ts b/packages/code-studio/src/main/SessionUtils.ts
deleted file mode 100644
index 5a544e392e..0000000000
--- a/packages/code-studio/src/main/SessionUtils.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-import {
- SessionDetails,
- SessionWrapper,
-} from '@deephaven/dashboard-core-plugins';
-import dh, {
- ConnectOptions,
- CoreClient,
- IdeConnection,
- LoginOptions,
-} from '@deephaven/jsapi-shim';
-import {
- LOGIN_OPTIONS_REQUEST,
- requestParentResponse,
- SESSION_DETAILS_REQUEST,
-} from '@deephaven/jsapi-utils';
-import Log from '@deephaven/log';
-import shortid from 'shortid';
-import NoConsolesError from './NoConsolesError';
-
-const log = Log.module('SessionUtils');
-
-export enum AUTH_TYPE {
- ANONYMOUS = 'anonymous',
- PARENT = 'parent',
-}
-
-export function getBaseUrl(): URL {
- return new URL(import.meta.env.VITE_CORE_API_URL ?? '', `${window.location}`);
-}
-
-export function getWebsocketUrl(): string {
- const baseUrl = getBaseUrl();
- return `${baseUrl.protocol}//${baseUrl.host}`;
-}
-
-export function getAuthType(): AUTH_TYPE {
- const searchParams = new URLSearchParams(window.location.search);
- switch (searchParams.get('authProvider')) {
- case 'parent':
- return AUTH_TYPE.PARENT;
- default:
- return AUTH_TYPE.ANONYMOUS;
- }
-}
-
-export function getEnvoyPrefix(): string | null {
- const searchParams = new URLSearchParams(window.location.search);
- return searchParams.get('envoyPrefix');
-}
-
-/**
- * @returns New connection to the server
- */
-export function createConnection(): IdeConnection {
- const websocketUrl = getWebsocketUrl();
-
- log.info(`Starting connection to '${websocketUrl}'...`);
-
- return new dh.IdeConnection(websocketUrl);
-}
-
-/**
- * Create a new session using the default URL
- * @returns A session and config that is ready to use
- */
-export async function createSessionWrapper(
- connection: IdeConnection,
- details: SessionDetails
-): Promise {
- log.info('Getting console types...');
-
- const types = await connection.getConsoleTypes();
-
- log.info('Available types:', types);
-
- if (types.length === 0) {
- throw new NoConsolesError('No console types available');
- }
-
- const type = types[0];
-
- log.info('Starting session with type', type);
-
- const session = await connection.startSession(type);
-
- const config = { type, id: shortid.generate() };
-
- log.info('Console session established', config);
-
- return {
- session,
- config,
- connection,
- details,
- };
-}
-
-export function createCoreClient(options?: ConnectOptions): CoreClient {
- const websocketUrl = getWebsocketUrl();
-
- log.info('createCoreClient', websocketUrl);
-
- return new dh.CoreClient(websocketUrl, options);
-}
-
-async function requestParentLoginOptions(): Promise {
- return requestParentResponse(LOGIN_OPTIONS_REQUEST);
-}
-
-async function requestParentSessionDetails(): Promise {
- return requestParentResponse(SESSION_DETAILS_REQUEST);
-}
-
-export async function getLoginOptions(
- authType: AUTH_TYPE
-): Promise {
- switch (authType) {
- case AUTH_TYPE.PARENT:
- return requestParentLoginOptions();
- case AUTH_TYPE.ANONYMOUS:
- return { type: dh.CoreClient.LOGIN_TYPE_ANONYMOUS };
- default:
- throw new Error(`Unknown auth type: ${authType}`);
- }
-}
-
-export async function getSessionDetails(
- authType: AUTH_TYPE
-): Promise {
- switch (authType) {
- case AUTH_TYPE.PARENT:
- return requestParentSessionDetails();
- case AUTH_TYPE.ANONYMOUS:
- return {};
- default:
- throw new Error(`Unknown auth type: ${authType}`);
- }
-}
-
-export default { createSessionWrapper };
diff --git a/packages/code-studio/src/plugins/PluginUtils.tsx b/packages/code-studio/src/plugins/PluginUtils.tsx
deleted file mode 100644
index 1f01172caa..0000000000
--- a/packages/code-studio/src/plugins/PluginUtils.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import React, { ForwardRefExoticComponent } from 'react';
-import Log from '@deephaven/log';
-import RemoteComponent from './RemoteComponent';
-import loadRemoteModule from './loadRemoteModule';
-
-const log = Log.module('PluginUtils');
-
-class PluginUtils {
- /**
- * Load a component plugin from the server.
- * @param pluginName Name of the table plugin to load
- * @returns A lazily loaded JSX.Element from the plugin
- */
- static loadComponentPlugin(
- pluginName: string
- ): ForwardRefExoticComponent> {
- const baseUrl = new URL(
- import.meta.env.VITE_COMPONENT_PLUGINS_URL ?? '',
- `${window.location}`
- );
- const pluginUrl = new URL(`${pluginName}.js`, baseUrl);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const Plugin: any = React.forwardRef((props, ref) => (
- {
- if (err != null && err !== '') {
- const errorMessage = `Error loading plugin ${pluginName} from ${pluginUrl} due to ${err}`;
- log.error(errorMessage);
- return {`${errorMessage}`}
;
- }
- // eslint-disable-next-line react/jsx-props-no-spreading
- return ;
- }}
- />
- ));
- Plugin.pluginName = pluginName;
- Plugin.displayName = 'Plugin';
- return Plugin;
- }
-
- /**
- * Imports a commonjs plugin module from the provided URL
- * @param pluginUrl The URL of the plugin to load
- * @returns The loaded module
- */
- static async loadModulePlugin(pluginUrl: string): Promise {
- const myModule = await loadRemoteModule(pluginUrl);
- return myModule;
- }
-
- /**
- * Loads a JSON file and returns the JSON object
- * @param jsonUrl The URL of the JSON file to load
- * @returns The JSON object of the manifest file
- */
- static async loadJson(
- jsonUrl: string
- ): Promise<{ plugins: { name: string; main: string }[] }> {
- const res = await fetch(jsonUrl);
- if (!res.ok) {
- throw new Error(res.statusText);
- }
- try {
- return await res.json();
- } catch {
- throw new Error('Could not be parsed as JSON');
- }
- }
-}
-
-export default PluginUtils;
diff --git a/packages/code-studio/src/plugins/README.md b/packages/code-studio/src/plugins/README.md
deleted file mode 100644
index a12f285985..0000000000
--- a/packages/code-studio/src/plugins/README.md
+++ /dev/null
@@ -1,139 +0,0 @@
-# Deephaven Javascript Plugins
-
-Javascript plugins allow a user to write arbitrary Javascript code and attach it to a Table.
-
-## Creating a Plugin
-
-1. Start with default `Remote Component` setup from here:
- https://github.com/Paciolan/remote-component#creating-remote-components
-
-2. Install Web Pack
- https://webpack.js.org/guides/getting-started/
-
- ```
- npm install webpack webpack-cli --save-dev
- ```
-
-3. Install Babel
- https://www.valentinog.com/blog/babel/
-
- ```
- npm i @babel/core babel-loader @babel/preset-env @babel/preset-react --save-dev
- ```
-
-4. Create `.babelrc`
-
- ```
- {
- "presets": ["@babel/preset-env", "@babel/preset-react"]
- }
- ```
-
-5. Update `webpack.config.js` to look like this:
-
- ```
- module.exports = {
- output: {
- libraryTarget: "commonjs"
- },
- externals: {
- react: "react",
- },
- module: {
- rules: [
- {
- test: /\.(js|jsx)$/,
- exclude: /node_modules/,
- use: {
- loader: "babel-loader"
- }
- }
- ]
- }
- };
- ```
-
-6. Place your Javascript code in `index.js`
-
-7. Run Web Pack
-
-```
-/npx webpack --config webpack.config.js
-```
-
-Your output will be in `dist/main.js`
-
-## Javascript Code for a Plugin
-
-1. Your plugin must be a React Component.
-
-2. The `table`, `user`, `client`, `panel`, `onFilter` method, and an `onFetchColumns` method are passed in as props.
-
-```
-const { table, onFilter, onFetchColumns } = this.props;
-onFilter([
- {
- name: 'column name',
- type: 'column type',
- value: 'value to filter on',
- },
-]),
-// These columns will always be in the viewport
-onFetchColumns(['A', 'B'])
-```
-
-3. You may have an optional `getMenu(data)` method that will return an array of menu objects.
-
-```
-getMenu(data) {
- const { value } = data;
- const actions = [];
-
- actions.push({
- title: 'Display value',
- group: 0,
- order: 0,
- action: () => alert(value),
- });
-
- return actions
-}
-```
-
-The `data` object contains the following:
-
-```
-{
- table,
- model,
- value,
- valueText,
- column,
- rowIndex,
- columnIndex,
- modelRow,
- modelColumn,
-}
-```
-
-## Uploading a Plugin
-
-1. Create a directory on the Server to place the plugins.
-
-2. Set the config value for `Webapi.plugins` to point to the plugins directory.
-
-3. Copy the output file `main.js` to that directory on the server and rename it (e.g. `ExamplePlugin.js`).
-
-4. The file name is used as the name of the plugin.
- e.g. `ExamplePlugin.js` will be named `ExamplePlugin`
-
-## Attach a Plugin in a Query
-
-Simply set the PLUGIN_NAME attribute on the Table with the name of the plugin
-For a plugin located at https://host/url/iriside/plugins/ExamplePlugin.js
-The name will ExamplePlugin
-
-```
-t=TableTools.emptyTable(100).updateView("MyColumn=`MyValue`")
-t.setAttribute("PluginName", "ExamplePlugin")
-```
diff --git a/packages/code-studio/src/plugins/RemoteComponent.js b/packages/code-studio/src/plugins/RemoteComponent.js
deleted file mode 100644
index 63ff714bf8..0000000000
--- a/packages/code-studio/src/plugins/RemoteComponent.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// These imports directly from dist/lib will possibly break if the version is updated
-// They are used to suppress a dev server warning that is given if using the normal import from the docs
-import { createRemoteComponent } from '@paciolan/remote-component/dist/lib/createRemoteComponent';
-import { createRequires } from '@paciolan/remote-component/dist/lib/createRequires';
-import { resolve } from '../remote-component.config';
-
-const requires = createRequires(resolve);
-
-export const RemoteComponent = createRemoteComponent({ requires });
-export default RemoteComponent;
diff --git a/packages/code-studio/src/plugins/internal/ExampleAppPlugin.jsx b/packages/code-studio/src/plugins/internal/ExampleAppPlugin.jsx
deleted file mode 100644
index f8f6701ad1..0000000000
--- a/packages/code-studio/src/plugins/internal/ExampleAppPlugin.jsx
+++ /dev/null
@@ -1,77 +0,0 @@
-/* eslint-disable */
-import React, { Component } from 'react';
-// import Glue from '@glue42/core'; // uncomment this when glue core is installed
-
-class ExampleAppPlugin extends Component {
- constructor(props) {
- super(props);
- this.handleClick = this.handleClick.bind(this);
-
- this.state = {
- id: null,
- oldPanel: null,
- panelId: null,
- glue: null,
- };
- }
-
- componentDidMount() {
- this.initialize();
- }
-
- async initialize() {
- const { openDashboardByName, openObject } = this.props;
- // uncomment this when glue core is installed
- // const glue = await Glue();
- console.log('Glue42 intialized.', glue);
- this.setState({ glue });
- glue.interop.register(
- {
- name: 'Open',
- accepts: 'string name',
- },
- async args => {
- const { name } = args;
- const tabModel = await openDashboardByName('Test A');
- const { id } = tabModel;
- let oldPanel = null;
- let panelId = null;
- const { dashboardOpenedPanelMaps } = this.props;
- const dashboardMap = dashboardOpenedPanelMaps[id];
- if (dashboardMap) {
- const iterator = dashboardMap.keys();
- let isOpened = false;
- let result = iterator.next();
- while (!result.done) {
- const key = result.value;
- if (dashboardMap.get(key)?.props?.metadata?.table === 'a') {
- oldPanel = dashboardMap.get(key);
- panelId = key;
- isOpened = true;
- openObject('Test', name, key);
- break;
- }
- result = iterator.next();
- }
- if (!isOpened) {
- openObject('Test', name);
- }
- }
- this.setState({ id, oldPanel, panelId });
- }
- );
- }
-
- handleClick() {
- const { glue } = this.state;
- if (glue) {
- glue.interop.invoke('Open', { name: 'a' });
- }
- }
-
- render() {
- return ;
- }
-}
-
-export default ExampleAppPlugin;
diff --git a/packages/code-studio/src/plugins/internal/ExamplePlugin.jsx b/packages/code-studio/src/plugins/internal/ExamplePlugin.jsx
deleted file mode 100644
index f3ee4d246d..0000000000
--- a/packages/code-studio/src/plugins/internal/ExamplePlugin.jsx
+++ /dev/null
@@ -1,146 +0,0 @@
-/* eslint-disable */
-import React, { Component } from 'react';
-import {
- Modal,
- ModalBody,
- ModalFooter,
- ModalHeader,
-} from '@deephaven/components';
-
-class ExamplePlugin extends Component {
- constructor(props) {
- super(props);
-
- this.getMenu = this.getMenu.bind(this);
- this.handleOpenModal = this.handleOpenModal.bind(this);
- this.handleCloseModal = this.handleCloseModal.bind(this);
-
- this.confirmButton = React.createRef();
-
- this.state = {
- isModalOpen: false,
- };
- }
-
- /**
- * Optional method to get a menu from the plugin.
- *
- * @param {object} data data from deephaven
- */
- getMenu(data) {
- const { onFilter, table } = this.props;
- const { value, column, model } = data;
- const { name, type } = column;
- const actions = [];
-
- actions.push({
- title: 'Display value',
- group: 0,
- order: 0,
- action: () => alert(value),
- });
-
- actions.push({
- title: 'Show Dialog',
- group: 0,
- order: 10,
- action: this.handleOpenModal,
- });
-
- actions.push({
- title: 'Display Table',
- group: 0,
- order: 20,
- action: () => alert(table),
- });
-
- actions.push({
- title: 'Display Model',
- group: 0,
- order: 30,
- action: () => alert(model),
- });
-
- const subMenu = [];
-
- actions.push({
- title: 'Filter Sub Menu',
- group: 0,
- order: 40,
- actions: subMenu,
- });
-
- subMenu.push({
- title: 'Filter by value',
- group: 0,
- order: 0,
- action: () =>
- onFilter([
- {
- name,
- type,
- value,
- },
- ]),
- });
-
- subMenu.push({
- title: 'Clear Filter',
- group: 0,
- order: 10,
- action: () => onFilter([]),
- });
-
- return actions;
- }
-
- handleOpenModal() {
- this.setState({
- isModalOpen: true,
- });
- }
-
- handleCloseModal() {
- this.setState({
- isModalOpen: false,
- });
- }
-
- render() {
- const { isModalOpen } = this.state;
-
- return (
-
-
- {
- this.confirmButton.current.focus();
- }}
- >
- Plugin Modal Title
- Plugin Modal Body
-
-
-
-
-
-
- );
- }
-}
-
-export default ExamplePlugin;
diff --git a/packages/code-studio/src/serviceWorker.ts b/packages/code-studio/src/serviceWorker.ts
deleted file mode 100644
index 2bda906a36..0000000000
--- a/packages/code-studio/src/serviceWorker.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-/* eslint-disable no-console, no-param-reassign, no-use-before-define */
-// This optional code is used to register a service worker.
-// register() is not called by default.
-
-// This lets the app load faster on subsequent visits in production, and gives
-// it offline capabilities. However, it also means that developers (and users)
-// will only see deployed updates on subsequent visits to a page, after all the
-// existing tabs open on the page have been closed, since previously cached
-// resources are updated in the background.
-
-// To learn more about the benefits of this model and instructions on how to
-// opt-in, read http://bit.ly/CRA-PWA
-
-interface ServiceWorkerConfig {
- onUpdate: (registration: ServiceWorkerRegistration) => void;
- onSuccess: (registration: ServiceWorkerRegistration) => void;
-}
-const isLocalhost = Boolean(
- window.location.hostname === 'localhost' ||
- // [::1] is the IPv6 localhost address.
- window.location.hostname === '[::1]' ||
- // 127.0.0.1/8 is considered localhost for IPv4.
- window.location.hostname.match(
- /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
- )
-);
-
-export function register(config: ServiceWorkerConfig): void {
- if (
- import.meta.env.NODE_ENV === 'production' &&
- 'serviceWorker' in navigator
- ) {
- // The URL constructor is available in all browsers that support SW.
- const publicUrl = new URL(import.meta.env.BASE_URL, window.location.href);
- if (publicUrl.origin !== window.location.origin) {
- // Our service worker won't work if BASE_URL is on a different origin
- // from what our page is served on. This might happen if a CDN is used to
- // serve assets; see https://github.com/facebook/create-react-app/issues/2374
- return;
- }
-
- window.addEventListener('load', () => {
- const swUrl = `${import.meta.env.BASE_URL}/service-worker.js`;
-
- if (isLocalhost) {
- // This is running on localhost. Let's check if a service worker still exists or not.
- checkValidServiceWorker(swUrl, config);
-
- // Add some additional logging to localhost, pointing developers to the
- // service worker/PWA documentation.
- navigator.serviceWorker.ready.then(() => {
- console.log(
- 'This web app is being served cache-first by a service ' +
- 'worker. To learn more, visit http://bit.ly/CRA-PWA'
- );
- });
- } else {
- // Is not localhost. Just register service worker
- registerValidSW(swUrl, config);
- }
- });
- }
-}
-
-function registerValidSW(swUrl: string | URL, config: ServiceWorkerConfig) {
- navigator.serviceWorker
- .register(swUrl)
- .then(registration => {
- registration.onupdatefound = () => {
- const installingWorker = registration.installing;
- if (installingWorker == null) {
- return;
- }
- installingWorker.onstatechange = () => {
- if (installingWorker.state === 'installed') {
- if (navigator.serviceWorker.controller) {
- // At this point, the updated precached content has been fetched,
- // but the previous service worker will still serve the older
- // content until all client tabs are closed.
- console.log(
- 'New content is available and will be used when all ' +
- 'tabs for this page are closed. See http://bit.ly/CRA-PWA.'
- );
-
- // Execute callback
- if (config != null && config.onUpdate != null) {
- config.onUpdate(registration);
- }
- } else {
- // At this point, everything has been precached.
- // It's the perfect time to display a
- // "Content is cached for offline use." message.
- console.log('Content is cached for offline use.');
-
- // Execute callback
- if (config != null && config.onSuccess != null) {
- config.onSuccess(registration);
- }
- }
- }
- };
- };
- })
- .catch(error => {
- console.error('Error during service worker registration:', error);
- });
-}
-
-function checkValidServiceWorker(swUrl: string, config: ServiceWorkerConfig) {
- // Check if the service worker can be found. If it can't reload the page.
- fetch(swUrl)
- .then(response => {
- // Ensure service worker exists, and that we really are getting a JS file.
- const contentType = response.headers.get('content-type');
- if (
- response.status === 404 ||
- (contentType != null && contentType.indexOf('javascript') === -1)
- ) {
- // No service worker found. Probably a different app. Reload the page.
- navigator.serviceWorker.ready.then(registration => {
- registration.unregister().then(() => {
- window.location.reload();
- });
- });
- } else {
- // Service worker found. Proceed as normal.
- registerValidSW(swUrl, config);
- }
- })
- .catch(() => {
- console.log(
- 'No internet connection found. App is running in offline mode.'
- );
- });
-}
-
-export function unregister(): void {
- if ('serviceWorker' in navigator) {
- navigator.serviceWorker.getRegistrations().then(registrations => {
- registrations.forEach(reg => {
- if (!reg.scope.endsWith('/download/')) {
- reg.unregister();
- }
- });
- });
- }
-}
diff --git a/packages/code-studio/src/styleguide/StyleGuideRoot.tsx b/packages/code-studio/src/styleguide/StyleGuideRoot.tsx
index 63b30fa094..fb03a8c236 100644
--- a/packages/code-studio/src/styleguide/StyleGuideRoot.tsx
+++ b/packages/code-studio/src/styleguide/StyleGuideRoot.tsx
@@ -1,16 +1,14 @@
import React from 'react';
import { Provider } from 'react-redux';
-import 'fira';
+import { FontBootstrap } from '@deephaven/app-utils';
import '@deephaven/components/scss/BaseStyleSheet.scss';
import { MonacoUtils } from '@deephaven/console';
import { store } from '@deephaven/redux';
import MonacoWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import DownloadServiceWorkerUtils from '../DownloadServiceWorkerUtils';
-import { unregister } from '../serviceWorker';
import StyleGuideInit from './StyleGuideInit';
export function StyleGuideRoot() {
- unregister();
DownloadServiceWorkerUtils.registerOnLoaded();
MonacoUtils.init({ getWorker: () => new MonacoWorker() });
@@ -20,9 +18,11 @@ export function StyleGuideRoot() {
window['__react-beautiful-dnd-disable-dev-warnings'] = true;
return (
-
-
-
+
+
+
+
+
);
}
diff --git a/packages/code-studio/src/styleguide/index.tsx b/packages/code-studio/src/styleguide/index.tsx
index c60aed01dd..35474fc333 100644
--- a/packages/code-studio/src/styleguide/index.tsx
+++ b/packages/code-studio/src/styleguide/index.tsx
@@ -1,6 +1,5 @@
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom';
-import 'fira';
import '@deephaven/components/scss/BaseStyleSheet.scss';
import { LoadingOverlay } from '@deephaven/components';
import { ApiBootstrap } from '@deephaven/jsapi-bootstrap';
@@ -11,6 +10,12 @@ logInit();
// eslint-disable-next-line react-refresh/only-export-components
const StyleGuideRoot = React.lazy(() => import('./StyleGuideRoot'));
+// eslint-disable-next-line react-refresh/only-export-components
+const FontBootstrap = React.lazy(async () => {
+ const module = await import('@deephaven/app-utils');
+ return { default: module.FontBootstrap };
+});
+
ReactDOM.render(
}>
-
+
+
+
,
document.getElementById('root')
diff --git a/packages/code-studio/tsconfig.json b/packages/code-studio/tsconfig.json
index c531a71b4c..89eae0923e 100644
--- a/packages/code-studio/tsconfig.json
+++ b/packages/code-studio/tsconfig.json
@@ -9,65 +9,28 @@
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx"],
"exclude": ["node_modules", "src/**/*.test.*", "src/**/__mocks__/*"],
"references": [
- {
- "path": "../chart"
- },
- {
- "path": "../components"
- },
- {
- "path": "../console"
- },
- {
- "path": "../dashboard"
- },
- {
- "path": "../dashboard-core-plugins"
- },
- {
- "path": "../file-explorer"
- },
- {
- "path": "../golden-layout"
- },
- {
- "path": "../grid"
- },
- {
- "path": "../iris-grid"
- },
- {
- "path": "../jsapi-bootstrap"
- },
- {
- "path": "../jsapi-shim"
- },
- {
- "path": "../jsapi-types"
- },
- {
- "path": "../jsapi-utils"
- },
- {
- "path": "../log"
- },
- {
- "path": "../pouch-storage"
- },
- {
- "path": "../react-hooks"
- },
- {
- "path": "../redux"
- },
- {
- "path": "../storage"
- },
- {
- "path": "../utils"
- },
- {
- "path": "../filters"
- }
+ { "path": "../auth-plugins" },
+ { "path": "../chart" },
+ { "path": "../components" },
+ { "path": "../console" },
+ { "path": "../dashboard" },
+ { "path": "../dashboard-core-plugins" },
+ { "path": "../file-explorer" },
+ { "path": "../golden-layout" },
+ { "path": "../grid" },
+ { "path": "../iris-grid" },
+ { "path": "../jsapi-bootstrap" },
+ { "path": "../jsapi-components" },
+ { "path": "../jsapi-shim" },
+ { "path": "../jsapi-types" },
+ { "path": "../jsapi-utils" },
+ { "path": "../log" },
+ { "path": "../app-utils" },
+ { "path": "../pouch-storage" },
+ { "path": "../react-hooks" },
+ { "path": "../redux" },
+ { "path": "../storage" },
+ { "path": "../utils" },
+ { "path": "../filters" }
]
}
diff --git a/packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx b/packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx
index 777a193837..839ff8ef2b 100644
--- a/packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx
+++ b/packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx
@@ -3,8 +3,8 @@ import { render } from '@testing-library/react';
import { CommandHistoryStorage } from '@deephaven/console';
import type { Container } from '@deephaven/golden-layout';
import { IdeConnection, IdeSession } from '@deephaven/jsapi-types';
+import { SessionConfig, SessionWrapper } from '@deephaven/jsapi-utils';
import { ConsolePanel } from './ConsolePanel';
-import { SessionConfig, SessionWrapper } from '../redux';
const mockConsole = jest.fn(() => null);
jest.mock('@deephaven/console', () => ({
diff --git a/packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx b/packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx
index 26cc919a96..20ef8a75bf 100644
--- a/packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx
+++ b/packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx
@@ -12,6 +12,7 @@ import {
} from '@deephaven/console';
import { PanelEvent } from '@deephaven/dashboard';
import { IdeSession, VariableDefinition } from '@deephaven/jsapi-shim';
+import { SessionWrapper } from '@deephaven/jsapi-utils';
import Log from '@deephaven/log';
import {
getCommandHistoryStorage,
@@ -23,7 +24,7 @@ import { assertNotNull } from '@deephaven/utils';
import type { JSZipObject } from 'jszip';
import { ConsoleEvent } from '../events';
import Panel from './Panel';
-import { getDashboardSessionWrapper, SessionWrapper } from '../redux';
+import { getDashboardSessionWrapper } from '../redux';
import './ConsolePanel.scss';
const log = Log.module('ConsolePanel');
diff --git a/packages/dashboard-core-plugins/src/redux/actions.ts b/packages/dashboard-core-plugins/src/redux/actions.ts
index b2b7d0b2ff..0bbb220528 100644
--- a/packages/dashboard-core-plugins/src/redux/actions.ts
+++ b/packages/dashboard-core-plugins/src/redux/actions.ts
@@ -1,31 +1,15 @@
import deepEqual from 'deep-equal';
-import { updateDashboardData } from '@deephaven/dashboard';
import { ThunkAction } from 'redux-thunk';
+import { updateDashboardData } from '@deephaven/dashboard';
+import { SessionWrapper } from '@deephaven/jsapi-utils';
import { RootState } from '@deephaven/redux';
import { Action } from 'redux';
-import { IdeConnection, IdeSession } from '@deephaven/jsapi-shim';
+import { IdeConnection } from '@deephaven/jsapi-shim';
import { getLinksForDashboard } from './selectors';
import { FilterSet } from '../panels';
import { Link } from '../linker/LinkerUtils';
import { ColumnSelectionValidator } from '../linker/ColumnSelectionValidator';
-export interface SessionConfig {
- type: string;
- id: string;
-}
-
-export interface SessionDetails {
- workerName?: string;
- processInfoId?: string;
-}
-
-export interface SessionWrapper {
- session: IdeSession;
- connection: IdeConnection;
- config: SessionConfig;
- details?: SessionDetails;
-}
-
/**
* Set the connection for the dashboard specified
* @param id The ID of the dashboard to set the connection for
diff --git a/packages/dashboard-core-plugins/src/redux/selectors.ts b/packages/dashboard-core-plugins/src/redux/selectors.ts
index 6f24688760..712f457f49 100644
--- a/packages/dashboard-core-plugins/src/redux/selectors.ts
+++ b/packages/dashboard-core-plugins/src/redux/selectors.ts
@@ -1,11 +1,11 @@
import { getDashboardData } from '@deephaven/dashboard';
import { Column, IdeConnection, Table } from '@deephaven/jsapi-shim';
+import { SessionWrapper } from '@deephaven/jsapi-utils';
import { RootState } from '@deephaven/redux';
import { FilterChangeEvent } from '../FilterPlugin';
import { Link } from '../linker/LinkerUtils';
import { FilterSet } from '../panels';
import { ColumnSelectionValidator } from '../linker/ColumnSelectionValidator';
-import { SessionWrapper } from './actions';
const EMPTY_MAP = new Map();
diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json
index 268e60e84a..27da646b45 100644
--- a/packages/dashboard/package.json
+++ b/packages/dashboard/package.json
@@ -26,7 +26,6 @@
"@deephaven/golden-layout": "file:../golden-layout",
"@deephaven/log": "file:../log",
"@deephaven/react-hooks": "file:../react-hooks",
- "@deephaven/redux": "file:../redux",
"@deephaven/utils": "file:../utils",
"deep-equal": "^2.0.5",
"lodash.ismatch": "^4.1.1",
@@ -35,12 +34,14 @@
"shortid": "^2.2.16"
},
"peerDependencies": {
+ "@deephaven/redux": "file:../redux",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react-redux": "^7.2.4"
},
"devDependencies": {
"@deephaven/mocks": "file:../mocks",
+ "@deephaven/redux": "file:../redux",
"@deephaven/tsconfig": "file:../tsconfig",
"@types/lodash.ismatch": "^4.4.0"
},
diff --git a/packages/embed-chart/.env b/packages/embed-chart/.env
index bcdccf5a84..a99529c1f9 100644
--- a/packages/embed-chart/.env
+++ b/packages/embed-chart/.env
@@ -1,8 +1,8 @@
-# Location of the iris script and API server
-# Set this value to __mocks__ to use mock server instead
+# See the values in [code sudio](../code-studio/.env) for more details
VITE_CORE_API_URL=/jsapi
VITE_CORE_API_NAME=dh-core.js
VITE_BUILD_PATH=./build
VITE_LOG_LEVEL=2
VITE_FAVICON=./favicon-cc-app.svg
VITE_PROXY_URL=http://localhost:10000
+VITE_MODULE_PLUGINS_URL=/js-plugins
\ No newline at end of file
diff --git a/packages/embed-chart/package.json b/packages/embed-chart/package.json
index 66fcc93c66..1e26341fa3 100644
--- a/packages/embed-chart/package.json
+++ b/packages/embed-chart/package.json
@@ -16,6 +16,7 @@
"build"
],
"dependencies": {
+ "@deephaven/app-utils": "file:../app-utils",
"@deephaven/chart": "file:../chart",
"@deephaven/components": "file:../components",
"@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
diff --git a/packages/embed-chart/src/App.tsx b/packages/embed-chart/src/App.tsx
index 991cd8c17c..d3d9e6d1a7 100644
--- a/packages/embed-chart/src/App.tsx
+++ b/packages/embed-chart/src/App.tsx
@@ -4,6 +4,7 @@ import { ContextMenuRoot, LoadingOverlay } from '@deephaven/components'; // Use
import dh, { IdeConnection } from '@deephaven/jsapi-shim'; // Import the shim to use the JS API
import Log from '@deephaven/log';
import './App.scss'; // Styles for in this app
+import { useConnection } from '@deephaven/app-utils';
const log = Log.module('EmbedChart.App');
@@ -36,6 +37,7 @@ function App(): JSX.Element {
() => new URLSearchParams(window.location.search),
[]
);
+ const connection = useConnection();
useEffect(
function initializeApp() {
@@ -48,17 +50,6 @@ function App(): JSX.Element {
throw new Error('No name param provided');
}
- // Connect to the Web API server
- const baseUrl = new URL(
- import.meta.env.VITE_CORE_API_URL ?? '',
- `${window.location}`
- );
-
- const websocketUrl = `${baseUrl.protocol}//${baseUrl.host}`;
-
- log.debug(`Starting connection...`);
- const connection = new dh.IdeConnection(websocketUrl);
-
log.debug('Loading figure', name, '...');
// Load the figure up.
@@ -80,7 +71,7 @@ function App(): JSX.Element {
}
initApp();
},
- [searchParams]
+ [connection, searchParams]
);
const isLoaded = model != null;
diff --git a/packages/embed-chart/src/index.tsx b/packages/embed-chart/src/index.tsx
index d45f2ec217..1eed483623 100644
--- a/packages/embed-chart/src/index.tsx
+++ b/packages/embed-chart/src/index.tsx
@@ -1,9 +1,6 @@
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom';
-// Fira fonts are not necessary, but look the best
-import 'fira';
-
// Need to import the base style sheet for proper styling
// eslint-disable-next-line import/no-unresolved
import '@deephaven/components/scss/BaseStyleSheet.scss';
@@ -11,9 +8,16 @@ import { LoadingOverlay } from '@deephaven/components';
import { ApiBootstrap } from '@deephaven/jsapi-bootstrap';
import './index.scss';
+// Lazy load components for code splitting and also to avoid importing the jsapi-shim before API is bootstrapped.
// eslint-disable-next-line react-refresh/only-export-components
const App = React.lazy(() => import('./App'));
+// eslint-disable-next-line react-refresh/only-export-components
+const AppBootstrap = React.lazy(async () => {
+ const module = await import('@deephaven/app-utils');
+ return { default: module.AppBootstrap };
+});
+
ReactDOM.render(
}>
-
+
+
+
,
document.getElementById('root')
diff --git a/packages/embed-chart/tsconfig.json b/packages/embed-chart/tsconfig.json
index 1fc3a596fd..3a49fb4a9f 100644
--- a/packages/embed-chart/tsconfig.json
+++ b/packages/embed-chart/tsconfig.json
@@ -8,23 +8,12 @@
},
"include": ["src"],
"references": [
- {
- "path": "../chart"
- },
- {
- "path": "../components"
- },
- {
- "path": "../jsapi-bootstrap"
- },
- {
- "path": "../jsapi-shim"
- },
- {
- "path": "../jsapi-utils"
- },
- {
- "path": "../log"
- }
+ { "path": "../app-utils" },
+ { "path": "../chart" },
+ { "path": "../components" },
+ { "path": "../jsapi-bootstrap" },
+ { "path": "../jsapi-shim" },
+ { "path": "../jsapi-utils" },
+ { "path": "../log" }
]
}
diff --git a/packages/embed-chart/vite.config.ts b/packages/embed-chart/vite.config.ts
index 812906d593..b9d011faaf 100644
--- a/packages/embed-chart/vite.config.ts
+++ b/packages/embed-chart/vite.config.ts
@@ -42,7 +42,7 @@ export default defineConfig(({ mode }) => {
// Vite does not have a "any unknown fallback to proxy" like CRA
// It is possible to add one with a custom middleware though if this list grows
if (env.VITE_PROXY_URL) {
- [env.VITE_CORE_API_URL].forEach(p => {
+ [env.VITE_CORE_API_URL, env.VITE_MODULE_PLUGINS_URL].forEach(p => {
proxy[p] = {
target: env.VITE_PROXY_URL,
changeOrigin: true,
diff --git a/packages/embed-grid/.env b/packages/embed-grid/.env
index bcdccf5a84..a99529c1f9 100644
--- a/packages/embed-grid/.env
+++ b/packages/embed-grid/.env
@@ -1,8 +1,8 @@
-# Location of the iris script and API server
-# Set this value to __mocks__ to use mock server instead
+# See the values in [code sudio](../code-studio/.env) for more details
VITE_CORE_API_URL=/jsapi
VITE_CORE_API_NAME=dh-core.js
VITE_BUILD_PATH=./build
VITE_LOG_LEVEL=2
VITE_FAVICON=./favicon-cc-app.svg
VITE_PROXY_URL=http://localhost:10000
+VITE_MODULE_PLUGINS_URL=/js-plugins
\ No newline at end of file
diff --git a/packages/embed-grid/package.json b/packages/embed-grid/package.json
index f43c89980b..6eea6675c0 100644
--- a/packages/embed-grid/package.json
+++ b/packages/embed-grid/package.json
@@ -16,6 +16,7 @@
"build"
],
"dependencies": {
+ "@deephaven/app-utils": "file:../app-utils",
"@deephaven/components": "file:../components",
"@deephaven/iris-grid": "file:../iris-grid",
"@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
diff --git a/packages/embed-grid/src/App.tsx b/packages/embed-grid/src/App.tsx
index 0e634a22ff..24cad16d25 100644
--- a/packages/embed-grid/src/App.tsx
+++ b/packages/embed-grid/src/App.tsx
@@ -1,4 +1,5 @@
import React, { useEffect, useMemo, useState } from 'react';
+import { useConnection } from '@deephaven/app-utils';
import { ContextMenuRoot, LoadingOverlay } from '@deephaven/components'; // Use the loading spinner from the Deephaven components package
import {
InputFilter,
@@ -67,6 +68,7 @@ async function loadTable(
* See create-react-app docs for how to update these env vars: https://create-react-app.dev/docs/adding-custom-environment-variables/
*/
function App(): JSX.Element {
+ const connection = useConnection();
const [model, setModel] = useState();
const [error, setError] = useState();
const [inputFilters, setInputFilters] = useState();
@@ -85,34 +87,16 @@ function App(): JSX.Element {
try {
// Get the table name from the query param `name`.
const name = searchParams.get('name');
-
if (name == null) {
throw new Error('No name param provided');
}
-
- // Connect to the Web API server
- const baseUrl = new URL(
- import.meta.env.VITE_CORE_API_URL ?? '',
- `${window.location}`
- );
-
- const websocketUrl = `${baseUrl.protocol}//${baseUrl.host}`;
-
- log.debug(`Starting connection...`);
- const connection = new dh.IdeConnection(websocketUrl);
-
log.debug('Loading table', name, '...');
-
// Load the table up.
const table = await loadTable(connection, name);
-
// Create the `IrisGridModel` for use with the `IrisGrid` component
log.debug(`Creating model...`);
-
const newModel = await IrisGridModelFactory.makeModel(table);
-
setModel(newModel);
-
log.debug('Table successfully loaded!');
} catch (e: unknown) {
log.error('Unable to load table', e);
@@ -122,7 +106,7 @@ function App(): JSX.Element {
}
initApp();
},
- [searchParams]
+ [connection, searchParams]
);
useEffect(
diff --git a/packages/embed-grid/src/index.tsx b/packages/embed-grid/src/index.tsx
index d45f2ec217..1eed483623 100644
--- a/packages/embed-grid/src/index.tsx
+++ b/packages/embed-grid/src/index.tsx
@@ -1,9 +1,6 @@
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom';
-// Fira fonts are not necessary, but look the best
-import 'fira';
-
// Need to import the base style sheet for proper styling
// eslint-disable-next-line import/no-unresolved
import '@deephaven/components/scss/BaseStyleSheet.scss';
@@ -11,9 +8,16 @@ import { LoadingOverlay } from '@deephaven/components';
import { ApiBootstrap } from '@deephaven/jsapi-bootstrap';
import './index.scss';
+// Lazy load components for code splitting and also to avoid importing the jsapi-shim before API is bootstrapped.
// eslint-disable-next-line react-refresh/only-export-components
const App = React.lazy(() => import('./App'));
+// eslint-disable-next-line react-refresh/only-export-components
+const AppBootstrap = React.lazy(async () => {
+ const module = await import('@deephaven/app-utils');
+ return { default: module.AppBootstrap };
+});
+
ReactDOM.render(
}>
-
+
+
+
,
document.getElementById('root')
diff --git a/packages/embed-grid/tsconfig.json b/packages/embed-grid/tsconfig.json
index 867f3a6fa8..00b78da0e9 100644
--- a/packages/embed-grid/tsconfig.json
+++ b/packages/embed-grid/tsconfig.json
@@ -8,23 +8,12 @@
},
"include": ["src"],
"references": [
- {
- "path": "../components"
- },
- {
- "path": "../iris-grid"
- },
- {
- "path": "../jsapi-bootstrap"
- },
- {
- "path": "../jsapi-shim"
- },
- {
- "path": "../jsapi-utils"
- },
- {
- "path": "../log"
- }
+ { "path": "../app-utils" },
+ { "path": "../components" },
+ { "path": "../iris-grid" },
+ { "path": "../jsapi-bootstrap" },
+ { "path": "../jsapi-shim" },
+ { "path": "../jsapi-utils" },
+ { "path": "../log" }
]
}
diff --git a/packages/embed-grid/vite.config.ts b/packages/embed-grid/vite.config.ts
index 221c56e26c..02ff2355da 100644
--- a/packages/embed-grid/vite.config.ts
+++ b/packages/embed-grid/vite.config.ts
@@ -42,7 +42,7 @@ export default defineConfig(({ mode }) => {
// Vite does not have a "any unknown fallback to proxy" like CRA
// It is possible to add one with a custom middleware though if this list grows
if (env.VITE_PROXY_URL) {
- [env.VITE_CORE_API_URL].forEach(p => {
+ [env.VITE_CORE_API_URL, env.VITE_MODULE_PLUGINS_URL].forEach(p => {
proxy[p] = {
target: env.VITE_PROXY_URL,
changeOrigin: true,
diff --git a/packages/grid/package.json b/packages/grid/package.json
index 9c3a65ab36..f56871fb0c 100644
--- a/packages/grid/package.json
+++ b/packages/grid/package.json
@@ -22,7 +22,7 @@
"build:sass": "sass --embed-sources --load-path=../../node_modules ./src:./dist"
},
"peerDependencies": {
- "react": "^17.0.0"
+ "react": "^17.x"
},
"devDependencies": {
"@deephaven/tsconfig": "file:../tsconfig"
diff --git a/packages/jsapi-bootstrap/package.json b/packages/jsapi-bootstrap/package.json
index 84040d12cd..a707cf2c57 100644
--- a/packages/jsapi-bootstrap/package.json
+++ b/packages/jsapi-bootstrap/package.json
@@ -24,10 +24,12 @@
"dependencies": {
"@deephaven/components": "file:../components",
"@deephaven/jsapi-types": "file:../jsapi-types",
+ "@deephaven/jsapi-utils": "file:../jsapi-utils",
"@deephaven/log": "file:../log"
},
"devDependencies": {
- "@deephaven/tsconfig": "file:../tsconfig"
+ "@deephaven/tsconfig": "file:../tsconfig",
+ "react": "^17.x"
},
"peerDependencies": {
"react": "^17.x"
diff --git a/packages/jsapi-bootstrap/src/ApiBootstrap.tsx b/packages/jsapi-bootstrap/src/ApiBootstrap.tsx
index 39ce7ae1be..665d90bb33 100644
--- a/packages/jsapi-bootstrap/src/ApiBootstrap.tsx
+++ b/packages/jsapi-bootstrap/src/ApiBootstrap.tsx
@@ -8,17 +8,27 @@ import {
import type { dh as DhType } from '@deephaven/jsapi-types';
import Log from '@deephaven/log';
-const log = Log.module('@deephaven/code-studio');
+const log = Log.module('@deephaven/jsapi-bootstrap.ApiBootstrap');
export const ApiContext = createContext(null);
export type ApiBootstrapProps = {
+ /** URL of the API to load */
apiUrl: string;
+
+ /** Children to render when the API has loaded */
children: JSX.Element;
+
+ /** Element to display if there is a failure loading the API */
failureElement?: JSX.Element;
+
+ /** Whether to set the API globally on window.dh when it has loaded */
setGlobally?: boolean;
};
+/**
+ * ApiBootstrap loads the API from the provided URL, rendering the children once loaded.
+ */
export function ApiBootstrap({
apiUrl,
children,
diff --git a/packages/jsapi-bootstrap/tsconfig.json b/packages/jsapi-bootstrap/tsconfig.json
index 20bd393c00..5989c85509 100644
--- a/packages/jsapi-bootstrap/tsconfig.json
+++ b/packages/jsapi-bootstrap/tsconfig.json
@@ -7,17 +7,9 @@
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx"],
"exclude": ["node_modules", "src/**/*.test.*", "src/**/__mocks__/*"],
"references": [
- {
- "path": "../components"
- },
- {
- "path": "../jsapi-shim"
- },
- {
- "path": "../jsapi-utils"
- },
- {
- "path": "../log"
- }
+ { "path": "../components" },
+ { "path": "../jsapi-shim" },
+ { "path": "../jsapi-utils" },
+ { "path": "../log" }
]
}
diff --git a/packages/jsapi-components/src/TableInput.tsx b/packages/jsapi-components/src/TableInput.tsx
index 8dd30cb5f0..b739c97dbc 100644
--- a/packages/jsapi-components/src/TableInput.tsx
+++ b/packages/jsapi-components/src/TableInput.tsx
@@ -11,7 +11,7 @@ import {
SearchInput,
SelectValueList,
} from '@deephaven/components';
-import type { LongWrapper, Table } from '@deephaven/jsapi-shim';
+import type { LongWrapper, Table } from '@deephaven/jsapi-types';
import { PromiseUtils } from '@deephaven/utils';
import Log from '@deephaven/log';
import { Formatter, FormatterUtils, Settings } from '@deephaven/jsapi-utils';
diff --git a/packages/jsapi-components/src/useTable.ts b/packages/jsapi-components/src/useTable.ts
index 4cd9212ee9..3d1ec9b353 100644
--- a/packages/jsapi-components/src/useTable.ts
+++ b/packages/jsapi-components/src/useTable.ts
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
-import { Column, Row, Table } from '@deephaven/jsapi-shim';
+import dh from '@deephaven/jsapi-shim';
+import { Column, Row, Table } from '@deephaven/jsapi-types';
import Log from '@deephaven/log';
import useTableListener from './useTableListener';
import ColumnNameError from './ColumnNameError';
diff --git a/packages/jsapi-components/src/useTableColumn.ts b/packages/jsapi-components/src/useTableColumn.ts
index 5cfd68f181..d720191b31 100644
--- a/packages/jsapi-components/src/useTableColumn.ts
+++ b/packages/jsapi-components/src/useTableColumn.ts
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
-import { Column, Table } from '@deephaven/jsapi-shim';
+import { Column, Table } from '@deephaven/jsapi-types';
import useTable from './useTable';
/**
diff --git a/packages/jsapi-components/src/useTableListener.ts b/packages/jsapi-components/src/useTableListener.ts
index 70f4f8e86a..7e245c9a47 100644
--- a/packages/jsapi-components/src/useTableListener.ts
+++ b/packages/jsapi-components/src/useTableListener.ts
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
-import { Evented, EventListener } from '@deephaven/jsapi-shim';
+import { Evented, EventListener } from '@deephaven/jsapi-types';
import Log from '@deephaven/log';
const log = Log.module('useTableListener');
diff --git a/packages/jsapi-components/tsconfig.json b/packages/jsapi-components/tsconfig.json
index 20bd393c00..a4b5915642 100644
--- a/packages/jsapi-components/tsconfig.json
+++ b/packages/jsapi-components/tsconfig.json
@@ -7,17 +7,11 @@
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx"],
"exclude": ["node_modules", "src/**/*.test.*", "src/**/__mocks__/*"],
"references": [
- {
- "path": "../components"
- },
- {
- "path": "../jsapi-shim"
- },
- {
- "path": "../jsapi-utils"
- },
- {
- "path": "../log"
- }
+ { "path": "../components" },
+ { "path": "../jsapi-bootstrap" },
+ { "path": "../jsapi-shim" },
+ { "path": "../jsapi-types" },
+ { "path": "../jsapi-utils" },
+ { "path": "../log" }
]
}
diff --git a/packages/jsapi-types/src/dh.types.ts b/packages/jsapi-types/src/dh.types.ts
index 02070d9e6d..f1e8015fa6 100644
--- a/packages/jsapi-types/src/dh.types.ts
+++ b/packages/jsapi-types/src/dh.types.ts
@@ -1084,5 +1084,7 @@ export interface CoreClient extends CoreClientContructor {
login(options: LoginOptions): Promise;
getAsIdeConnection(): Promise;
getStorageService(): StorageService;
- getServerConfigValues(): [string, string][];
+ getServerConfigValues(): Promise<[string, string][]>;
+ getAuthConfigValues(): Promise<[string, string][]>;
+ disconnect(): void;
}
diff --git a/packages/jsapi-utils/package.json b/packages/jsapi-utils/package.json
index 459a0c6abd..be972abddf 100644
--- a/packages/jsapi-utils/package.json
+++ b/packages/jsapi-utils/package.json
@@ -23,6 +23,7 @@
"dependencies": {
"@deephaven/filters": "file:../filters",
"@deephaven/jsapi-shim": "file:../jsapi-shim",
+ "@deephaven/jsapi-types": "file:../jsapi-types",
"@deephaven/log": "file:../log",
"@deephaven/utils": "file:../utils",
"lodash.clamp": "^4.0.3",
diff --git a/packages/jsapi-utils/src/ConnectionUtils.ts b/packages/jsapi-utils/src/ConnectionUtils.ts
index 4138298b5f..92a0571a7c 100644
--- a/packages/jsapi-utils/src/ConnectionUtils.ts
+++ b/packages/jsapi-utils/src/ConnectionUtils.ts
@@ -2,7 +2,7 @@ import {
IdeConnection,
VariableChanges,
VariableDefinition,
-} from '@deephaven/jsapi-shim';
+} from '@deephaven/jsapi-types';
import { TimeoutError } from '@deephaven/utils';
/** Default timeout for fetching a variable definition */
diff --git a/packages/jsapi-utils/src/DateUtils.ts b/packages/jsapi-utils/src/DateUtils.ts
index 3ec45fa140..b4f9c31c9d 100644
--- a/packages/jsapi-utils/src/DateUtils.ts
+++ b/packages/jsapi-utils/src/DateUtils.ts
@@ -1,5 +1,5 @@
import dh from '@deephaven/jsapi-shim';
-import type { DateWrapper } from '@deephaven/jsapi-shim';
+import type { DateWrapper } from '@deephaven/jsapi-types';
interface DateParts {
year: T;
diff --git a/packages/jsapi-utils/src/MessageUtils.ts b/packages/jsapi-utils/src/MessageUtils.ts
index ff941ac4dc..8fc9e1b7aa 100644
--- a/packages/jsapi-utils/src/MessageUtils.ts
+++ b/packages/jsapi-utils/src/MessageUtils.ts
@@ -60,7 +60,7 @@ export async function requestParentResponse(
throw new Error('window.opener is null, unable to send request.');
}
return new Promise((resolve, reject) => {
- let timeoutId: NodeJS.Timeout;
+ let timeoutId: number;
const id = shortid();
const listener = (event: MessageEvent>) => {
const { data } = event;
@@ -69,12 +69,12 @@ export async function requestParentResponse(
log.debug("Ignore message, id doesn't match", data);
return;
}
- clearTimeout(timeoutId);
+ window.clearTimeout(timeoutId);
window.removeEventListener('message', listener);
resolve(data.payload);
};
window.addEventListener('message', listener);
- timeoutId = setTimeout(() => {
+ timeoutId = window.setTimeout(() => {
window.removeEventListener('message', listener);
reject(new TimeoutError('Request timed out'));
}, timeout);
diff --git a/packages/code-studio/src/main/NoConsolesError.ts b/packages/jsapi-utils/src/NoConsolesError.ts
similarity index 100%
rename from packages/code-studio/src/main/NoConsolesError.ts
rename to packages/jsapi-utils/src/NoConsolesError.ts
diff --git a/packages/jsapi-utils/src/SessionUtils.ts b/packages/jsapi-utils/src/SessionUtils.ts
new file mode 100644
index 0000000000..f7db6bcf93
--- /dev/null
+++ b/packages/jsapi-utils/src/SessionUtils.ts
@@ -0,0 +1,115 @@
+import {
+ ConnectOptions,
+ CoreClient,
+ IdeConnection,
+ IdeSession,
+} from '@deephaven/jsapi-types';
+import {
+ requestParentResponse,
+ SESSION_DETAILS_REQUEST,
+} from '@deephaven/jsapi-utils';
+import Log from '@deephaven/log';
+import shortid from 'shortid';
+import NoConsolesError, { isNoConsolesError } from './NoConsolesError';
+
+const log = Log.module('SessionUtils');
+
+export interface SessionConfig {
+ type: string;
+ id: string;
+}
+
+export interface SessionDetails {
+ workerName?: string;
+ processInfoId?: string;
+}
+
+export interface SessionWrapper {
+ session: IdeSession;
+ connection: IdeConnection;
+ config: SessionConfig;
+ details?: SessionDetails;
+}
+
+/**
+ * @returns New connection to the server
+ */
+export function createConnection(websocketUrl: string): IdeConnection {
+ log.info(`Starting connection to '${websocketUrl}'...`);
+
+ return new dh.IdeConnection(websocketUrl);
+}
+
+/**
+ * Create a new session using the default URL
+ * @returns A session and config that is ready to use
+ */
+export async function createSessionWrapper(
+ connection: IdeConnection,
+ details: SessionDetails
+): Promise {
+ log.info('Getting console types...');
+
+ const types = await connection.getConsoleTypes();
+
+ if (types.length === 0) {
+ throw new NoConsolesError('No console types available');
+ }
+
+ log.info('Available types:', types);
+
+ const type = types[0];
+
+ log.info('Starting session with type', type);
+
+ const session = await connection.startSession(type);
+
+ const config = { type, id: shortid.generate() };
+
+ log.info('Console session established', config);
+
+ return {
+ session,
+ config,
+ connection,
+ details,
+ };
+}
+
+export function createCoreClient(
+ websocketUrl: string,
+ options?: ConnectOptions
+): CoreClient {
+ log.info('createCoreClient', websocketUrl);
+
+ return new dh.CoreClient(websocketUrl, options);
+}
+
+async function requestParentSessionDetails(): Promise {
+ return requestParentResponse(SESSION_DETAILS_REQUEST);
+}
+
+export async function getSessionDetails(): Promise {
+ const searchParams = new URLSearchParams(window.location.search);
+ switch (searchParams.get('authProvider')) {
+ case 'parent':
+ return requestParentSessionDetails();
+ }
+ return {};
+}
+
+export async function loadSessionWrapper(
+ connection: IdeConnection,
+ sessionDetails: SessionDetails
+): Promise {
+ let sessionWrapper: SessionWrapper | undefined;
+ try {
+ sessionWrapper = await createSessionWrapper(connection, sessionDetails);
+ } catch (e) {
+ // Consoles may be disabled on the server, but we should still be able to start up and open existing objects
+ if (!isNoConsolesError(e)) {
+ throw e;
+ }
+ }
+ return sessionWrapper;
+}
diff --git a/packages/jsapi-utils/src/TableUtils.ts b/packages/jsapi-utils/src/TableUtils.ts
index 5e05632ad1..417df3370f 100644
--- a/packages/jsapi-utils/src/TableUtils.ts
+++ b/packages/jsapi-utils/src/TableUtils.ts
@@ -5,7 +5,8 @@ import {
OperatorValue as FilterOperatorValue,
} from '@deephaven/filters';
import Log from '@deephaven/log';
-import dh, {
+import dh from '@deephaven/jsapi-shim';
+import {
Column,
FilterCondition,
FilterValue,
@@ -14,7 +15,7 @@ import dh, {
Sort,
Table,
TreeTable,
-} from '@deephaven/jsapi-shim';
+} from '@deephaven/jsapi-types';
import {
CancelablePromise,
PromiseUtils,
diff --git a/packages/jsapi-utils/src/formatters/DateTimeColumnFormatter.ts b/packages/jsapi-utils/src/formatters/DateTimeColumnFormatter.ts
index 0d956da3aa..9b21f430ba 100644
--- a/packages/jsapi-utils/src/formatters/DateTimeColumnFormatter.ts
+++ b/packages/jsapi-utils/src/formatters/DateTimeColumnFormatter.ts
@@ -1,5 +1,6 @@
/* eslint class-methods-use-this: "off" */
-import dh, { DateWrapper, TimeZone } from '@deephaven/jsapi-shim';
+import dh from '@deephaven/jsapi-shim';
+import type { DateWrapper, TimeZone } from '@deephaven/jsapi-types';
import Log from '@deephaven/log';
import TableColumnFormatter, {
TableColumnFormat,
diff --git a/packages/jsapi-utils/src/index.ts b/packages/jsapi-utils/src/index.ts
index 798c8b9674..67e1d33b41 100644
--- a/packages/jsapi-utils/src/index.ts
+++ b/packages/jsapi-utils/src/index.ts
@@ -5,6 +5,8 @@ export * from './Formatter';
export { default as FormatterUtils } from './FormatterUtils';
export * from './FormatterUtils';
export * from './MessageUtils';
+export * from './NoConsolesError';
+export * from './SessionUtils';
export * from './Settings';
export * from './TableUtils';
export * from './ViewportDataUtils';
diff --git a/packages/jsapi-utils/tsconfig.json b/packages/jsapi-utils/tsconfig.json
index 0f5b5a9173..b49a766622 100644
--- a/packages/jsapi-utils/tsconfig.json
+++ b/packages/jsapi-utils/tsconfig.json
@@ -13,17 +13,10 @@
],
"exclude": ["node_modules", "src/**/*.test.*", "src/**/__mocks__/*"],
"references": [
- {
- "path": "../filters"
- },
- {
- "path": "../jsapi-shim"
- },
- {
- "path": "../log"
- },
- {
- "path": "../utils"
- }
+ { "path": "../filters" },
+ { "path": "../jsapi-shim" },
+ { "path": "../jsapi-types" },
+ { "path": "../log" },
+ { "path": "../utils" }
]
}
diff --git a/packages/pouch-storage/package.json b/packages/pouch-storage/package.json
index 549276a72a..2b429d034b 100644
--- a/packages/pouch-storage/package.json
+++ b/packages/pouch-storage/package.json
@@ -31,7 +31,7 @@
"pouchdb-find": "^7.3.0"
},
"peerDependencies": {
- "react": "^17.0.0"
+ "react": "^17.x"
},
"devDependencies": {
"@deephaven/tsconfig": "file:../tsconfig"
diff --git a/packages/react-hooks/package.json b/packages/react-hooks/package.json
index 0c0dc6864b..1aa210aa4e 100644
--- a/packages/react-hooks/package.json
+++ b/packages/react-hooks/package.json
@@ -25,7 +25,7 @@
"@deephaven/utils": "file:../utils"
},
"peerDependencies": {
- "react": "^17.0.0"
+ "react": "^17.x"
},
"devDependencies": {
"@deephaven/tsconfig": "file:../tsconfig"
diff --git a/packages/redux/tsconfig.json b/packages/redux/tsconfig.json
index fefd4491a1..f1f3671a39 100644
--- a/packages/redux/tsconfig.json
+++ b/packages/redux/tsconfig.json
@@ -7,20 +7,10 @@
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx"],
"exclude": ["node_modules", "src/**/*.test.*", "src/**/__mocks__/*"],
"references": [
- {
- "path": "../components"
- },
- {
- "path": "../console"
- },
- {
- "path": "../file-explorer"
- },
- {
- "path": "../jsapi-utils"
- },
- {
- "path": "../log"
- }
+ { "path": "../components" },
+ { "path": "../console" },
+ { "path": "../file-explorer" },
+ { "path": "../jsapi-utils" },
+ { "path": "../log" }
]
}
diff --git a/packages/storage/package.json b/packages/storage/package.json
index 644f60d059..52dd9f2138 100644
--- a/packages/storage/package.json
+++ b/packages/storage/package.json
@@ -26,7 +26,7 @@
"lodash.throttle": "^4.1.1"
},
"peerDependencies": {
- "react": "^17.0.0"
+ "react": "^17.x"
},
"devDependencies": {
"@deephaven/tsconfig": "file:../tsconfig"