diff --git a/client/client.ts b/client/client.ts
index 7986fbe7..788f45ec 100644
--- a/client/client.ts
+++ b/client/client.ts
@@ -1,29 +1,18 @@
import omit from 'lodash/omit'
-import * as settings from './settings'
import * as types from './types'
export class Client {
serverUrl?: string
- constructor(props: { serverUrl?: string } = {}) {
- this.serverUrl = props.serverUrl
- }
-
- async readServerUrl() {
- return (
- this.serverUrl ||
- // @ts-ignore
- (await window?.opendataeditor?.readServerUrl()) ||
- settings.SERVER_URL
- )
+ constructor() {
+ this.serverUrl = 'http://localhost:4040'
}
async request(
path: string,
props: { [key: string]: any; file?: File; isBytes?: boolean } = {}
) {
- const serverUrl = await this.readServerUrl()
- return makeRequest(serverUrl + path, props)
+ return makeRequest(this.serverUrl + path, props)
}
// Article
diff --git a/client/components/Application/Dialog.tsx b/client/components/Application/Dialog.tsx
index 7e330961..ca1650ac 100644
--- a/client/components/Application/Dialog.tsx
+++ b/client/components/Application/Dialog.tsx
@@ -10,12 +10,12 @@ import DeleteFolderDialog from './Dialogs/DeleteFolder'
import IndexFilesDialog from './Dialogs/IndexFiles'
import MoveFileDialog from './Dialogs/MoveFile'
import MoveFolderDialog from './Dialogs/MoveFolder'
-import StartDialog from './Dialogs/Start'
import { useStore } from './store'
export default function Dialog() {
const dialog = useStore((state) => state.dialog)
if (!dialog) return null
+ // @ts-ignore
const Dialog = DIALOGS[dialog]
return
}
@@ -34,5 +34,4 @@ const DIALOGS = {
indexFiles: IndexFilesDialog,
moveFile: MoveFileDialog,
moveFolder: MoveFolderDialog,
- start: StartDialog,
}
diff --git a/client/components/Application/Dialogs/Start.tsx b/client/components/Application/Dialogs/Start.tsx
deleted file mode 100644
index 7cfd3371..00000000
--- a/client/components/Application/Dialogs/Start.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import Box from '@mui/material/Box'
-import WaitDialog from '../../Parts/Dialogs/Wait'
-import RocketLaunchIcon from '@mui/icons-material/RocketLaunch'
-
-export default function StartDialog() {
- return (
-
-
-
- Initializing the application!
- {' '}
-
- If it is a first run on your computer, it may take
- a few minutes
- {' '}
- to download dependencies. Next time it will be instant!
-
-
- )
-}
diff --git a/client/components/Application/store.ts b/client/components/Application/store.ts
index d39a617f..29c88271 100644
--- a/client/components/Application/store.ts
+++ b/client/components/Application/store.ts
@@ -84,9 +84,7 @@ export function makeStore(props: ApplicationProps) {
onStart: async () => {
const { client, loadConfig, loadFiles, updateState } = get()
- updateState({ dialog: 'start' })
- // Wait for the server
// @ts-ignore
const sendFatalError = window?.opendataeditor?.sendFatalError
let ready = false
@@ -101,21 +99,18 @@ export function makeStore(props: ApplicationProps) {
} catch (error) {
attempt += 1
if (attempt >= maxAttempts) {
- const serverUrl = await client.readServerUrl()
+ const serverUrl = client.serverUrl
const message = `Client cannot connect to server on "${serverUrl}"`
sendFatalError ? sendFatalError(message) : alert(message)
}
await delay(delaySeconds * 1000)
}
}
-
// Setup project sync polling
setInterval(async () => {
const { files } = await client.projectSync({})
updateState({ files })
}, settings.PROJECT_SYNC_INTERVAL_MILLIS)
-
- updateState({ dialog: undefined })
},
onFileCreate: async (paths) => {
const { loadFiles, selectFile } = get()
diff --git a/client/components/Parts/Dialogs/Wait.tsx b/client/components/Parts/Dialogs/Wait.tsx
deleted file mode 100644
index f56d1b0a..00000000
--- a/client/components/Parts/Dialogs/Wait.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import * as React from 'react'
-import Box from '@mui/material/Box'
-import Dialog from '@mui/material/Dialog'
-import DialogTitle from '@mui/material/DialogTitle'
-import DialogContent from '@mui/material/DialogContent'
-import LinearProgress from '@mui/material/LinearProgress'
-
-export interface WaitDialogProps {
- open?: boolean
- title?: string
- maxWidth?: 'md' | 'xl'
- children?: React.ReactNode
-}
-
-export default function WaitDialog(props: WaitDialogProps) {
- return (
-
- )
-}
diff --git a/client/loading.html b/client/loading.html
new file mode 100644
index 00000000..c0fa139f
--- /dev/null
+++ b/client/loading.html
@@ -0,0 +1,71 @@
+
+
+
+
+ Open Data Editor
+
+
+
+
+
+
+
+
Open Data Editor is loading
+
The first time launching Open Data Editor needs to ensure requirements are installed.
+
+ - Ensure python is installed
+ - Ensure python dependencies are installed
+ - Ensure server is running
+
+
+
+
+
diff --git a/desktop/bridge.ts b/desktop/bridge.ts
index 527f0973..6bb38c2d 100644
--- a/desktop/bridge.ts
+++ b/desktop/bridge.ts
@@ -1,10 +1,7 @@
import { ipcMain, dialog, app } from 'electron'
import log from 'electron-log'
-export function createBridge({ serverPort }: { serverPort: number }) {
- ipcMain.handle('readServerUrl', async () => {
- return `http://localhost:${serverPort}`
- })
+export function createBridge() {
ipcMain.handle('sendFatalError', async (_ev, message: string) => {
log.error(message)
await dialog.showMessageBox({
diff --git a/desktop/index.ts b/desktop/index.ts
index 3e6b5b8e..aabeaf70 100644
--- a/desktop/index.ts
+++ b/desktop/index.ts
@@ -1,15 +1,10 @@
import { app, dialog, BrowserWindow } from 'electron'
-import { electronApp, optimizer } from '@electron-toolkit/utils'
+import { electronApp, optimizer } from '@electron-toolkit/utils'
import { createWindow } from './window'
import { createBridge } from './bridge'
-import { is } from '@electron-toolkit/utils'
import { join } from 'path'
import log from 'electron-log'
-import * as system from './system'
-import * as server from './server'
-import * as python from './python'
import * as settings from './settings'
-import * as resources from './resources'
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
@@ -17,19 +12,10 @@ import * as resources from './resources'
app.whenReady().then(async () => {
log.info('# Start application')
electronApp.setAppUserModelId(settings.APP_USER_MODEL_ID)
- const serverPort = await system.findPort()
log.info('## Start client')
- createBridge({ serverPort })
- createWindow()
-
- if (!is.dev) {
- log.info('## Start server')
- await resources.ensureRunner()
- await python.ensurePython()
- await python.ensureLibraries()
- await server.runServer({ serverPort })
- }
+ createBridge()
+ await createWindow()
})
// Default open or close DevTools by F12 in development
diff --git a/desktop/preload/index.ts b/desktop/preload/index.ts
index f034ad57..f1912d5b 100644
--- a/desktop/preload/index.ts
+++ b/desktop/preload/index.ts
@@ -1,7 +1,7 @@
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('opendataeditor', {
- readServerUrl: () => ipcRenderer.invoke('readServerUrl'),
sendFatalError: (message: string) => ipcRenderer.invoke('sendFatalError', message),
openDirectoryDialog: () => ipcRenderer.invoke('openDirectoryDialog'),
+ ensureLogs: (callback: any) => ipcRenderer.on('ensureLogs', (_event, message: string) => callback(message))
})
diff --git a/desktop/python.ts b/desktop/python.ts
index eab6abbf..5144d5af 100644
--- a/desktop/python.ts
+++ b/desktop/python.ts
@@ -6,20 +6,22 @@ import log from 'electron-log'
import toml from 'toml'
import * as system from './system'
-export async function ensurePython() {
- log.info('[ensurePython]', { path: settings.APP_PYTHON })
+export async function ensurePythonVirtualEnvironment() {
+ // When running, ODE will ensure that a virtual environment for dependencies exists
+ log.info('[ensurePythonVirtualEnvironment]', { path: settings.APP_PYTHON_VENV })
let message = 'existed'
- if (!fs.existsSync(settings.APP_PYTHON)) {
- await system.execFile(settings.PYTHON_SOURCE, ['-m', 'venv', settings.APP_PYTHON])
+ if (!fs.existsSync(settings.APP_PYTHON_VENV)) {
+ await system.execFile(settings.PYTHON_SOURCE, ['-m', 'venv', settings.APP_PYTHON_VENV])
message = 'created'
}
- log.info('[ensurePython]', { message })
+ log.info('[ensurePythonVirtualEnvironment]', { message })
}
-export async function ensureLibraries() {
- log.info('[ensureLibraries]')
+export async function ensurePythonRequirements() {
+ // When running, ODE will ensure that all python dependencies are installed.
+ log.info('[ensurePythonRequirements]')
const required = await readRequiredLibraries()
const installed = await readInstalledLibraries()
@@ -35,7 +37,7 @@ export async function ensureLibraries() {
...missing,
])
- log.info('[ensureLibraries]', { missing })
+ log.info('[ensurePythonRequirements]', { missing })
}
export async function readRequiredLibraries() {
diff --git a/desktop/resources.ts b/desktop/resources.ts
index c31fd6b2..6c2750ec 100644
--- a/desktop/resources.ts
+++ b/desktop/resources.ts
@@ -3,18 +3,20 @@ import fsp from 'fs/promises'
import * as settings from './settings'
import log from 'electron-log'
-export async function ensureRunner() {
- log.info('[ensureRunner]', { path: settings.APP_RUNNER })
+export async function ensurePython() {
+ // ODE builds a Python 3.10 distribution and ships it with the app.
+ // It is generated when running make build
+ log.info('[ensurePython]', { path: settings.APP_PYTHON })
let message = 'existed'
- if (!fs.existsSync(settings.APP_RUNNER)) {
- await fsp.mkdir(settings.APP_RUNNER, { recursive: true })
- await fsp.cp(settings.DIST_RUNNER, settings.APP_RUNNER, {
+ if (!fs.existsSync(settings.APP_PYTHON)) {
+ await fsp.mkdir(settings.APP_PYTHON, { recursive: true })
+ await fsp.cp(settings.DIST_PYTHON, settings.APP_PYTHON, {
recursive: true,
verbatimSymlinks: true,
})
message = 'created'
}
- log.info('[ensureRunner]', { message })
+ log.info('[ensurePython]', { message })
}
diff --git a/desktop/server.ts b/desktop/server.ts
index 2bc04831..9b81eef2 100644
--- a/desktop/server.ts
+++ b/desktop/server.ts
@@ -2,13 +2,13 @@ import { spawnFile } from './system'
import * as settings from './settings'
import log from 'electron-log'
-export async function runServer({ serverPort }: { serverPort: number }) {
- log.info('[runServer]', { serverPort })
+export async function runServer() {
+ log.info(`[Run FastAPI server on port 4040`)
- // Start server
+ // Start production server
const proc = spawnFile(
settings.PYTHON_TARGET,
- ['-m', 'server', settings.APP_TMP, '--port', serverPort.toString()],
+ ['-m', 'server', settings.APP_TMP, '--port', `4040`],
process.resourcesPath
)
diff --git a/desktop/settings.ts b/desktop/settings.ts
index 94097f38..235013b8 100644
--- a/desktop/settings.ts
+++ b/desktop/settings.ts
@@ -4,20 +4,17 @@ import { join } from 'path'
export const WIN = process.platform === 'win32'
export const HOME = os.homedir()
-export const PORT_DEV = 4040
-export const PORT_PROD = 4444
-
export const DIST = process.resourcesPath
-export const DIST_RUNNER = join(DIST, 'runner')
+export const DIST_PYTHON = join(DIST, 'runner')
export const DIST_SERVER = join(DIST, 'server')
export const APP_NAME = 'opendataeditor'
export const APP_USER_MODEL_ID = 'org.opendataeditor'
export const APP_HOME = join(HOME, `.${APP_NAME}`)
-export const APP_RUNNER = join(APP_HOME, 'runner')
-export const APP_PYTHON = join(APP_HOME, 'python')
+export const APP_PYTHON = join(APP_HOME, 'runner')
+export const APP_PYTHON_VENV = join(APP_HOME, 'python')
// APP_TMP will be the folder to upload files the first time the user opens the Application
export const APP_TMP = join(APP_HOME, 'tmp')
-export const PYTHON_SOURCE = join(APP_RUNNER, WIN ? 'python.exe' : 'bin/python3')
-export const PYTHON_TARGET = join(APP_PYTHON, WIN ? 'Scripts\\python.exe' : 'bin/python3')
+export const PYTHON_SOURCE = join(APP_PYTHON, WIN ? 'python.exe' : 'bin/python3')
+export const PYTHON_TARGET = join(APP_PYTHON_VENV, WIN ? 'Scripts\\python.exe' : 'bin/python3')
diff --git a/desktop/system.ts b/desktop/system.ts
index af45f19e..cbce1dcf 100644
--- a/desktop/system.ts
+++ b/desktop/system.ts
@@ -1,22 +1,8 @@
import cp from 'child_process'
import util from 'util'
import log from 'electron-log'
-import portfinder from 'portfinder'
-import { is } from '@electron-toolkit/utils'
-import * as settings from './settings'
const execFilePromise = util.promisify(cp.execFile)
-export async function findPort() {
- log.info('[findPort]')
-
- const port = !is.dev
- ? await portfinder.getPortPromise({ port: settings.PORT_PROD })
- : settings.PORT_DEV
-
- log.info('[findPort]', { port })
- return port
-}
-
export async function execFile(path: string, args: string[], cwd?: string) {
log.info('[execFile]', { path, args, cwd })
diff --git a/desktop/window.ts b/desktop/window.ts
index 10ee94ab..5d232d3f 100644
--- a/desktop/window.ts
+++ b/desktop/window.ts
@@ -1,16 +1,35 @@
import { shell, BrowserWindow } from 'electron'
+
import { resolve, join } from 'path'
import { is } from '@electron-toolkit/utils'
+import log from 'electron-log'
+import * as server from './server'
+import * as python from './python'
+import * as resources from './resources'
+import EventEmitter from 'events'
+
+const loadingEvents = new EventEmitter()
+
// @ts-ignore
import icon from './assets/icon.png?asset'
-export function createWindow() {
+export async function createWindow() {
// Create the browser window.
+ var splashWindow = new BrowserWindow({
+ width: 500,
+ height: 500,
+ frame: false,
+ alwaysOnTop: true,
+ resizable: false,
+ autoHideMenuBar: true,
+ ...(process.platform === 'linux' ? { icon } : {}),
+ webPreferences: {
+ preload: join(__dirname, 'preload', 'index.js'),
+ },
+ });
+
const mainWindow = new BrowserWindow({
- // width: 900,
- // height: 670,
show: false,
- // alwaysOnTop: true,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
@@ -18,23 +37,41 @@ export function createWindow() {
},
})
- mainWindow.on('ready-to-show', () => {
- mainWindow.maximize()
- mainWindow.show()
- })
-
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
- // HMR for renderer base on electron-vite cli.
- // Load the remote URL for development or the local html file for production.
- if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
- mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
- } else {
- mainWindow.loadFile(resolve(__dirname, '..', 'client', 'index.html'))
+ loadingEvents.on('finished', () => {
+ splashWindow.close();
+ log.info('Opening index.html')
+ // HMR for renderer base on electron-vite cli.
+ // Load the remote URL for development or the local html file for production.
+ if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
+ mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
+ } else {
+ mainWindow.loadFile(resolve(__dirname, '..', 'client', 'index.html'))
+ }
+ mainWindow.maximize()
+ mainWindow.show();
+ })
+
+ // Open the DevTools.
+ // mainWindow.webContents.openDevTools()
+
+ if (!is.dev) {
+ log.info('Opening loading.html')
+ splashWindow.loadFile(resolve(__dirname, '..', 'client', 'loading.html'))
+ splashWindow.center()
+ log.info('## Start server')
+ await resources.ensurePython()
+ await python.ensurePythonVirtualEnvironment()
+ splashWindow?.webContents.send('ensureLogs', "python")
+ await python.ensurePythonRequirements()
+ splashWindow?.webContents.send('ensureLogs', "requirements")
+ await server.runServer()
+ splashWindow?.webContents.send('ensureLogs', "server")
}
- return mainWindow
+ loadingEvents.emit('finished')
}
diff --git a/electron.vite.config.ts b/electron.vite.config.ts
index 2f87496c..16c9d4d2 100644
--- a/electron.vite.config.ts
+++ b/electron.vite.config.ts
@@ -24,6 +24,7 @@ export default defineConfig({
outDir: 'build/client',
rollupOptions: {
input: {
+ loading: 'client/loading.html',
index: 'client/index.html',
},
},
diff --git a/package.json b/package.json
index e86ead11..58c2b834 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
"@fontsource/roboto": "5.0.5",
"@fontsource/roboto-mono": "5.0.5",
"@inovua/reactdatagrid-community": "5.10.1",
- "@modyfi/vite-plugin-yaml": "1.0.4",
+ "@modyfi/vite-plugin-yaml": "1.1.0",
"@monaco-editor/react": "4.5.1",
"@mui/icons-material": "5.14.1",
"@mui/material": "5.14.2",
@@ -66,8 +66,8 @@
"@types/validator": "13.7.17",
"@typescript-eslint/eslint-plugin": "6.7.3",
"@typescript-eslint/parser": "6.7.3",
- "@vitejs/plugin-react": "4.0.4",
- "@vitest/coverage-v8": "0.34.4",
+ "@vitejs/plugin-react": "4.3.1",
+ "@vitest/coverage-v8": "1.6.0",
"ahooks": "3.7.8",
"classnames": "2.3.2",
"concurrently": "8.2.0",
@@ -75,10 +75,10 @@
"dayjs": "1.11.9",
"delay": "6.0.0",
"dirty-json": "0.9.2",
- "electron": "26.2.1",
- "electron-builder": "24.6.4",
- "electron-updater": "6.1.4",
- "electron-vite": "1.0.28",
+ "electron": "31.0.1",
+ "electron-builder": "^24.13.3",
+ "electron-updater": "6.2.1",
+ "electron-vite": "2.1.0",
"eslint": "8.45.0",
"fast-deep-equal": "3.1.3",
"husky": "8.0.3",
@@ -101,8 +101,8 @@
"validator": "13.9.0",
"vega": "5.25.0",
"vega-lite": "5.14.1",
- "vite": "4.4.9",
- "vitest": "0.34.4",
+ "vite": "5.3.1",
+ "vitest": "1.6.0",
"zustand": "4.3.9"
},
"prettier": {