mirror of
				https://github.com/transformerlab/transformerlab-app.git
				synced 2025-04-14 07:48:20 +03:00 
			
		
		
		
	Merge pull request #322 from transformerlab/demo/webapp
Add start:cloud target to serve TransformerLab on web
This commit is contained in:
		
							
								
								
									
										215
									
								
								.erb/configs/webpack.config.cloud.dev.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								.erb/configs/webpack.config.cloud.dev.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,215 @@ | ||||
| import 'webpack-dev-server'; | ||||
| import path from 'path'; | ||||
| import fs from 'fs'; | ||||
| import webpack from 'webpack'; | ||||
| import HtmlWebpackPlugin from 'html-webpack-plugin'; | ||||
| import chalk from 'chalk'; | ||||
| import { merge } from 'webpack-merge'; | ||||
| import { execSync, spawn } from 'child_process'; | ||||
| import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; | ||||
| import baseConfig from './webpack.config.base'; | ||||
| import webpackPaths from './webpack.paths'; | ||||
| import checkNodeEnv from '../scripts/check-node-env'; | ||||
|  | ||||
| // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's | ||||
| // at the dev webpack config is not accidentally run in a production environment | ||||
| if (process.env.NODE_ENV === 'production') { | ||||
|   checkNodeEnv('development'); | ||||
| } | ||||
|  | ||||
| const port = process.env.PORT || 1212; | ||||
| const manifest = path.resolve(webpackPaths.dllPath, 'renderer.json'); | ||||
| const skipDLLs = | ||||
|   module.parent?.filename.includes('webpack.config.renderer.dev.dll') || | ||||
|   module.parent?.filename.includes('webpack.config.eslint'); | ||||
|  | ||||
| /** | ||||
|  * Warn if the DLL is not built | ||||
|  */ | ||||
| if ( | ||||
|   !skipDLLs && | ||||
|   !(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest)) | ||||
| ) { | ||||
|   console.log( | ||||
|     chalk.black.bgYellow.bold( | ||||
|       'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"', | ||||
|     ), | ||||
|   ); | ||||
|   execSync('npm run postinstall'); | ||||
| } | ||||
|  | ||||
| const configuration: webpack.Configuration = { | ||||
|   devtool: 'inline-source-map', | ||||
|  | ||||
|   mode: 'development', | ||||
|  | ||||
|   target: ['web', 'electron-renderer'], | ||||
|  | ||||
|   entry: [ | ||||
|     `webpack-dev-server/client?http://localhost:${port}/dist`, | ||||
|     'webpack/hot/only-dev-server', | ||||
|     path.join(webpackPaths.srcMainPath, 'preload-cloud.ts'), | ||||
|     path.join(webpackPaths.srcRendererPath, 'index.tsx'), | ||||
|   ], | ||||
|  | ||||
|   output: { | ||||
|     path: webpackPaths.distRendererPath, | ||||
|     publicPath: '/', | ||||
|     filename: 'renderer.dev.js', | ||||
|     library: { | ||||
|       type: 'umd', | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   module: { | ||||
|     rules: [ | ||||
|       { | ||||
|         test: /\.s?(c|a)ss$/, | ||||
|         use: [ | ||||
|           'style-loader', | ||||
|           { | ||||
|             loader: 'css-loader', | ||||
|             options: { | ||||
|               modules: true, | ||||
|               sourceMap: true, | ||||
|               importLoaders: 1, | ||||
|             }, | ||||
|           }, | ||||
|           'sass-loader', | ||||
|         ], | ||||
|         include: /\.module\.s?(c|a)ss$/, | ||||
|       }, | ||||
|       { | ||||
|         test: /\.s?css$/, | ||||
|         use: ['style-loader', 'css-loader', 'sass-loader'], | ||||
|         exclude: /\.module\.s?(c|a)ss$/, | ||||
|       }, | ||||
|       // Fonts | ||||
|       { | ||||
|         test: /\.(woff|woff2|eot|ttf|otf)$/i, | ||||
|         type: 'asset/resource', | ||||
|       }, | ||||
|       // Images | ||||
|       { | ||||
|         test: /\.(png|jpg|jpeg|gif)$/i, | ||||
|         type: 'asset/resource', | ||||
|       }, | ||||
|       // SVG | ||||
|       { | ||||
|         test: /\.svg$/, | ||||
|         use: [ | ||||
|           { | ||||
|             loader: '@svgr/webpack', | ||||
|             options: { | ||||
|               prettier: false, | ||||
|               svgo: false, | ||||
|               svgoConfig: { | ||||
|                 plugins: [{ removeViewBox: false }], | ||||
|               }, | ||||
|               titleProp: true, | ||||
|               ref: true, | ||||
|             }, | ||||
|           }, | ||||
|           'file-loader', | ||||
|         ], | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   plugins: [ | ||||
|     ...(skipDLLs | ||||
|       ? [] | ||||
|       : [ | ||||
|           new webpack.DllReferencePlugin({ | ||||
|             context: webpackPaths.dllPath, | ||||
|             manifest: require(manifest), | ||||
|             sourceType: 'var', | ||||
|           }), | ||||
|         ]), | ||||
|  | ||||
|     new webpack.NoEmitOnErrorsPlugin(), | ||||
|  | ||||
|     /** | ||||
|      * Create global constants which can be configured at compile time. | ||||
|      * | ||||
|      * Useful for allowing different behaviour between development builds and | ||||
|      * release builds | ||||
|      * | ||||
|      * NODE_ENV should be production so that modules do not perform certain | ||||
|      * development checks | ||||
|      * | ||||
|      * By default, use 'development' as NODE_ENV. This can be overriden with | ||||
|      * 'staging', for example, by changing the ENV variables in the npm scripts | ||||
|      */ | ||||
|     new webpack.EnvironmentPlugin({ | ||||
|       NODE_ENV: 'development', | ||||
|     }), | ||||
|  | ||||
|     new webpack.LoaderOptionsPlugin({ | ||||
|       debug: true, | ||||
|     }), | ||||
|  | ||||
|     new ReactRefreshWebpackPlugin(), | ||||
|  | ||||
|     new HtmlWebpackPlugin({ | ||||
|       filename: path.join('index.html'), | ||||
|       template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), | ||||
|       minify: { | ||||
|         collapseWhitespace: true, | ||||
|         removeAttributeQuotes: true, | ||||
|         removeComments: true, | ||||
|       }, | ||||
|       isBrowser: false, | ||||
|       env: process.env.NODE_ENV, | ||||
|       isDevelopment: process.env.NODE_ENV !== 'production', | ||||
|       nodeModules: webpackPaths.appNodeModulesPath, | ||||
|     }), | ||||
|   ], | ||||
|  | ||||
|   node: { | ||||
|     __dirname: false, | ||||
|     __filename: false, | ||||
|   }, | ||||
|  | ||||
|   devServer: { | ||||
|     port, | ||||
|     compress: true, | ||||
|     hot: true, | ||||
|     headers: { 'Access-Control-Allow-Origin': '*' }, | ||||
|     static: { | ||||
|       publicPath: '/', | ||||
|     }, | ||||
|     historyApiFallback: { | ||||
|       verbose: true, | ||||
|     }, | ||||
|     setupMiddlewares(middlewares) { | ||||
|       console.log('Starting preload-cloud.js builder...'); | ||||
|       console.log(path.join(webpackPaths.srcMainPath, 'preload-cloud.ts')); | ||||
|       const preloadProcess = spawn('npm', ['run', 'start:preload-cloud'], { | ||||
|         shell: true, | ||||
|         stdio: 'inherit', | ||||
|       }) | ||||
|         .on('close', (code: number) => process.exit(code!)) | ||||
|         .on('error', (spawnError) => console.error(spawnError)); | ||||
|  | ||||
|       // console.log('Starting Main Process...'); | ||||
|       // let args = ['run', 'start:main']; | ||||
|       // if (process.env.MAIN_ARGS) { | ||||
|       //   args = args.concat( | ||||
|       //     ['--', ...process.env.MAIN_ARGS.matchAll(/"[^"]+"|[^\s"]+/g)].flat(), | ||||
|       //   ); | ||||
|       // } | ||||
|       // spawn('npm', args, { | ||||
|       //   shell: true, | ||||
|       //   stdio: 'inherit', | ||||
|       // }) | ||||
|       //   .on('close', (code: number) => { | ||||
|       //     preloadProcess.kill(); | ||||
|       //     process.exit(code!); | ||||
|       //   }) | ||||
|       //   .on('error', (spawnError) => console.error(spawnError)); | ||||
|       return middlewares; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export default merge(baseConfig, configuration); | ||||
							
								
								
									
										71
									
								
								.erb/configs/webpack.config.preload-cloud.dev.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								.erb/configs/webpack.config.preload-cloud.dev.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| import path from 'path'; | ||||
| import webpack from 'webpack'; | ||||
| import { merge } from 'webpack-merge'; | ||||
| import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; | ||||
| import baseConfig from './webpack.config.base'; | ||||
| import webpackPaths from './webpack.paths'; | ||||
| import checkNodeEnv from '../scripts/check-node-env'; | ||||
|  | ||||
| // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's | ||||
| // at the dev webpack config is not accidentally run in a production environment | ||||
| if (process.env.NODE_ENV === 'production') { | ||||
|   checkNodeEnv('development'); | ||||
| } | ||||
|  | ||||
| const configuration: webpack.Configuration = { | ||||
|   devtool: 'inline-source-map', | ||||
|  | ||||
|   mode: 'development', | ||||
|  | ||||
|   target: 'electron-preload', | ||||
|  | ||||
|   entry: path.join(webpackPaths.srcMainPath, 'preload-cloud.ts'), | ||||
|  | ||||
|   output: { | ||||
|     path: webpackPaths.dllPath, | ||||
|     filename: 'preload.js', | ||||
|     library: { | ||||
|       type: 'umd', | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   plugins: [ | ||||
|     new BundleAnalyzerPlugin({ | ||||
|       analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', | ||||
|     }), | ||||
|  | ||||
|     /** | ||||
|      * Create global constants which can be configured at compile time. | ||||
|      * | ||||
|      * Useful for allowing different behaviour between development builds and | ||||
|      * release builds | ||||
|      * | ||||
|      * NODE_ENV should be production so that modules do not perform certain | ||||
|      * development checks | ||||
|      * | ||||
|      * By default, use 'development' as NODE_ENV. This can be overriden with | ||||
|      * 'staging', for example, by changing the ENV variables in the npm scripts | ||||
|      */ | ||||
|     new webpack.EnvironmentPlugin({ | ||||
|       NODE_ENV: 'development', | ||||
|     }), | ||||
|  | ||||
|     new webpack.LoaderOptionsPlugin({ | ||||
|       debug: true, | ||||
|     }), | ||||
|   ], | ||||
|  | ||||
|   /** | ||||
|    * Disables webpack processing of __dirname and __filename. | ||||
|    * If you run the bundle in node.js it falls back to these values of node.js. | ||||
|    * https://github.com/webpack/webpack/issues/2010 | ||||
|    */ | ||||
|   node: { | ||||
|     __dirname: false, | ||||
|     __filename: false, | ||||
|   }, | ||||
|  | ||||
|   watch: true, | ||||
| }; | ||||
|  | ||||
| export default merge(baseConfig, configuration); | ||||
| @@ -4,11 +4,20 @@ import detectPort from 'detect-port'; | ||||
| const port = process.env.PORT || '1212'; | ||||
|  | ||||
| detectPort(port, (err, availablePort) => { | ||||
|   // This is a hacked in place to check the version of Node but it works to prevent users from using Node 23 and above | ||||
|   // because electron build breaks on Node 23. Remove this once electron is fixed. | ||||
|   if (process.versions.node.split('.')[0] >= 23) { | ||||
|     console.error( | ||||
|       `Node.js version 23 and above are not supported. Current version: ${process.version}`, | ||||
|     ); | ||||
|     process.exit(1); | ||||
|   } | ||||
|  | ||||
|   if (port !== String(availablePort)) { | ||||
|     throw new Error( | ||||
|       chalk.whiteBright.bgRed.bold( | ||||
|         `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start` | ||||
|       ) | ||||
|         `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start`, | ||||
|       ), | ||||
|     ); | ||||
|   } else { | ||||
|     process.exit(0); | ||||
|   | ||||
| @@ -38,7 +38,9 @@ | ||||
|     "start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run start:renderer", | ||||
|     "start:main": "cross-env NODE_ENV=development electronmon -r ts-node/register/transpile-only .", | ||||
|     "start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts", | ||||
|     "start:preload-cloud": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload-cloud.dev.ts", | ||||
|     "start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts", | ||||
|     "start:cloud": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.cloud.dev.ts", | ||||
|     "test": "jest" | ||||
|   }, | ||||
|   "browserslist": [], | ||||
|   | ||||
							
								
								
									
										146
									
								
								src/main/preload-cloud.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								src/main/preload-cloud.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | ||||
| // Disable no-unused-vars, broken for spread args | ||||
| /* eslint no-unused-vars: off */ | ||||
|  | ||||
| // webFrame.setZoomFactor(1); | ||||
|  | ||||
| console.log('CLOUD PRELOAD'); | ||||
|  | ||||
| // This function contextBridge.exposeInMainWorld is a custom function that takes the provided argument | ||||
| // adds it to the window object, and makes it available to the renderer process: | ||||
| function exposeInMainWorld(key: string, value: unknown) { | ||||
|   window[key] = value; | ||||
| } | ||||
|  | ||||
| const contextBridge = {} as any; | ||||
| contextBridge.exposeInMainWorld = exposeInMainWorld; | ||||
|  | ||||
| // Now make a stub ipcRenderer object that will fake the real ipcRenderer object: | ||||
| const ipcRenderer = { | ||||
|   send: async (_channel: string, ..._args: unknown[]) => { | ||||
|     return new Promise((resolve) => { | ||||
|       setTimeout(() => { | ||||
|         console.log(`Message sent to ${_channel} with args:`, _args); | ||||
|         resolve(); | ||||
|       }, 1); // Simulate async operation with 1 ms delay | ||||
|     }); | ||||
|   }, | ||||
|   on: (_channel: string, _func: (...args: unknown[]) => void) => { }, | ||||
|   once: (_channel: string, _func: (...args: unknown[]) => void) => { }, | ||||
|   invoke: async (_channel: string, ..._args: unknown[]) => { | ||||
|     console.log(`Invoking ${_channel} with args:`, _args); | ||||
|     return new Promise((resolve) => { | ||||
|       setTimeout(() => { | ||||
|         resolve(`Response from ${_channel}`); | ||||
|       }, 1); // Simulate async operation with 1 ms delay | ||||
|     }); | ||||
|   }, | ||||
|   removeAllListeners: (_channel: string) => { }, | ||||
| }; | ||||
|  | ||||
| // write to the browser window to break the HTML: | ||||
| // export type Channels = | ||||
| //   | 'getStoreValue' | ||||
| //   | 'setStoreValue' | ||||
| //   | 'deleteStoreValue' | ||||
| //   | 'openURL' | ||||
| //   | 'server:checkSystemRequirements' | ||||
| //   | 'server:checkIfInstalledLocally' | ||||
| //   | 'server:checkLocalVersion' | ||||
| //   | 'server:startLocalServer' | ||||
| //   | 'server:InstallLocally' | ||||
| //   | 'server:install_conda' | ||||
| //   | 'server:install_create-conda-environment' | ||||
| //   | 'server:install_install-dependencies' | ||||
| //   | 'server:checkIfCondaExists' | ||||
| //   | 'server:checkIfCondaEnvironmentExists' | ||||
| //   | 'server:checkIfUvicornExists' | ||||
| //   | 'server:checkDependencies' | ||||
| //   | 'serverLog:startListening' | ||||
| //   | 'serverLog:stopListening' | ||||
| //   | 'serverLog:update'; | ||||
|  | ||||
| // actually make the Channels type empty: | ||||
| export type Channels = ''; | ||||
|  | ||||
| const electronHandler = { | ||||
|   ipcRenderer: { | ||||
|     sendMessage(channel: Channels, ...args: unknown[]) { | ||||
|       ipcRenderer.send(channel, ...args); | ||||
|     }, | ||||
|     on(channel: Channels, func: (...args: unknown[]) => void) { | ||||
|       const subscription = (_event: any, ...args: unknown[]) => func(...args); | ||||
|       ipcRenderer.on(channel, subscription); | ||||
|  | ||||
|       return () => { | ||||
|         ipcRenderer.removeListener(channel, subscription); | ||||
|       }; | ||||
|     }, | ||||
|     once(channel: Channels, func: (...args: unknown[]) => void) { | ||||
|       ipcRenderer.once(channel, (_event, ...args) => func(...args)); | ||||
|     }, | ||||
|     invoke(channel: Channels, ...args: unknown[]) { | ||||
|       return ipcRenderer.invoke(channel, ...args); | ||||
|     }, | ||||
|     removeAllListeners: (channel: string) => | ||||
|       ipcRenderer.removeAllListeners(channel), | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| contextBridge.exposeInMainWorld('electron', electronHandler); | ||||
|  | ||||
| contextBridge.exposeInMainWorld('platform', { | ||||
|   appmode: "cloud", | ||||
|   node: () => process.versions.node, | ||||
|   chrome: () => process.versions.chrome, | ||||
|   electron: () => process.versions.electron, | ||||
|   isMac: () => process.platform === 'darwin', | ||||
|   isWindows: () => process.platform === 'win32', | ||||
|   isLinux: () => process.platform === 'linux', | ||||
|   platform: () => process.platform, | ||||
|   arch: () => process.arch, | ||||
| }); | ||||
|  | ||||
| contextBridge.exposeInMainWorld('storage', { | ||||
|   get: (key: string) => { | ||||
|     const keyValue = localStorage.getItem(key); | ||||
|     try { | ||||
|       return Promise.resolve(JSON.parse(keyValue)); | ||||
|     } catch (err) { | ||||
|       // In case soemthing made it into storage wihout getting stringify-ed | ||||
|       return Promise.resolve(keyValue); | ||||
|  | ||||
|     } | ||||
|   }, | ||||
|   set: (key: string, value: any) => { | ||||
|     localStorage.setItem(key, JSON.stringify(value)); | ||||
|     return Promise.resolve(); | ||||
|   }, | ||||
|   delete: (key: string) => { | ||||
|     localStorage.removeItem(key); | ||||
|     console.log('Deleted key from localStorage:', key); | ||||
|     return Promise.resolve(); | ||||
|   }, | ||||
| }); | ||||
|  | ||||
|  | ||||
| // contextBridge.exposeInMainWorld('sshClient', { | ||||
| //   connect: (data) => ipcRenderer.invoke('ssh:connect', data), | ||||
| //   data: (data) => ipcRenderer.send('ssh:data', data), | ||||
|  | ||||
| //   onData: (data) => ipcRenderer.on('ssh:data', data), | ||||
| //   onSSHConnected: (callback) => ipcRenderer.on('ssh:connected', callback), | ||||
|  | ||||
| //   removeAllListeners: () => { | ||||
| //     ipcRenderer.removeAllListeners('ssh:data'); | ||||
| //     ipcRenderer.removeAllListeners('ssh:connected'); | ||||
| //   }, | ||||
| // }); | ||||
|  | ||||
| contextBridge.exposeInMainWorld('autoUpdater', { | ||||
|   onMessage: (f) => { | ||||
|     f(null, 'Update not available.'); | ||||
|   }, | ||||
|   removeAllListeners: () => ipcRenderer.removeAllListeners('autoUpdater'), | ||||
|   requestUpdate: () => ipcRenderer.invoke('autoUpdater:requestUpdate'), | ||||
| }); | ||||
|  | ||||
| @@ -60,6 +60,7 @@ export type ElectronHandler = typeof electronHandler; | ||||
| contextBridge.exposeInMainWorld('electron', electronHandler); | ||||
|  | ||||
| contextBridge.exposeInMainWorld('platform', { | ||||
|   appmode: "electron", | ||||
|   node: () => process.versions.node, | ||||
|   chrome: () => process.versions.chrome, | ||||
|   electron: () => process.versions.electron, | ||||
|   | ||||
| @@ -47,9 +47,10 @@ export default function App() { | ||||
|   useEffect(() => { | ||||
|     async function getSavedExperimentId() { | ||||
|       const connectionWithoutDots = connection.replace(/\./g, '-'); | ||||
|       const experimentId = await window.storage.get( | ||||
|         `experimentId.${connectionWithoutDots}` | ||||
|       ); | ||||
|       // window.storage should be defined by cloud or electron preload script | ||||
|       const experimentId = window.storage | ||||
|         ? await window.storage.get(`experimentId.${connectionWithoutDots}`) | ||||
|         : 1; | ||||
|       if (experimentId) { | ||||
|         setExperimentId(experimentId); | ||||
|       } else if (connection !== '') { | ||||
| @@ -74,7 +75,8 @@ export default function App() { | ||||
|   }, [connection]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (experimentId == '') return; | ||||
|     // if there is no experiment or window.storage isn't setup then skip | ||||
|     if (experimentId == '' || !window.storage) return; | ||||
|     const connectionWithoutDots = connection.replace(/\./g, '-'); | ||||
|     window.storage.set(`experimentId.${connectionWithoutDots}`, experimentId); | ||||
|   }, [experimentId]); | ||||
|   | ||||
| @@ -45,6 +45,8 @@ export default function LoginModal({ | ||||
|  | ||||
|   const [host, setHost] = useState(''); | ||||
|  | ||||
|   const WEB_APP = window.platform.appmode == "cloud"; | ||||
|  | ||||
|   React.useEffect(() => { | ||||
|     window.storage | ||||
|       .get('recentConnections') | ||||
| @@ -130,12 +132,14 @@ export default function LoginModal({ | ||||
|         </OneTimePopup> | ||||
|         <Tabs | ||||
|           aria-label="Basic tabs" | ||||
|           defaultValue={0} | ||||
|           defaultValue={WEB_APP ? 1 : 0} | ||||
|           sx={{ overflow: 'hidden', height: '100%' }} | ||||
|           onChange={(_event, newValue) => { }} | ||||
|         > | ||||
|           <TabList tabFlex={1}> | ||||
|             {!WEB_APP && ( | ||||
|               <Tab>Local Engine</Tab> | ||||
|             )} | ||||
|             <Tab>Connect to Remote Engine</Tab> | ||||
|             {/* <Tab value="SSH">Connect via SSH</Tab> */} | ||||
|           </TabList> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Tony Salomone
					Tony Salomone