Skip to content

Commit

Permalink
Allow updating local assets (#11314)
Browse files Browse the repository at this point in the history
* Allow updating local assets

* Update shim

* Fix duplicate upload

* Manually invalidate once we upload file

* Fix queryKey

* upd prettier ignore

* revert prettier ignore
  • Loading branch information
MrFlashAccount authored Oct 15, 2024
1 parent 4a2e522 commit 3711b25
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 95 deletions.
17 changes: 2 additions & 15 deletions app/gui/project-manager-shim-middleware/projectManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,27 +187,14 @@ function generateDirectoryName(name: string, directory = getProjectsDirectory())

// If the name already consists a suffix, reuse it.
const matches = name.match(/^(.*)_(\d+)$/)
const initialSuffix = -1
let suffix = initialSuffix
// Matches start with the whole match, so we need to skip it. Then come our two capture groups.
const [matchedName, matchedSuffix] = matches?.slice(1) ?? []

if (typeof matchedName !== 'undefined' && typeof matchedSuffix !== 'undefined') {
name = matchedName
suffix = parseInt(matchedSuffix)
}

let finalPath: string
// eslint-disable-next-line no-constant-condition
while (true) {
suffix++
const newName = `${name}${suffix === 0 ? '' : `_${suffix}`}`
const candidatePath = pathModule.join(directory, newName)
if (!fs.existsSync(candidatePath)) {
finalPath = candidatePath
break
}
}
return finalPath
return pathModule.join(directory, name)
}

/**
Expand Down
7 changes: 4 additions & 3 deletions app/gui/src/dashboard/layouts/AssetsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1782,6 +1782,10 @@ export default function AssetsTable(props: AssetsTableProps) {
deleteAsset(projectId)
toastAndLog('uploadProjectError', error)
})

void queryClient.invalidateQueries({
queryKey: [backend.type, 'listDirectory', asset.parentId],
})
} else {
uploadFileMutation
.mutateAsync([
Expand Down Expand Up @@ -1896,7 +1900,6 @@ export default function AssetsTable(props: AssetsTableProps) {

fileMap.set(asset.id, conflict.file)

insertAssets([asset], event.parentId)
void doUploadFile(asset, isUpdating ? 'update' : 'new')
}
}}
Expand Down Expand Up @@ -1934,8 +1937,6 @@ export default function AssetsTable(props: AssetsTableProps) {

const assets = [...newFiles, ...newProjects]

insertAssets(assets, event.parentId)

for (const asset of assets) {
void doUploadFile(asset, 'new')
}
Expand Down
52 changes: 34 additions & 18 deletions app/ide-desktop/client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/** @file Definition of an Electron application, which entails the creation of a rudimentary HTTP
/**
* @file Definition of an Electron application, which entails the creation of a rudimentary HTTP
* server and the presentation of a Chrome web view, designed for optimal performance and
* compatibility across a wide range of hardware configurations. The application's web component
* is then served and showcased within the web view, complemented by the establishment of an
* Inter-Process Communication channel, which enables seamless communication between the served web
* application and the Electron process. */
* application and the Electron process.
*/

import './cjs-shim' // must be imported first

