From 204743acbc1707c335469d0c135005d7ca8baff3 Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 12 Oct 2021 15:33:47 -0400 Subject: [PATCH] Create autosave files for launcher panel entries --- products/jbrowse-desktop/public/electron.ts | 57 +++++++++++++++++-- products/jbrowse-desktop/src/JBrowse.tsx | 2 +- products/jbrowse-desktop/src/Loader.tsx | 12 ++-- .../src/StartScreen/LauncherPanel.tsx | 18 +++--- .../src/StartScreen/QuickstartPanel.tsx | 13 +++-- .../src/StartScreen/RecentSessionsPanel.tsx | 38 +++++++++---- .../jbrowse-desktop/src/StartScreen/util.tsx | 13 ++++- products/jbrowse-desktop/src/rootModel.ts | 15 +++-- 8 files changed, 126 insertions(+), 42 deletions(-) diff --git a/products/jbrowse-desktop/public/electron.ts b/products/jbrowse-desktop/public/electron.ts index 2e7d130aa4..5ab4e87a2c 100644 --- a/products/jbrowse-desktop/public/electron.ts +++ b/products/jbrowse-desktop/public/electron.ts @@ -51,11 +51,16 @@ const defaultSessionName = `jbrowse_session.json` const userData = app.getPath('userData') const recentSessionsPath = path.join(userData, 'recent_sessions.json') const quickstartDir = path.join(userData, 'quickstart') +const autosaveDir = path.join(userData, 'autosaved') function getQuickstartPath(sessionName: string, ext = 'json') { return path.join(quickstartDir, `${encodeURIComponent(sessionName)}.${ext}`) } +function getAutosavePath(sessionName: string, ext = 'json') { + return path.join(autosaveDir, `${encodeURIComponent(sessionName)}.${ext}`) +} + if (!fs.existsSync(recentSessionsPath)) { fs.writeFileSync(recentSessionsPath, stringify([]), 'utf8') } @@ -64,6 +69,10 @@ if (!fs.existsSync(quickstartDir)) { fs.mkdirSync(quickstartDir, { recursive: true }) } +if (!fs.existsSync(autosaveDir)) { + fs.mkdirSync(autosaveDir, { recursive: true }) +} + interface SessionSnap { defaultSession: { name: string @@ -268,9 +277,20 @@ ipcMain.handle('quit', () => { app.quit() }) -ipcMain.handle('listSessions', async () => { - return JSON.parse(await readFile(recentSessionsPath, 'utf8')) -}) +ipcMain.handle( + 'listSessions', + async (_event: unknown, showAutosaves: boolean) => { + const sessions = JSON.parse(await readFile(recentSessionsPath, 'utf8')) as { + path: string + }[] + + if (!showAutosaves) { + return sessions.filter(f => !f.path.startsWith(autosaveDir)) + } else { + return sessions + } + }, +) ipcMain.handle('loadExternalConfig', (_event: unknown, sessionPath) => { return readFile(sessionPath, 'utf8') @@ -341,6 +361,36 @@ ipcMain.handle( }) }, ) + +// creates an initial entry in autosave folder +ipcMain.handle( + 'createInitialAutosaveFile', + async (_event: unknown, snap: SessionSnap) => { + const rows = JSON.parse(fs.readFileSync(recentSessionsPath, 'utf8')) as [ + { path: string; updated: number }, + ] + const idx = rows.findIndex(r => r.path === path) + const path = getAutosavePath(`${+Date.now()}`) + const entry = { + path, + updated: +Date.now(), + name: snap.defaultSession?.name, + } + if (idx === -1) { + rows.unshift(entry) + } else { + rows[idx] = entry + } + await Promise.all([ + writeFile(recentSessionsPath, stringify(rows)), + writeFile(path, stringify(snap)), + ]) + + return path + }, +) + +// snapshots page and saves to path ipcMain.handle( 'saveSession', async (_event: unknown, path: string, snap: SessionSnap) => { @@ -368,7 +418,6 @@ ipcMain.handle( }, ) - ipcMain.handle('promptOpenFile', async (_event: unknown) => { const toLocalPath = path.join(app.getPath('desktop'), defaultSessionName) const choice = await dialog.showOpenDialog({ diff --git a/products/jbrowse-desktop/src/JBrowse.tsx b/products/jbrowse-desktop/src/JBrowse.tsx index 8756a87bcb..921b4d296b 100644 --- a/products/jbrowse-desktop/src/JBrowse.tsx +++ b/products/jbrowse-desktop/src/JBrowse.tsx @@ -10,7 +10,7 @@ import { AssemblyManager } from '@jbrowse/plugin-data-management' import { RootModel } from './rootModel' const JBrowse = observer( - ({ pluginManager }: { pluginManager: PluginManager; }) => { + ({ pluginManager }: { pluginManager: PluginManager }) => { const { rootModel } = pluginManager return rootModel ? ( diff --git a/products/jbrowse-desktop/src/Loader.tsx b/products/jbrowse-desktop/src/Loader.tsx index b84d472bc4..ad6d6ae607 100644 --- a/products/jbrowse-desktop/src/Loader.tsx +++ b/products/jbrowse-desktop/src/Loader.tsx @@ -5,7 +5,7 @@ import { CssBaseline, ThemeProvider, makeStyles } from '@material-ui/core' import { createJBrowseTheme } from '@jbrowse/core/ui' import { StringParam, useQueryParam } from 'use-query-params' import { ipcRenderer } from 'electron' -import { createPluginManager } from './StartScreen/util' +import { loadPluginManager } from './StartScreen/util' import JBrowse from './JBrowse' import StartScreen from './StartScreen' @@ -77,10 +77,10 @@ const Loader = observer(() => { // @ts-ignore pm.rootModel?.setOpenNewSessionCallback(async () => { const path = await ipcRenderer.invoke('promptOpenFile') - const data = await ipcRenderer.invoke('loadSession', path) - const pm = await createPluginManager(data) - handleSetPluginManager(pm) + handleSetPluginManager(await loadPluginManager(path)) }) + + // @ts-ignore setPluginManager(pm) setError(undefined) setSnapshotError('') @@ -93,9 +93,7 @@ const Loader = observer(() => { ;(async () => { if (config) { try { - const data = await ipcRenderer.invoke('loadSession', config) - const pm = await createPluginManager(data) - handleSetPluginManager(pm) + handleSetPluginManager(await loadPluginManager(config)) } catch (e) { handleError(e) } diff --git a/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx b/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx index af7b37feca..2a64634396 100644 --- a/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx @@ -1,9 +1,10 @@ import React, { useState } from 'react' import { Button, Typography, makeStyles } from '@material-ui/core' import PluginManager from '@jbrowse/core/PluginManager' +import { ipcRenderer } from 'electron' import QuickstartPanel from './QuickstartPanel' import OpenSequenceDialog from '../OpenSequenceDialog' -import { createPluginManager } from './util' +import { loadPluginManager } from './util' const useStyles = makeStyles(theme => ({ form: { @@ -50,13 +51,16 @@ export default function StartScreenOptionsPanel({ if (conf) { // note this can throw before dialog closes, but this is handled // by the dialog itself - const pm = await createPluginManager({ - assemblies: [conf], - defaultSession: { - name: 'New Session ' + new Date().toLocaleString('en-US'), + const path = await ipcRenderer.invoke( + 'createInitialAutosaveFile', + { + assemblies: [conf], + defaultSession: { + name: 'New Session ' + new Date().toLocaleString('en-US'), + }, }, - }) - setPluginManager(pm) + ) + setPluginManager(await loadPluginManager(path)) } setSequenceDialogOpen(false) }} diff --git a/products/jbrowse-desktop/src/StartScreen/QuickstartPanel.tsx b/products/jbrowse-desktop/src/StartScreen/QuickstartPanel.tsx index 6f3468836b..eb5a5fa2aa 100644 --- a/products/jbrowse-desktop/src/StartScreen/QuickstartPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/QuickstartPanel.tsx @@ -22,7 +22,7 @@ import { ipcRenderer } from 'electron' import deepmerge from 'deepmerge' // locals -import { createPluginManager } from './util' +import { loadPluginManager } from './util' const useStyles = makeStyles(theme => ({ button: { @@ -43,7 +43,7 @@ const useStyles = makeStyles(theme => ({ }, })) -function PreloadedDatasetSelector({ +function QuickstartPanel({ setPluginManager, }: { setPluginManager: (arg0: PluginManager) => void @@ -111,8 +111,11 @@ function PreloadedDatasetSelector({ config.defaultSession.name = `New session ${new Date().toLocaleString( 'en-US', )}` - const pm = await createPluginManager(config) - setPluginManager(pm) + const path = await ipcRenderer.invoke( + 'createInitialAutosaveFile', + config, + ) + setPluginManager(await loadPluginManager(path)) }} variant="contained" color="primary" @@ -211,4 +214,4 @@ function PreloadedDatasetSelector({ ) } -export default PreloadedDatasetSelector +export default QuickstartPanel diff --git a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx index 21c880c0f3..d3f78fa2c7 100644 --- a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx @@ -1,7 +1,9 @@ import React, { useState, useEffect, useMemo } from 'react' import { + Checkbox, CircularProgress, FormControl, + FormControlLabel, Grid, IconButton, Link, @@ -29,7 +31,7 @@ import PlaylistAddIcon from '@material-ui/icons/PlaylistAdd' // locals import RenameSessionDialog from './dialogs/RenameSessionDialog' import DeleteSessionDialog from './dialogs/DeleteSessionDialog' -import { useLocalStorage, createPluginManager } from './util' +import { useLocalStorage, loadPluginManager } from './util' import SessionCard from './SessionCard' const useStyles = makeStyles(theme => ({ @@ -99,12 +101,7 @@ function RecentSessionsList({ className={classes.pointer} onClick={async () => { try { - const data = await ipcRenderer.invoke( - 'loadSession', - params.row.path, - ) - const pm = await createPluginManager(data) - setPluginManager(pm) + setPluginManager(await loadPluginManager(params.row.path)) } catch (e) { console.error(e) setError(e) @@ -197,9 +194,7 @@ function RecentSessionsCards({ sessionData={session} onClick={async () => { try { - const pm = await createPluginManager( - await ipcRenderer.invoke('loadSession', session.path), - ) + const pm = await loadPluginManager(session.path) setPluginManager(pm) } catch (e) { console.error(e) @@ -242,6 +237,10 @@ export default function RecentSessionPanel({ const [updateSessionsList, setUpdateSessionsList] = useState(0) const [selectedSessions, setSelectedSessions] = useState() const [sessionsToDelete, setSessionsToDelete] = useState() + const [showAutosaves, setShowAutosaves] = useLocalStorage( + 'showAutosaves', + 'false', + ) const sortedSessions = useMemo( () => sessions?.sort((a, b) => b.updated - a.updated), @@ -251,14 +250,17 @@ export default function RecentSessionPanel({ useEffect(() => { ;(async () => { try { - const sessions = await ipcRenderer.invoke('listSessions') + const sessions = await ipcRenderer.invoke( + 'listSessions', + showAutosaves === 'true', + ) setSessions(sessions) } catch (e) { console.error(e) setError(e) } })() - }, [setError, updateSessionsList]) + }, [setError, updateSessionsList, showAutosaves]) if (!sessions) { return ( @@ -338,6 +340,18 @@ export default function RecentSessionPanel({ + + setShowAutosaves(showAutosaves === 'true' ? 'false' : 'true') + } + /> + } + label="Show autosaves" + /> + {sortedSessions.length ? ( displayMode === 'grid' ? ( o.internetAccountId) + // remove duplicates while mixing in default internet accounts jbrowse.internetAccounts = jbrowse.internetAccounts.filter( - ({ internetAccountId }, index) => - !ids.includes(internetAccountId, index + 1), + (arg, index) => !ids.includes(arg.internetAccountId, index + 1), ) const rootModel = JBrowseRootModel.create( diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index 3ad067cf27..7d6da941ec 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -79,7 +79,9 @@ export default function rootModelFactory(pluginManager: PluginManager) { .volatile(() => ({ error: undefined as unknown, textSearchManager: new TextSearchManager(pluginManager), - openNewSessionCallback: () => { console.error('openNewSessionCallback unimplemented') } + openNewSessionCallback: () => { + console.error('openNewSessionCallback unimplemented') + }, })) .actions(self => ({ async saveSession(val: unknown) { @@ -237,7 +239,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { onClick: async () => { try { await self.openNewSessionCallback() - } catch(e) { + } catch (e) { console.error(e) self.session?.notify(`${e}`, 'error') } @@ -392,8 +394,13 @@ export default function rootModelFactory(pluginManager: PluginManager) { } const url = window.location.href.split('?')[0] - const name = self.session?.name || '' - window.location.href = `${url}?config=${encodeURIComponent(name)}` + if (!self.sessionPath) { + self.session?.notify('You must save your session first') + } else { + window.location.href = `${url}?config=${encodeURIComponent( + self.sessionPath, + )}` + } }, /** * Add a top-level menu