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"