Expand Down Expand Up @@ -54,8 +56,10 @@ function pathToURL(path: string): URL {
// === App ===
// ===========

/** The Electron application. It is responsible for starting all the required services, and
* displaying and managing the app window. */
/**
* The Electron application. It is responsible for starting all the required services, and
* displaying and managing the app window.
*/
class App {
window: electron.BrowserWindow | null = null
server: server.Server | null = null
Expand Down Expand Up @@ -155,12 +159,14 @@ class App {
return { ...configParser.parseArgs(argsToParse), fileToOpen, urlToOpen }
}

/** Set the project to be opened on application startup.
/**
* Set the project to be opened on application startup.
*
* This method should be called before the application is ready, as it only
* modifies the startup options. If the application is already initialized,
* an error will be logged, and the method will have no effect.
* @param projectUrl - The `file://` url of project to be opened on startup. */
* @param projectUrl - The `file://` url of project to be opened on startup.
*/
setProjectToOpenOnStartup(projectUrl: URL) {
// Make sure that we are not initialized yet, as this method should be called before the
// application is ready.
Expand All @@ -176,8 +182,10 @@ class App {
}
}

/** This method is invoked when the application was spawned due to being a default application
* for a URL protocol or file extension. */
/**
* This method is invoked when the application was spawned due to being a default application
* for a URL protocol or file extension.
*/
handleItemOpening(fileToOpen: string | null, urlToOpen: URL | null) {
logger.log('Opening file or URL.', { fileToOpen, urlToOpen })
try {
Expand All @@ -196,8 +204,10 @@ class App {
}
}

/** Set Chrome options based on the app configuration. For comprehensive list of available
* Chrome options refer to: https://peter.sh/experiments/chromium-command-line-switches. */
/**
* Set Chrome options based on the app configuration. For comprehensive list of available
* Chrome options refer to: https://peter.sh/experiments/chromium-command-line-switches.
*/
setChromeOptions(chromeOptions: configParser.ChromeOption[]) {
const addIf = (
option: contentConfig.Option<boolean>,
Expand Down Expand Up @@ -256,10 +266,12 @@ class App {
await this.createWindowIfEnabled(windowSize)
this.initIpc()
await this.loadWindowContent()
/** The non-null assertion on the following line is safe because the window
/**
* The non-null assertion on the following line is safe because the window
* initialization is guarded by the `createWindowIfEnabled` method. The window is
* not yet created at this point, but it will be created by the time the
* authentication module uses the lambda providing the window. */
* authentication module uses the lambda providing the window.
*/
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
authentication.initAuthentication(() => this.window!)
})
Expand Down Expand Up @@ -443,8 +455,10 @@ class App {
})
}

/** Initialize Inter-Process Communication between the Electron application and the served
* website. */
/**
* Initialize Inter-Process Communication between the Electron application and the served
* website.
*/
initIpc() {
electron.ipcMain.on(ipc.Channel.error, (_event, data) => {
logger.error(`IPC error: ${JSON.stringify(data)}`)
Expand Down Expand Up @@ -475,9 +489,9 @@ class App {
})
electron.ipcMain.on(
ipc.Channel.importProjectFromPath,
(event, path: string, directory: string | null) => {
(event, path: string, directory: string | null, title: string) => {
const directoryParams = directory == null ? [] : [directory]
const info = projectManagement.importProjectFromPath(path, ...directoryParams)
const info = projectManagement.importProjectFromPath(path, ...directoryParams, title)
event.reply(ipc.Channel.importProjectFromPath, path, info)
},
)
Expand Down Expand Up @@ -537,9 +551,11 @@ class App {
})
}

/** The server port. In case the server was not started, the port specified in the configuration
/**
* The server port. In case the server was not started, the port specified in the configuration
* is returned. This might be used to connect this application window to another, existing
* application server. */
* application server.
*/
serverPort(): number {
return this.server?.config.port ?? this.args.groups.server.options.port.value
}
Expand Down
36 changes: 23 additions & 13 deletions app/ide-desktop/client/src/preload.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/** @file Preload script containing code that runs before web page is loaded into the browser
/**
* @file Preload script containing code that runs before web page is loaded into the browser
* window. It has access to both DOM APIs and Node environment, and is used to expose privileged
* APIs to the renderer via the contextBridge API. To learn more, visit:
* https://www.electronjs.org/docs/latest/tutorial/tutorial-preload. */
* https://www.electronjs.org/docs/latest/tutorial/tutorial-preload.
*/

import type * as accessToken from 'enso-common/src/accessToken'

Expand All @@ -14,7 +16,7 @@ import type * as projectManagement from '@/projectManagement'
// esbuild, we have to manually use "require". Switch this to an import once new electron version
// actually honours ".mjs" files for sandboxed preloading (this will likely become an error at that time).
// https://www.electronjs.org/fr/docs/latest/tutorial/esm#sandboxed-preload-scripts-cant-use-esm-imports
// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/no-var-requires
// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/no-var-requires,@typescript-eslint/no-require-imports
const electron = require('electron')

// =================
Expand Down Expand Up @@ -52,8 +54,8 @@ const IMPORT_PROJECT_RESOLVE_FUNCTIONS = new Map<
>()

exposeInMainWorld(BACKEND_API_KEY, {
importProjectFromPath: (projectPath: string, directory: string | null = null) => {
electron.ipcRenderer.send(ipc.Channel.importProjectFromPath, projectPath, directory)
importProjectFromPath: (projectPath: string, directory: string | null = null, title: string) => {
electron.ipcRenderer.send(ipc.Channel.importProjectFromPath, projectPath, directory, title)
return new Promise<projectManagement.ProjectInfo>(resolve => {
IMPORT_PROJECT_RESOLVE_FUNCTIONS.set(projectPath, resolve)
})
Expand Down Expand Up @@ -94,7 +96,8 @@ electron.ipcRenderer.on(
},
)

/** Object exposed on the Electron main window; provides proxy functions to:
/**
* Object exposed on the Electron main window; provides proxy functions to:
* - open OAuth flows in the system browser, and
* - handle deep links from the system browser or email client to the dashboard.
*
Expand All @@ -104,28 +107,35 @@ electron.ipcRenderer.on(
* The functions are exposed via this "API object", which is added to the main window.
*
* For more details, see:
* https://www.electronjs.org/docs/latest/api/context-bridge#api-functions. */
* https://www.electronjs.org/docs/latest/api/context-bridge#api-functions.
*/
exposeInMainWorld(AUTHENTICATION_API_KEY, {
/** Open a URL in the system browser (rather than in the app).
/**
* Open a URL in the system browser (rather than in the app).
*
* OAuth URLs must be opened this way because the dashboard application is sandboxed and thus
* not privileged to do so unless we explicitly expose this functionality. */
* not privileged to do so unless we explicitly expose this functionality.
*/
openUrlInSystemBrowser: (url: string) => {
electron.ipcRenderer.send(ipc.Channel.openUrlInSystemBrowser, url)
},
/** Set the callback that will be called when a deep link to the application is opened.
/**
* Set the callback that will be called when a deep link to the application is opened.
*
* The callback is intended to handle links like
* `enso://authentication/register?code=...&state=...` from external sources like the user's
* system browser or email client. Handling the links involves resuming whatever flow was in
* progress when the link was opened (e.g., an OAuth registration flow). */
* progress when the link was opened (e.g., an OAuth registration flow).
*/
setDeepLinkHandler: (callback: (url: string) => void) => {
deepLinkHandler = callback
},
/** Save the access token to a credentials file.
/**
* Save the access token to a credentials file.
*
* The backend doesn't have access to Electron's `localStorage` so we need to save access token
* to a file. Then the token will be used to sign cloud API requests. */
* to a file. Then the token will be used to sign cloud API requests.
*/
saveAccessToken: (accessTokenPayload: accessToken.AccessToken | null) => {
electron.ipcRenderer.send(ipc.Channel.saveAccessToken, accessTokenPayload)
},
Expand Down
Loading

0 comments on commit 3711b25

Please sign in to comment.