From 279b5548e722f254cee659eb91f215f08606059e Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Mon, 14 Aug 2023 21:51:19 +1000 Subject: [PATCH 1/9] wip --- .../src/dashboard/components/assetsTable.tsx | 9 ++ .../src/dashboard/components/dashboard.tsx | 99 +++++++++++++++---- .../src/dashboard/components/projectIcon.tsx | 6 +- .../src/dashboard/events/assetEvent.ts | 1 + .../src/dashboard/events/assetListEvent.ts | 8 ++ 5 files changed, 101 insertions(+), 22 deletions(-) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetsTable.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetsTable.tsx index 7df8613abf83..a7e12829fe38 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetsTable.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetsTable.tsx @@ -758,6 +758,14 @@ export default function AssetsTable(props: AssetsTableProps) { }) break } + case assetListEventModule.AssetListEventType.openProject: { + dispatchAssetEvent({ + type: assetEventModule.AssetEventType.openProject, + id: event.id, + shouldAutomaticallySwitchPage: event.shouldAutomaticallySwitchPage, + }) + break + } case assetListEventModule.AssetListEventType.delete: { setItems(oldItems => oldItems.filter(item => item.id !== event.id)) itemDepthsRef.current.delete(event.id) @@ -771,6 +779,7 @@ export default function AssetsTable(props: AssetsTableProps) { dispatchAssetEvent({ type: assetEventModule.AssetEventType.openProject, id: projectId, + shouldAutomaticallySwitchPage: true, }) }, [/* should never change */ dispatchAssetEvent] diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx index 7a8ffe0d2927..f6a357095365 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx @@ -23,6 +23,7 @@ import * as shortcutsProvider from '../../providers/shortcuts' import * as app from '../../components/app' import * as pageSwitcher from './pageSwitcher' +import * as projectIcon from './projectIcon' import * as spinner from './spinner' import Chat, * as chat from './chat' import DriveView from './driveView' @@ -91,28 +92,88 @@ export default function Dashboard(props: DashboardProps) { localStorageModule.LocalStorageKey.projectStartupInfo ) if (savedProjectStartupInfo != null) { - setProjectStartupInfo(savedProjectStartupInfo) - if (page !== pageSwitcher.Page.editor) { - // A workaround to hide the spinner, when the previous project is being loaded in - // the background. This `MutationObserver` is disconnected when the loader is - // removed from the DOM. - const observer = new MutationObserver(mutations => { - for (const mutation of mutations) { - for (const node of Array.from(mutation.addedNodes)) { - if (node instanceof HTMLElement && node.id === LOADER_ELEMENT_ID) { - document.body.style.cursor = 'auto' - node.style.display = 'none' + if (savedProjectStartupInfo.backendType === backendModule.BackendType.remote) { + if (session.accessToken != null) { + if (backend.type === backendModule.BackendType.remote) { + dispatchAssetListEvent({ + type: assetListEventModule.AssetListEventType.openProject, + id: savedProjectStartupInfo.project.projectId, + shouldAutomaticallySwitchPage: page === pageSwitcher.Page.editor, + }) + } else { + const httpClient = new http.Client( + new Headers([['Authorization', `Bearer ${session.accessToken}`]]) + ) + const remoteBackend = new remoteBackendModule.RemoteBackend( + httpClient, + logger + ) + void (async () => { + const projectId = savedProjectStartupInfo.project.projectId + const projectName = savedProjectStartupInfo.project.packageName + let project = await remoteBackend.getProjectDetails( + projectId, + projectName + ) + if ( + project.state.type !== backendModule.ProjectState.openInProgress && + project.state.type !== backendModule.ProjectState.opened + ) { + await remoteBackend.openProject(projectId, null, projectName) } - } - for (const node of Array.from(mutation.removedNodes)) { - if (node instanceof HTMLElement && node.id === LOADER_ELEMENT_ID) { - document.body.style.cursor = 'auto' - observer.disconnect() + let nextCheckTimestamp = 0 + while (project.state.type !== backendModule.ProjectState.opened) { + await new Promise(resolve => { + const delayMs = nextCheckTimestamp - Number(new Date()) + setTimeout(resolve, Math.max(0, delayMs)) + }) + nextCheckTimestamp = + Number(new Date()) + projectIcon.CHECK_STATUS_INTERVAL_MS + project = await backend.getProjectDetails(projectId, projectName) } - } + nextCheckTimestamp = 0 + while (true) { + try { + await new Promise(resolve => { + const delayMs = nextCheckTimestamp - Number(new Date()) + setTimeout(resolve, Math.max(0, delayMs)) + }) + nextCheckTimestamp = + Number(new Date()) + projectIcon.CHECK_RESOURCES_INTERVAL_MS + await backend.checkResources(projectId, projectName) + break + } catch { + // Ignored. + } + } + setProjectStartupInfo({ ...savedProjectStartupInfo, project }) + })() } - }) - observer.observe(document.body, { childList: true }) + } + } else { + setProjectStartupInfo(savedProjectStartupInfo) + if (page !== pageSwitcher.Page.editor) { + // A workaround to hide the spinner, when the previous project is being loaded in + // the background. This `MutationObserver` is disconnected when the loader is + // removed from the DOM. + const observer = new MutationObserver(mutations => { + for (const mutation of mutations) { + for (const node of Array.from(mutation.addedNodes)) { + if (node instanceof HTMLElement && node.id === LOADER_ELEMENT_ID) { + document.body.style.cursor = 'auto' + node.style.display = 'none' + } + } + for (const node of Array.from(mutation.removedNodes)) { + if (node instanceof HTMLElement && node.id === LOADER_ELEMENT_ID) { + document.body.style.cursor = 'auto' + observer.disconnect() + } + } + } + }) + observer.observe(document.body, { childList: true }) + } } } // This MUST only run when the component is mounted. diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectIcon.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectIcon.tsx index f4b7c884e102..79a72fe9fc02 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectIcon.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectIcon.tsx @@ -22,9 +22,9 @@ import SvgMask from '../../authentication/components/svgMask' const LOADING_MESSAGE = 'Your environment is being created. It will take some time, please be patient.' /** The interval between requests checking whether the IDE is ready. */ -const CHECK_STATUS_INTERVAL_MS = 5000 +export const CHECK_STATUS_INTERVAL_MS = 5000 /** The interval between requests checking whether the VM is ready. */ -const CHECK_RESOURCES_INTERVAL_MS = 1000 +export const CHECK_RESOURCES_INTERVAL_MS = 1000 /** The corresponding {@link SpinnerState} for each {@link backendModule.ProjectState}, * when using the remote backend. */ const REMOTE_SPINNER_STATE: Record = { @@ -221,7 +221,7 @@ export default function ProjectIcon(props: ProjectIconProps) { setShouldOpenWhenReady(false) void closeProject(false) } else { - setShouldOpenWhenReady(true) + setShouldOpenWhenReady(event.shouldAutomaticallySwitchPage) void openProject() } break diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/events/assetEvent.ts b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/events/assetEvent.ts index 44ecfc773445..941209bb08b2 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/events/assetEvent.ts +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/events/assetEvent.ts @@ -83,6 +83,7 @@ export interface AssetNewSecretEvent extends AssetBaseEvent { id: backendModule.ProjectId + shouldAutomaticallySwitchPage: boolean } /** A signal to cancel automatically opening any project that is currently opening. */ diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/events/assetListEvent.ts b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/events/assetListEvent.ts index 370dab2becc4..387b7cf3b681 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/events/assetListEvent.ts +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/events/assetListEvent.ts @@ -18,6 +18,7 @@ export enum AssetListEventType { newProject = 'new-project', uploadFiles = 'upload-files', newSecret = 'new-secret', + openProject = 'open-project', delete = 'delete', } @@ -32,6 +33,7 @@ interface AssetListEvents { newProject: AssetListNewProjectEvent uploadFiles: AssetListUploadFilesEvent newSecret: AssetListNewSecretEvent + openProject: AssetListOpenProjectEvent delete: AssetListDeleteEvent } @@ -76,6 +78,12 @@ interface AssetListNewSecretEvent extends AssetListBaseEvent { + id: backend.ProjectId + shouldAutomaticallySwitchPage: boolean +} + /** A signal that a file has been deleted. This must not be called before the request is * finished. */ interface AssetListDeleteEvent extends AssetListBaseEvent { From 6cac863026103490ee4f16e6b6edb11bb09ed527 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Tue, 15 Aug 2023 18:41:43 +1000 Subject: [PATCH 2/9] Fix restoring cloud projects when they are closed --- .../dashboard/components/assetContextMenu.tsx | 1 + .../src/dashboard/components/assetsTable.tsx | 8 ---- .../src/dashboard/components/dashboard.tsx | 43 +++++++++++++------ .../src/dashboard/components/driveView.tsx | 21 +++++++++ .../components/projectNameColumn.tsx | 2 + .../src/dashboard/events/assetListEvent.ts | 8 ---- 6 files changed, 54 insertions(+), 29 deletions(-) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetContextMenu.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetContextMenu.tsx index 9118d8f2b533..b55328a2a816 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetContextMenu.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetContextMenu.tsx @@ -80,6 +80,7 @@ export default function AssetContextMenu(props: AssetContextMenuProps diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetsTable.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetsTable.tsx index a7e12829fe38..991dfec9a473 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetsTable.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetsTable.tsx @@ -758,14 +758,6 @@ export default function AssetsTable(props: AssetsTableProps) { }) break } - case assetListEventModule.AssetListEventType.openProject: { - dispatchAssetEvent({ - type: assetEventModule.AssetEventType.openProject, - id: event.id, - shouldAutomaticallySwitchPage: event.shouldAutomaticallySwitchPage, - }) - break - } case assetListEventModule.AssetListEventType.delete: { setItems(oldItems => oldItems.filter(item => item.id !== event.id)) itemDepthsRef.current.delete(event.id) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx index f6a357095365..e838ed1e131c 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx @@ -4,6 +4,7 @@ import * as React from 'react' import * as common from 'enso-common' +import * as assetEventModule from '../events/assetEvent' import * as assetListEventModule from '../events/assetListEvent' import * as backendModule from '../backend' import * as hooks from '../../hooks' @@ -62,6 +63,7 @@ export default function Dashboard(props: DashboardProps) { const { unsetModal } = modalProvider.useSetModal() const { localStorage } = localStorageProvider.useLocalStorage() const { shortcuts } = shortcutsProvider.useShortcuts() + const [initialized, setInitialized] = React.useState(false) const [query, setQuery] = React.useState('') const [isHelpChatOpen, setIsHelpChatOpen] = React.useState(false) const [isHelpChatVisible, setIsHelpChatVisible] = React.useState(false) @@ -69,6 +71,9 @@ export default function Dashboard(props: DashboardProps) { const [page, setPage] = React.useState( () => localStorage.get(localStorageModule.LocalStorageKey.page) ?? pageSwitcher.Page.drive ) + const [queuedAssetEvents, setQueuedAssetEvents] = React.useState( + [] + ) const [projectStartupInfo, setProjectStartupInfo] = React.useState(null) const [assetListEvents, dispatchAssetListEvent] = @@ -83,6 +88,10 @@ export default function Dashboard(props: DashboardProps) { session.type === authProvider.UserSessionType.offline && backend.type === backendModule.BackendType.remote + React.useEffect(() => { + setInitialized(true) + }, []) + React.useEffect(() => { unsetModal() }, [page, /* should never change */ unsetModal]) @@ -95,11 +104,15 @@ export default function Dashboard(props: DashboardProps) { if (savedProjectStartupInfo.backendType === backendModule.BackendType.remote) { if (session.accessToken != null) { if (backend.type === backendModule.BackendType.remote) { - dispatchAssetListEvent({ - type: assetListEventModule.AssetListEventType.openProject, - id: savedProjectStartupInfo.project.projectId, - shouldAutomaticallySwitchPage: page === pageSwitcher.Page.editor, - }) + // `projectStartupInfo` is still `null`, so the `editor` page will be empty. + setPage(pageSwitcher.Page.drive) + setQueuedAssetEvents([ + { + type: assetEventModule.AssetEventType.openProject, + id: savedProjectStartupInfo.project.projectId, + shouldAutomaticallySwitchPage: page === pageSwitcher.Page.editor, + }, + ]) } else { const httpClient = new http.Client( new Headers([['Authorization', `Bearer ${session.accessToken}`]]) @@ -181,15 +194,18 @@ export default function Dashboard(props: DashboardProps) { }, []) React.useEffect(() => { - if (projectStartupInfo != null) { - localStorage.set( - localStorageModule.LocalStorageKey.projectStartupInfo, - projectStartupInfo - ) - } - return () => { - localStorage.delete(localStorageModule.LocalStorageKey.projectStartupInfo) + if (initialized) { + if (projectStartupInfo != null) { + localStorage.set( + localStorageModule.LocalStorageKey.projectStartupInfo, + projectStartupInfo + ) + } else { + localStorage.delete(localStorageModule.LocalStorageKey.projectStartupInfo) + } } + // `initialized` is NOT a dependency. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [projectStartupInfo, /* should never change */ localStorage]) React.useEffect(() => { @@ -385,6 +401,7 @@ export default function Dashboard(props: DashboardProps) { hidden={page !== pageSwitcher.Page.drive} page={page} initialProjectName={initialProjectName} + queuedAssetEvents={queuedAssetEvents} assetListEvents={assetListEvents} dispatchAssetListEvent={dispatchAssetListEvent} query={query} diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/driveView.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/driveView.tsx index 98403443d513..fa8810c03792 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/driveView.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/driveView.tsx @@ -24,6 +24,9 @@ export interface DriveViewProps { page: pageSwitcher.Page hidden: boolean initialProjectName: string | null + /** These events will be dispatched the next time the assets list is refreshed, rather than + * immediately. */ + queuedAssetEvents: assetEventModule.AssetEvent[] assetListEvents: assetListEventModule.AssetListEvent[] dispatchAssetListEvent: (directoryEvent: assetListEventModule.AssetListEvent) => void query: string @@ -43,6 +46,7 @@ export default function DriveView(props: DriveViewProps) { page, hidden, initialProjectName, + queuedAssetEvents: rawQueuedAssetEvents, query, assetListEvents, dispatchAssetListEvent, @@ -66,6 +70,15 @@ export default function DriveView(props: DriveViewProps) { const [assetEvents, dispatchAssetEvent] = hooks.useEvent() const [nameOfProjectToImmediatelyOpen, setNameOfProjectToImmediatelyOpen] = React.useState(initialProjectName) + const [queuedAssetEvents, setQueuedAssetEvents] = React.useState( + [] + ) + + React.useEffect(() => { + if (rawQueuedAssetEvents.length !== 0) { + setQueuedAssetEvents(oldEvents => [...oldEvents, ...rawQueuedAssetEvents]) + } + }, [rawQueuedAssetEvents]) const assetFilter = React.useMemo(() => { if (query === '') { @@ -111,10 +124,17 @@ export default function DriveView(props: DriveViewProps) { dispatchAssetEvent({ type: assetEventModule.AssetEventType.openProject, id: projectToLoad.id, + shouldAutomaticallySwitchPage: true, }) } setNameOfProjectToImmediatelyOpen(null) } + if (queuedAssetEvents.length !== 0) { + for (const event of queuedAssetEvents) { + dispatchAssetEvent(event) + } + setQueuedAssetEvents([]) + } if (!initialized) { setInitialized(true) if (initialProjectName != null) { @@ -131,6 +151,7 @@ export default function DriveView(props: DriveViewProps) { initialProjectName, logger, nameOfProjectToImmediatelyOpen, + queuedAssetEvents, /* should never change */ setNameOfProjectToImmediatelyOpen, /* should never change */ dispatchAssetEvent, ] diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectNameColumn.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectNameColumn.tsx index 8dccccfcdfc5..6eecb81ace9e 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectNameColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectNameColumn.tsx @@ -101,6 +101,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) { dispatchAssetEvent({ type: assetEventModule.AssetEventType.openProject, id: createdProject.projectId, + shouldAutomaticallySwitchPage: true, }) } catch (error) { dispatchAssetListEvent({ @@ -200,6 +201,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) { dispatchAssetEvent({ type: assetEventModule.AssetEventType.openProject, id: item.id, + shouldAutomaticallySwitchPage: true, }) } else if ( eventModule.isSingleClick(event) && diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/events/assetListEvent.ts b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/events/assetListEvent.ts index 387b7cf3b681..370dab2becc4 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/events/assetListEvent.ts +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/events/assetListEvent.ts @@ -18,7 +18,6 @@ export enum AssetListEventType { newProject = 'new-project', uploadFiles = 'upload-files', newSecret = 'new-secret', - openProject = 'open-project', delete = 'delete', } @@ -33,7 +32,6 @@ interface AssetListEvents { newProject: AssetListNewProjectEvent uploadFiles: AssetListUploadFilesEvent newSecret: AssetListNewSecretEvent - openProject: AssetListOpenProjectEvent delete: AssetListDeleteEvent } @@ -78,12 +76,6 @@ interface AssetListNewSecretEvent extends AssetListBaseEvent { - id: backend.ProjectId - shouldAutomaticallySwitchPage: boolean -} - /** A signal that a file has been deleted. This must not be called before the request is * finished. */ interface AssetListDeleteEvent extends AssetListBaseEvent { From 12283f199c76b120018fe3cf8b07fe0f07da7e63 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Tue, 15 Aug 2023 18:53:08 +1000 Subject: [PATCH 3/9] Fix --- .../src/dashboard/components/dashboard.tsx | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx index e838ed1e131c..8057138198d8 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx @@ -97,13 +97,25 @@ export default function Dashboard(props: DashboardProps) { }, [page, /* should never change */ unsetModal]) React.useEffect(() => { + let currentBackend = backend + if ( + supportsLocalBackend && + session.type !== authProvider.UserSessionType.offline && + localStorage.get(localStorageModule.LocalStorageKey.backendType) === + backendModule.BackendType.local + ) { + currentBackend = new localBackend.LocalBackend( + localStorage.get(localStorageModule.LocalStorageKey.projectStartupInfo) ?? null + ) + setBackend(currentBackend) + } const savedProjectStartupInfo = localStorage.get( localStorageModule.LocalStorageKey.projectStartupInfo ) if (savedProjectStartupInfo != null) { if (savedProjectStartupInfo.backendType === backendModule.BackendType.remote) { if (session.accessToken != null) { - if (backend.type === backendModule.BackendType.remote) { + if (currentBackend.type === backendModule.BackendType.remote) { // `projectStartupInfo` is still `null`, so the `editor` page will be empty. setPage(pageSwitcher.Page.drive) setQueuedAssetEvents([ @@ -142,7 +154,10 @@ export default function Dashboard(props: DashboardProps) { }) nextCheckTimestamp = Number(new Date()) + projectIcon.CHECK_STATUS_INTERVAL_MS - project = await backend.getProjectDetails(projectId, projectName) + project = await remoteBackend.getProjectDetails( + projectId, + projectName + ) } nextCheckTimestamp = 0 while (true) { @@ -153,7 +168,7 @@ export default function Dashboard(props: DashboardProps) { }) nextCheckTimestamp = Number(new Date()) + projectIcon.CHECK_RESOURCES_INTERVAL_MS - await backend.checkResources(projectId, projectName) + await remoteBackend.checkResources(projectId, projectName) break } catch { // Ignored. @@ -224,23 +239,6 @@ export default function Dashboard(props: DashboardProps) { } }, [/* should never change */ unsetModal]) - React.useEffect(() => { - if ( - supportsLocalBackend && - session.type !== authProvider.UserSessionType.offline && - localStorage.get(localStorageModule.LocalStorageKey.backendType) === - backendModule.BackendType.local - ) { - setBackend( - new localBackend.LocalBackend( - localStorage.get(localStorageModule.LocalStorageKey.projectStartupInfo) ?? null - ) - ) - } - // This hook MUST only run once, on mount. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - React.useEffect(() => { // The types come from a third-party API and cannot be changed. // eslint-disable-next-line no-restricted-syntax From f1f37ec8bb3cb7bd834654f8aef0e5cb4c986ca7 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Wed, 16 Aug 2023 16:22:46 +1000 Subject: [PATCH 4/9] Fix opening closed cloud project in background --- .../src/dashboard/components/dashboard.tsx | 33 +++---------------- .../src/dashboard/components/editor.tsx | 28 ++++++++++++++++ 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx index 8057138198d8..d15b9b9e3735 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx @@ -33,13 +33,6 @@ import Templates from './templates' import TheModal from './theModal' import TopBar from './topBar' -// ================= -// === Constants === -// ================= - -/** The `id` attribute of the loading spinner element. */ -const LOADER_ELEMENT_ID = 'loader' - // ================= // === Dashboard === // ================= @@ -126,6 +119,7 @@ export default function Dashboard(props: DashboardProps) { }, ]) } else { + setPage(pageSwitcher.Page.drive) const httpClient = new http.Client( new Headers([['Authorization', `Bearer ${session.accessToken}`]]) ) @@ -175,33 +169,14 @@ export default function Dashboard(props: DashboardProps) { } } setProjectStartupInfo({ ...savedProjectStartupInfo, project }) + if (page === pageSwitcher.Page.editor) { + setPage(page) + } })() } } } else { setProjectStartupInfo(savedProjectStartupInfo) - if (page !== pageSwitcher.Page.editor) { - // A workaround to hide the spinner, when the previous project is being loaded in - // the background. This `MutationObserver` is disconnected when the loader is - // removed from the DOM. - const observer = new MutationObserver(mutations => { - for (const mutation of mutations) { - for (const node of Array.from(mutation.addedNodes)) { - if (node instanceof HTMLElement && node.id === LOADER_ELEMENT_ID) { - document.body.style.cursor = 'auto' - node.style.display = 'none' - } - } - for (const node of Array.from(mutation.removedNodes)) { - if (node instanceof HTMLElement && node.id === LOADER_ELEMENT_ID) { - document.body.style.cursor = 'auto' - observer.disconnect() - } - } - } - }) - observer.observe(document.body, { childList: true }) - } } } // This MUST only run when the component is mounted. diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/editor.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/editor.tsx index 01c97b2ce923..c5c7cb37c4f1 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/editor.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/editor.tsx @@ -11,6 +11,9 @@ import GLOBAL_CONFIG from '../../../../../../../../gui/config.yaml' assert { typ // === Constants === // ================= +/** The `id` attribute of the loading spinner element created by the wasm entrypoint. */ +const LOADER_ELEMENT_ID = 'loader' + /** The horizontal offset of the editor's top bar from the left edge of the window. */ const TOP_BAR_X_OFFSET_PX = 96 /** The `id` attribute of the element into which the IDE will be rendered. */ @@ -50,6 +53,31 @@ export default function Editor(props: EditorProps) { } }, [visible]) + React.useEffect(() => { + if (projectStartupInfo != null && !visible) { + // A workaround to hide the spinner, when the previous project is being loaded in + // the background. This `MutationObserver` is disconnected when the loader is + // removed from the DOM. + const observer = new MutationObserver(mutations => { + for (const mutation of mutations) { + for (const node of Array.from(mutation.addedNodes)) { + if (node instanceof HTMLElement && node.id === LOADER_ELEMENT_ID) { + document.body.style.cursor = 'auto' + node.style.display = 'none' + } + } + for (const node of Array.from(mutation.removedNodes)) { + if (node instanceof HTMLElement && node.id === LOADER_ELEMENT_ID) { + document.body.style.cursor = 'auto' + observer.disconnect() + } + } + } + }) + observer.observe(document.body, { childList: true }) + } + }, [projectStartupInfo, visible]) + let hasEffectRun = false React.useEffect(() => { From 8f6484ca6db902cc2322191dfcdf530ba24f7425 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Fri, 18 Aug 2023 21:07:42 +1000 Subject: [PATCH 5/9] Open cloud project in background even if not on editor page --- .../src/dashboard/components/assetsTable.tsx | 4 ++-- .../authentication/src/dashboard/components/dashboard.tsx | 6 ++++-- .../authentication/src/dashboard/components/driveView.tsx | 2 +- .../src/dashboard/components/projectIcon.tsx | 8 ++++---- .../src/dashboard/components/projectNameColumn.tsx | 4 ++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetsTable.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetsTable.tsx index 991dfec9a473..4c40c21102db 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetsTable.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetsTable.tsx @@ -316,7 +316,7 @@ export interface AssetsTableState { ) => void /** Called when the project is opened via the {@link ProjectActionButton}. */ doOpenManually: (projectId: backendModule.ProjectId) => void - doOpenIde: (project: backendModule.ProjectAsset) => void + doOpenIde: (project: backendModule.ProjectAsset, switchPage: boolean) => void doCloseIde: () => void } @@ -344,7 +344,7 @@ export interface AssetsTableProps { dispatchAssetEvent: (event: assetEventModule.AssetEvent) => void assetListEvents: assetListEventModule.AssetListEvent[] dispatchAssetListEvent: (event: assetListEventModule.AssetListEvent) => void - doOpenIde: (project: backendModule.ProjectAsset) => void + doOpenIde: (project: backendModule.ProjectAsset, switchPage: boolean) => void doCloseIde: () => void } diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx index d15b9b9e3735..61da0d9bc470 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx @@ -289,8 +289,10 @@ export default function Dashboard(props: DashboardProps) { ) const openEditor = React.useCallback( - async (newProject: backendModule.ProjectAsset) => { - setPage(pageSwitcher.Page.editor) + async (newProject: backendModule.ProjectAsset, switchPage: boolean) => { + if (switchPage) { + setPage(pageSwitcher.Page.editor) + } if (projectStartupInfo?.project.projectId !== newProject.id) { setProjectStartupInfo({ project: await backend.getProjectDetails(newProject.id, newProject.title), diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/driveView.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/driveView.tsx index fa8810c03792..d657886d170e 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/driveView.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/driveView.tsx @@ -31,7 +31,7 @@ export interface DriveViewProps { dispatchAssetListEvent: (directoryEvent: assetListEventModule.AssetListEvent) => void query: string doCreateProject: (templateId: string | null) => void - doOpenEditor: (project: backendModule.ProjectAsset) => void + doOpenEditor: (project: backendModule.ProjectAsset, switchPage: boolean) => void doCloseEditor: () => void appRunner: AppRunner | null loadingProjectManagerDidFail: boolean diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectIcon.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectIcon.tsx index 79a72fe9fc02..de0155728e9f 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectIcon.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectIcon.tsx @@ -82,7 +82,7 @@ export interface ProjectIconProps { doOpenManually: (projectId: backendModule.ProjectId) => void onClose: () => void appRunner: AppRunner | null - openIde: () => void + openIde: (switchPage: boolean) => void } /** An interactive icon indicating the status of a project. */ @@ -246,8 +246,8 @@ export default function ProjectIcon(props: ProjectIconProps) { }) React.useEffect(() => { - if (shouldOpenWhenReady && state === backendModule.ProjectState.opened) { - openIde() + if (state === backendModule.ProjectState.opened) { + openIde(shouldOpenWhenReady) setShouldOpenWhenReady(false) } }, [shouldOpenWhenReady, state, openIde]) @@ -427,7 +427,7 @@ export default function ProjectIcon(props: ProjectIconProps) { onClick={clickEvent => { clickEvent.stopPropagation() unsetModal() - openIde() + openIde(true) }} > diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectNameColumn.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectNameColumn.tsx index 6eecb81ace9e..138392e27faf 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectNameColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectNameColumn.tsx @@ -222,8 +222,8 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) { assetEvents={assetEvents} doOpenManually={doOpenManually} appRunner={appRunner} - openIde={() => { - doOpenIde(item) + openIde={(switchPage: boolean) => { + doOpenIde(item, switchPage) }} onClose={doCloseIde} /> From 62772f05e5726faf68a809ee8d2368a7b5b2eb68 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Fri, 18 Aug 2023 21:17:18 +1000 Subject: [PATCH 6/9] Fix logic for opening projects --- .../src/dashboard/components/projectIcon.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectIcon.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectIcon.tsx index de0155728e9f..cd9a37b9a6de 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectIcon.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectIcon.tsx @@ -120,6 +120,7 @@ export default function ProjectIcon(props: ProjectIconProps) { ((state: spinner.SpinnerState | null) => void) | null >(null) const [shouldOpenWhenReady, setShouldOpenWhenReady] = React.useState(false) + const [shouldSwitchPage, setShouldSwitchPage] = React.useState(false) const [toastId, setToastId] = React.useState(null) const openProject = React.useCallback(async () => { @@ -221,7 +222,8 @@ export default function ProjectIcon(props: ProjectIconProps) { setShouldOpenWhenReady(false) void closeProject(false) } else { - setShouldOpenWhenReady(event.shouldAutomaticallySwitchPage) + setShouldOpenWhenReady(true) + setShouldSwitchPage(event.shouldAutomaticallySwitchPage) void openProject() } break @@ -246,11 +248,11 @@ export default function ProjectIcon(props: ProjectIconProps) { }) React.useEffect(() => { - if (state === backendModule.ProjectState.opened) { - openIde(shouldOpenWhenReady) + if (shouldOpenWhenReady && state === backendModule.ProjectState.opened) { + openIde(shouldSwitchPage) setShouldOpenWhenReady(false) } - }, [shouldOpenWhenReady, state, openIde]) + }, [shouldOpenWhenReady, shouldSwitchPage, state, openIde]) React.useEffect(() => { switch (checkState) { From e29f63a7c0dc9f1a70c502ba9bec9059fbb8c2bf Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Fri, 25 Aug 2023 16:57:48 +1000 Subject: [PATCH 7/9] Fix opening cloud project that is not at root --- .../dashboard/src/authentication/src/dashboard/backend.ts | 1 + .../authentication/src/dashboard/components/dashboard.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/backend.ts b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/backend.ts index ee41b504fc4d..3ca78504e5a9 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/backend.ts +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/backend.ts @@ -162,6 +162,7 @@ export interface Project extends ListedProject { /** Information required to open a project. */ export interface ProjectStartupInfo { project: Project + projectAsset: ProjectAsset backendType: BackendType accessToken: string | null } diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx index cb319580d3bf..f91cb968bfd0 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx @@ -113,7 +113,11 @@ export default function Dashboard(props: DashboardProps) { if (savedProjectStartupInfo != null) { if (savedProjectStartupInfo.backendType === backendModule.BackendType.remote) { if (session.accessToken != null) { - if (currentBackend.type === backendModule.BackendType.remote) { + if ( + currentBackend.type === backendModule.BackendType.remote && + savedProjectStartupInfo.projectAsset.parentId === + backend.rootDirectoryId(session.organization) + ) { // `projectStartupInfo` is still `null`, so the `editor` page will be empty. setPage(pageSwitcher.Page.drive) setQueuedAssetEvents([ @@ -301,6 +305,7 @@ export default function Dashboard(props: DashboardProps) { if (projectStartupInfo?.project.projectId !== newProject.id) { setProjectStartupInfo({ project: await backend.getProjectDetails(newProject.id, newProject.title), + projectAsset: newProject, backendType: backend.type, accessToken: session.accessToken, }) From c772247d40a4bcb81dabc4d1ada2df4fad90ed6f Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Fri, 25 Aug 2023 17:57:29 +1000 Subject: [PATCH 8/9] Fix race condition causing cloud projects to not open --- app/ide-desktop/lib/client/watch.ts | 2 +- .../src/dashboard/components/assetsTable.tsx | 29 +- .../src/dashboard/components/dashboard.tsx | 73 +++-- .../src/dashboard/components/driveView.tsx | 5 +- .../src/dashboard/components/projectIcon.tsx | 140 ++------- .../src/dashboard/remoteBackend.ts | 268 ++++++++++++------ 6 files changed, 252 insertions(+), 265 deletions(-) diff --git a/app/ide-desktop/lib/client/watch.ts b/app/ide-desktop/lib/client/watch.ts index 15370fcd7186..dcce5893e581 100644 --- a/app/ide-desktop/lib/client/watch.ts +++ b/app/ide-desktop/lib/client/watch.ts @@ -97,7 +97,7 @@ const ALL_BUNDLES_READY = new Promise((resolve, reject) => { console.log('Bundling content.') const contentOpts = contentBundler.bundlerOptionsFromEnv({ - devMode: true, + devMode: process.env.DEV_MODE !== 'false', supportsLocalBackend: true, supportsDeepLinks: false, }) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetsTable.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetsTable.tsx index ccaa7d74d73c..a28fecd7960f 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetsTable.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/assetsTable.tsx @@ -113,9 +113,9 @@ export interface AssetsTableState { setSortColumn: (column: columnModule.SortableColumn | null) => void sortDirection: sorting.SortDirection | null setSortDirection: (sortDirection: sorting.SortDirection | null) => void + dispatchAssetListEvent: (event: assetListEventModule.AssetListEvent) => void assetEvents: assetEventModule.AssetEvent[] dispatchAssetEvent: (event: assetEventModule.AssetEvent) => void - dispatchAssetListEvent: (event: assetListEventModule.AssetListEvent) => void doToggleDirectoryExpansion: ( directoryId: backendModule.DirectoryId, key: backendModule.AssetId, @@ -149,10 +149,10 @@ export interface AssetsTableProps { /** These events will be dispatched the next time the assets list is refreshed, rather than * immediately. */ queuedAssetEvents: assetEventModule.AssetEvent[] - assetEvents: assetEventModule.AssetEvent[] - dispatchAssetEvent: (event: assetEventModule.AssetEvent) => void assetListEvents: assetListEventModule.AssetListEvent[] dispatchAssetListEvent: (event: assetListEventModule.AssetListEvent) => void + assetEvents: assetEventModule.AssetEvent[] + dispatchAssetEvent: (event: assetEventModule.AssetEvent) => void doOpenIde: (project: backendModule.ProjectAsset, switchPage: boolean) => void doCloseIde: () => void loadingProjectManagerDidFail: boolean @@ -168,10 +168,10 @@ export default function AssetsTable(props: AssetsTableProps) { query, initialProjectName, queuedAssetEvents: rawQueuedAssetEvents, - assetEvents, - dispatchAssetEvent, assetListEvents, dispatchAssetListEvent, + assetEvents, + dispatchAssetEvent, doOpenIde, doCloseIde: rawDoCloseIde, loadingProjectManagerDidFail, @@ -193,9 +193,7 @@ export default function AssetsTable(props: AssetsTableProps) { const [sortColumn, setSortColumn] = React.useState(null) const [sortDirection, setSortDirection] = React.useState(null) const [selectedKeys, setSelectedKeys] = React.useState(() => new Set()) - const [queuedAssetEvents, setQueuedAssetEvents] = React.useState( - [] - ) + const [, setQueuedAssetEvents] = React.useState([]) const [nameOfProjectToImmediatelyOpen, setNameOfProjectToImmediatelyOpen] = React.useState(initialProjectName) const nodeMap = React.useMemo( @@ -291,12 +289,16 @@ export default function AssetsTable(props: AssetsTableProps) { } setNameOfProjectToImmediatelyOpen(null) } - if (queuedAssetEvents.length !== 0) { - for (const event of queuedAssetEvents) { - dispatchAssetEvent(event) + setQueuedAssetEvents(oldQueuedAssetEvents => { + if (oldQueuedAssetEvents.length !== 0) { + window.setTimeout(() => { + for (const event of oldQueuedAssetEvents) { + dispatchAssetEvent(event) + } + }, 0) } - setQueuedAssetEvents([]) - } + return [] + }) if (!initialized) { setInitialized(true) if (initialProjectName != null) { @@ -313,7 +315,6 @@ export default function AssetsTable(props: AssetsTableProps) { initialProjectName, logger, nameOfProjectToImmediatelyOpen, - queuedAssetEvents, /* should never change */ setNameOfProjectToImmediatelyOpen, /* should never change */ dispatchAssetEvent, ] diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx index f91cb968bfd0..26e50c5743df 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx @@ -24,7 +24,6 @@ import * as shortcutsProvider from '../../providers/shortcuts' import * as app from '../../components/app' import * as pageSwitcher from './pageSwitcher' -import * as projectIcon from './projectIcon' import * as spinner from './spinner' import Chat, * as chat from './chat' import DriveView from './driveView' @@ -69,8 +68,11 @@ export default function Dashboard(props: DashboardProps) { ) const [projectStartupInfo, setProjectStartupInfo] = React.useState(null) + const [openProjectAbortController, setOpenProjectAbortController] = + React.useState(null) const [assetListEvents, dispatchAssetListEvent] = hooks.useEvent() + const [assetEvents, dispatchAssetEvent] = hooks.useEvent() const isListingLocalDirectoryAndWillFail = backend.type === backendModule.BackendType.local && loadingProjectManagerDidFail @@ -137,50 +139,23 @@ export default function Dashboard(props: DashboardProps) { logger ) void (async () => { - const projectId = savedProjectStartupInfo.project.projectId - const projectName = savedProjectStartupInfo.project.packageName - let project = await remoteBackend.getProjectDetails( - projectId, - projectName + const abortController = new AbortController() + setOpenProjectAbortController(abortController) + await remoteBackendModule.waitUntilProjectIsReady( + remoteBackend, + savedProjectStartupInfo.projectAsset, + abortController ) - if ( - project.state.type !== backendModule.ProjectState.openInProgress && - project.state.type !== backendModule.ProjectState.opened - ) { - await remoteBackend.openProject(projectId, null, projectName) - } - let nextCheckTimestamp = 0 - while (project.state.type !== backendModule.ProjectState.opened) { - await new Promise(resolve => { - const delayMs = nextCheckTimestamp - Number(new Date()) - setTimeout(resolve, Math.max(0, delayMs)) - }) - nextCheckTimestamp = - Number(new Date()) + projectIcon.CHECK_STATUS_INTERVAL_MS - project = await remoteBackend.getProjectDetails( - projectId, - projectName + if (!abortController.signal.aborted) { + const project = await remoteBackend.getProjectDetails( + savedProjectStartupInfo.projectAsset.id, + savedProjectStartupInfo.projectAsset.title ) - } - nextCheckTimestamp = 0 - while (true) { - try { - await new Promise(resolve => { - const delayMs = nextCheckTimestamp - Number(new Date()) - setTimeout(resolve, Math.max(0, delayMs)) - }) - nextCheckTimestamp = - Number(new Date()) + projectIcon.CHECK_RESOURCES_INTERVAL_MS - await remoteBackend.checkResources(projectId, projectName) - break - } catch { - // Ignored. + setProjectStartupInfo({ ...savedProjectStartupInfo, project }) + if (page === pageSwitcher.Page.editor) { + setPage(page) } } - setProjectStartupInfo({ ...savedProjectStartupInfo, project }) - if (page === pageSwitcher.Page.editor) { - setPage(page) - } })() } } @@ -192,6 +167,20 @@ export default function Dashboard(props: DashboardProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + hooks.useEventHandler(assetEvents, event => { + switch (event.type) { + case assetEventModule.AssetEventType.openProject: { + openProjectAbortController?.abort() + setOpenProjectAbortController(null) + break + } + default: { + // Ignored. + break + } + } + }) + React.useEffect(() => { if (initialized) { if (projectStartupInfo != null) { @@ -389,6 +378,8 @@ export default function Dashboard(props: DashboardProps) { queuedAssetEvents={queuedAssetEvents} assetListEvents={assetListEvents} dispatchAssetListEvent={dispatchAssetListEvent} + assetEvents={assetEvents} + dispatchAssetEvent={dispatchAssetEvent} query={query} doCreateProject={doCreateProject} doOpenEditor={openEditor} diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/driveView.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/driveView.tsx index 8074e12d8200..67691ed78f99 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/driveView.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/driveView.tsx @@ -27,6 +27,8 @@ export interface DriveViewProps { queuedAssetEvents: assetEventModule.AssetEvent[] assetListEvents: assetListEventModule.AssetListEvent[] dispatchAssetListEvent: (directoryEvent: assetListEventModule.AssetListEvent) => void + assetEvents: assetEventModule.AssetEvent[] + dispatchAssetEvent: (directoryEvent: assetEventModule.AssetEvent) => void query: string doCreateProject: (templateId: string | null) => void doOpenEditor: (project: backendModule.ProjectAsset, switchPage: boolean) => void @@ -48,6 +50,8 @@ export default function DriveView(props: DriveViewProps) { query, assetListEvents, dispatchAssetListEvent, + assetEvents, + dispatchAssetEvent, doCreateProject, doOpenEditor, doCloseEditor, @@ -61,7 +65,6 @@ export default function DriveView(props: DriveViewProps) { const { backend } = backendProvider.useBackend() const toastAndLog = hooks.useToastAndLog() const [isFileBeingDragged, setIsFileBeingDragged] = React.useState(false) - const [assetEvents, dispatchAssetEvent] = hooks.useEvent() React.useEffect(() => { const onBlur = () => { diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectIcon.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectIcon.tsx index cd9a37b9a6de..547244e5c9e9 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectIcon.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectIcon.tsx @@ -11,6 +11,7 @@ import * as backendModule from '../backend' import * as backendProvider from '../../providers/backend' import * as hooks from '../../hooks' import * as modalProvider from '../../providers/modal' +import * as remoteBackend from '../remoteBackend' import Spinner, * as spinner from './spinner' import SvgMask from '../../authentication/components/svgMask' @@ -21,10 +22,6 @@ import SvgMask from '../../authentication/components/svgMask' const LOADING_MESSAGE = 'Your environment is being created. It will take some time, please be patient.' -/** The interval between requests checking whether the IDE is ready. */ -export const CHECK_STATUS_INTERVAL_MS = 5000 -/** The interval between requests checking whether the VM is ready. */ -export const CHECK_RESOURCES_INTERVAL_MS = 1000 /** The corresponding {@link SpinnerState} for each {@link backendModule.ProjectState}, * when using the remote backend. */ const REMOTE_SPINNER_STATE: Record = { @@ -48,26 +45,6 @@ const LOCAL_SPINNER_STATE: Record void) | null @@ -122,12 +98,14 @@ export default function ProjectIcon(props: ProjectIconProps) { const [shouldOpenWhenReady, setShouldOpenWhenReady] = React.useState(false) const [shouldSwitchPage, setShouldSwitchPage] = React.useState(false) const [toastId, setToastId] = React.useState(null) + const [openProjectAbortController, setOpenProjectAbortController] = + React.useState(null) const openProject = React.useCallback(async () => { setState(backendModule.ProjectState.openInProgress) try { switch (backend.type) { - case backendModule.BackendType.remote: + case backendModule.BackendType.remote: { if ( state !== backendModule.ProjectState.openInProgress && state !== backendModule.ProjectState.opened @@ -135,10 +113,19 @@ export default function ProjectIcon(props: ProjectIconProps) { setToastId(toast.toast.loading(LOADING_MESSAGE)) await backend.openProject(item.id, null, item.title) } - setCheckState(CheckState.checkingStatus) + const abortController = new AbortController() + setOpenProjectAbortController(abortController) + await remoteBackend.waitUntilProjectIsReady(backend, item, abortController) + if (!abortController.signal.aborted) { + setState(oldState => + oldState === backendModule.ProjectState.openInProgress + ? backendModule.ProjectState.opened + : oldState + ) + } break - case backendModule.BackendType.local: - setCheckState(CheckState.localProject) + } + case backendModule.BackendType.local: { await backend.openProject(item.id, null, item.title) setState(oldState => oldState === backendModule.ProjectState.openInProgress @@ -146,17 +133,16 @@ export default function ProjectIcon(props: ProjectIconProps) { : oldState ) break + } } } catch (error) { - setCheckState(CheckState.notChecking) toastAndLog(`Could not open project '${item.title}'`, error) setState(backendModule.ProjectState.closed) } }, [ state, backend, - item.id, - item.title, + item, /* should never change */ toastAndLog, /* should never change */ setState, ]) @@ -232,7 +218,8 @@ export default function ProjectIcon(props: ProjectIconProps) { setShouldOpenWhenReady(false) onSpinnerStateChange?.(null) setOnSpinnerStateChange(null) - setCheckState(CheckState.notChecking) + openProjectAbortController?.abort() + setOpenProjectAbortController(null) void closeProject(false) break } @@ -254,90 +241,6 @@ export default function ProjectIcon(props: ProjectIconProps) { } }, [shouldOpenWhenReady, shouldSwitchPage, state, openIde]) - React.useEffect(() => { - switch (checkState) { - case CheckState.notChecking: - case CheckState.localProject: - case CheckState.done: { - return - } - case CheckState.checkingStatus: { - let handle: number | null = null - let continuePolling = true - let previousTimestamp = 0 - const checkProjectStatus = async () => { - try { - const response = await backend.getProjectDetails(item.id, item.title) - handle = null - if ( - continuePolling && - response.state.type === backendModule.ProjectState.opened - ) { - continuePolling = false - setCheckState(CheckState.checkingResources) - } - } finally { - if (continuePolling) { - const nowTimestamp = Number(new Date()) - const delay = - CHECK_STATUS_INTERVAL_MS - (nowTimestamp - previousTimestamp) - previousTimestamp = nowTimestamp - handle = window.setTimeout( - () => void checkProjectStatus(), - Math.max(0, delay) - ) - } - } - } - void checkProjectStatus() - return () => { - continuePolling = false - if (handle != null) { - window.clearTimeout(handle) - } - } - } - case CheckState.checkingResources: { - let handle: number | null = null - let continuePolling = true - let previousTimestamp = 0 - const checkProjectResources = async () => { - try { - // This call will error if the VM is not ready yet. - await backend.checkResources(item.id, item.title) - setToastId(null) - handle = null - if (continuePolling) { - continuePolling = false - setState(backendModule.ProjectState.opened) - setCheckState(CheckState.done) - } - } catch { - if (continuePolling) { - const nowTimestamp = Number(new Date()) - const delay = - CHECK_RESOURCES_INTERVAL_MS - (nowTimestamp - previousTimestamp) - previousTimestamp = nowTimestamp - handle = window.setTimeout( - () => void checkProjectResources(), - Math.max(0, delay) - ) - } - } - } - void checkProjectResources() - return () => { - continuePolling = false - if (handle != null) { - window.clearTimeout(handle) - } - } - } - } - // `backend` is NOT a dependency as an asset belongs to a specific backend. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [checkState, item.id, item.title, /* should never change */ setState]) - const closeProject = async (triggerOnClose = true) => { setToastId(null) setShouldOpenWhenReady(false) @@ -345,7 +248,8 @@ export default function ProjectIcon(props: ProjectIconProps) { onSpinnerStateChange?.(null) setOnSpinnerStateChange(null) appRunner?.stopApp() - setCheckState(CheckState.notChecking) + openProjectAbortController?.abort() + setOpenProjectAbortController(null) if ( state !== backendModule.ProjectState.closing && state !== backendModule.ProjectState.closed diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/remoteBackend.ts b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/remoteBackend.ts index 0717cde7a63b..e181a04bb837 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/remoteBackend.ts +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/remoteBackend.ts @@ -3,7 +3,7 @@ * Each exported function in the {@link RemoteBackend} in this module corresponds to * an API endpoint. The functions are asynchronous and return a {@link Promise} that resolves to * the response from the API. */ -import * as backend from './backend' +import * as backendModule from './backend' import * as config from '../config' import * as errorModule from '../error' import * as http from '../http' @@ -21,19 +21,69 @@ const STATUS_SUCCESS_LAST = 299 const STATUS_SERVER_ERROR = 500 /** Default HTTP body for an "open project" request. */ -const DEFAULT_OPEN_PROJECT_BODY: backend.OpenProjectRequestBody = { +const DEFAULT_OPEN_PROJECT_BODY: backendModule.OpenProjectRequestBody = { forceCreate: false, } -// ======================== -// === Helper functions === -// ======================== +// ============================ +// === responseIsSuccessful === +// ============================ -/** Returns true if and only if a response has a success HTTP status code (200-299). */ +/** Whether a response has a success HTTP status code (200-299). */ function responseIsSuccessful(response: Response) { return response.status >= STATUS_SUCCESS_FIRST && response.status <= STATUS_SUCCESS_LAST } +// =============================== +// === waitUntilProjectIsReady === +// =============================== + +/** The interval between requests checking whether the IDE is ready. */ +const CHECK_STATUS_INTERVAL_MS = 5000 +/** The interval between requests checking whether the VM is ready. */ +const CHECK_RESOURCES_INTERVAL_MS = 1000 + +/** Return a {@link Promise} that resolves only when a project is ready to open. */ +export async function waitUntilProjectIsReady( + backend: backendModule.Backend, + item: backendModule.ProjectAsset, + abortController: AbortController = new AbortController() +) { + let project = await backend.getProjectDetails(item.id, item.title) + if ( + project.state.type !== backendModule.ProjectState.openInProgress && + project.state.type !== backendModule.ProjectState.opened + ) { + await backend.openProject(item.id, null, item.title) + } + let nextCheckTimestamp = 0 + while ( + !abortController.signal.aborted && + project.state.type !== backendModule.ProjectState.opened + ) { + await new Promise(resolve => { + const delayMs = nextCheckTimestamp - Number(new Date()) + setTimeout(resolve, Math.max(0, delayMs)) + }) + nextCheckTimestamp = Number(new Date()) + CHECK_STATUS_INTERVAL_MS + project = await backend.getProjectDetails(item.id, item.title) + } + nextCheckTimestamp = 0 + while (!abortController.signal.aborted) { + try { + await new Promise(resolve => { + const delayMs = nextCheckTimestamp - Number(new Date()) + setTimeout(resolve, Math.max(0, delayMs)) + }) + nextCheckTimestamp = Number(new Date()) + CHECK_RESOURCES_INTERVAL_MS + await backend.checkResources(item.id, item.title) + break + } catch { + // Ignored. + } + } +} + // ============= // === Paths === // ============= @@ -71,51 +121,51 @@ const LIST_TAGS_PATH = 'tags' /** Relative HTTP path to the "list versions" endpoint of the Cloud backend API. */ const LIST_VERSIONS_PATH = 'versions' /** Relative HTTP path to the "update directory" endpoint of the Cloud backend API. */ -function updateDirectoryPath(directoryId: backend.DirectoryId) { +function updateDirectoryPath(directoryId: backendModule.DirectoryId) { return `directories/${directoryId}` } /** Relative HTTP path to the "delete directory" endpoint of the Cloud backend API. */ -function deleteDirectoryPath(directoryId: backend.DirectoryId) { +function deleteDirectoryPath(directoryId: backendModule.DirectoryId) { return `directories/${directoryId}` } /** Relative HTTP path to the "close project" endpoint of the Cloud backend API. */ -function closeProjectPath(projectId: backend.ProjectId) { +function closeProjectPath(projectId: backendModule.ProjectId) { return `projects/${projectId}/close` } /** Relative HTTP path to the "get project details" endpoint of the Cloud backend API. */ -function getProjectDetailsPath(projectId: backend.ProjectId) { +function getProjectDetailsPath(projectId: backendModule.ProjectId) { return `projects/${projectId}` } /** Relative HTTP path to the "open project" endpoint of the Cloud backend API. */ -function openProjectPath(projectId: backend.ProjectId) { +function openProjectPath(projectId: backendModule.ProjectId) { return `projects/${projectId}/open` } /** Relative HTTP path to the "project update" endpoint of the Cloud backend API. */ -function projectUpdatePath(projectId: backend.ProjectId) { +function projectUpdatePath(projectId: backendModule.ProjectId) { return `projects/${projectId}` } /** Relative HTTP path to the "delete project" endpoint of the Cloud backend API. */ -function deleteProjectPath(projectId: backend.ProjectId) { +function deleteProjectPath(projectId: backendModule.ProjectId) { return `projects/${projectId}` } /** Relative HTTP path to the "check resources" endpoint of the Cloud backend API. */ -function checkResourcesPath(projectId: backend.ProjectId) { +function checkResourcesPath(projectId: backendModule.ProjectId) { return `projects/${projectId}/resources` } /** Relative HTTP path to the "delete file" endpoint of the Cloud backend API. */ -function deleteFilePath(fileId: backend.FileId) { +function deleteFilePath(fileId: backendModule.FileId) { return `files/${fileId}` } /** Relative HTTP path to the "get project" endpoint of the Cloud backend API. */ -function getSecretPath(secretId: backend.SecretId) { +function getSecretPath(secretId: backendModule.SecretId) { return `secrets/${secretId}` } /** Relative HTTP path to the "delete secret" endpoint of the Cloud backend API. */ -function deleteSecretPath(secretId: backend.SecretId) { +function deleteSecretPath(secretId: backendModule.SecretId) { return `secrets/${secretId}` } /** Relative HTTP path to the "delete tag" endpoint of the Cloud backend API. */ -function deleteTagPath(tagId: backend.TagId) { +function deleteTagPath(tagId: backendModule.TagId) { return `secrets/${tagId}` } @@ -125,37 +175,37 @@ function deleteTagPath(tagId: backend.TagId) { /** HTTP response body for the "list users" endpoint. */ interface ListUsersResponseBody { - users: backend.SimpleUser[] + users: backendModule.SimpleUser[] } /** HTTP response body for the "list projects" endpoint. */ interface ListDirectoryResponseBody { - assets: backend.AnyAsset[] + assets: backendModule.AnyAsset[] } /** HTTP response body for the "list projects" endpoint. */ interface ListProjectsResponseBody { - projects: backend.ListedProjectRaw[] + projects: backendModule.ListedProjectRaw[] } /** HTTP response body for the "list files" endpoint. */ interface ListFilesResponseBody { - files: backend.File[] + files: backendModule.File[] } /** HTTP response body for the "list secrets" endpoint. */ interface ListSecretsResponseBody { - secrets: backend.SecretInfo[] + secrets: backendModule.SecretInfo[] } /** HTTP response body for the "list tag" endpoint. */ interface ListTagsResponseBody { - tags: backend.Tag[] + tags: backendModule.Tag[] } /** HTTP response body for the "list versions" endpoint. */ interface ListVersionsResponseBody { - versions: [backend.Version, ...backend.Version[]] + versions: [backendModule.Version, ...backendModule.Version[]] } // ===================== @@ -163,8 +213,8 @@ interface ListVersionsResponseBody { // ===================== /** Class for sending requests to the Cloud backend API endpoints. */ -export class RemoteBackend extends backend.Backend { - readonly type = backend.BackendType.remote +export class RemoteBackend extends backendModule.Backend { + readonly type = backendModule.BackendType.remote /** Create a new instance of the {@link RemoteBackend} API client. * @@ -196,16 +246,20 @@ export class RemoteBackend extends backend.Backend { } /** Return the root directory id for the given user. */ - override rootDirectoryId(user: backend.UserOrOrganization | null): backend.DirectoryId { - return backend.DirectoryId( + override rootDirectoryId( + user: backendModule.UserOrOrganization | null + ): backendModule.DirectoryId { + return backendModule.DirectoryId( // `user` is only null when the user is offline, in which case the remote backend cannot // be accessed anyway. - user != null ? user.id.replace(/^organization-/, `${backend.AssetType.directory}-`) : '' + user != null + ? user.id.replace(/^organization-/, `${backendModule.AssetType.directory}-`) + : '' ) } /** Return a list of all users in the same organization. */ - async listUsers(): Promise { + async listUsers(): Promise { const response = await this.get(LIST_USERS_PATH) if (!responseIsSuccessful(response)) { return this.throw(`Unable to list users in the organization.`) @@ -215,8 +269,10 @@ export class RemoteBackend extends backend.Backend { } /** Set the username and parent organization of the current user. */ - async createUser(body: backend.CreateUserRequestBody): Promise { - const response = await this.post(CREATE_USER_PATH, body) + async createUser( + body: backendModule.CreateUserRequestBody + ): Promise { + const response = await this.post(CREATE_USER_PATH, body) if (!responseIsSuccessful(response)) { return this.throw('Unable to create user.') } else { @@ -225,7 +281,7 @@ export class RemoteBackend extends backend.Backend { } /** Invite a new user to the organization by email. */ - async inviteUser(body: backend.InviteUserRequestBody): Promise { + async inviteUser(body: backendModule.InviteUserRequestBody): Promise { const response = await this.post(INVITE_USER_PATH, body) if (!responseIsSuccessful(response)) { return this.throw(`Unable to invite user '${body.userEmail}'.`) @@ -235,8 +291,11 @@ export class RemoteBackend extends backend.Backend { } /** Adds a permission for a specific user on a specific asset. */ - async createPermission(body: backend.CreatePermissionRequestBody): Promise { - const response = await this.post(CREATE_PERMISSION_PATH, body) + async createPermission(body: backendModule.CreatePermissionRequestBody): Promise { + const response = await this.post( + CREATE_PERMISSION_PATH, + body + ) if (!responseIsSuccessful(response)) { return this.throw(`Unable to set permissions.`) } else { @@ -247,8 +306,8 @@ export class RemoteBackend extends backend.Backend { /** Return organization info for the current user. * * @returns `null` if a non-successful status code (not 200-299) was received. */ - async usersMe(): Promise { - const response = await this.get(USERS_ME_PATH) + async usersMe(): Promise { + const response = await this.get(USERS_ME_PATH) if (!responseIsSuccessful(response)) { return null } else { @@ -260,9 +319,9 @@ export class RemoteBackend extends backend.Backend { * * @throws An error if a non-successful status code (not 200-299) was received. */ async listDirectory( - query: backend.ListDirectoryRequestParams, + query: backendModule.ListDirectoryRequestParams, title: string | null - ): Promise { + ): Promise { const response = await this.get( LIST_DIRECTORY_PATH + '?' + @@ -291,17 +350,25 @@ export class RemoteBackend extends backend.Backend { // This type assertion is safe; it is only needed to convert `type` to a // newtype. // eslint-disable-next-line no-restricted-syntax - ({ ...asset, type: asset.id.match(/^(.+?)-/)?.[1] } as backend.AnyAsset) + ({ + ...asset, + type: asset.id.match(/^(.+?)-/)?.[1], + } as backendModule.AnyAsset) ) .map(asset => - asset.type === backend.AssetType.project && - asset.projectState.type === backend.ProjectState.opened - ? { ...asset, projectState: { type: backend.ProjectState.openInProgress } } + asset.type === backendModule.AssetType.project && + asset.projectState.type === backendModule.ProjectState.opened + ? { + ...asset, + projectState: { type: backendModule.ProjectState.openInProgress }, + } : asset ) .map(asset => ({ ...asset, - permissions: (asset.permissions ?? []).sort(backend.compareUserPermissions), + permissions: (asset.permissions ?? []).sort( + backendModule.compareUserPermissions + ), })) } } @@ -310,9 +377,12 @@ export class RemoteBackend extends backend.Backend { * * @throws An error if a non-successful status code (not 200-299) was received. */ async createDirectory( - body: backend.CreateDirectoryRequestBody - ): Promise { - const response = await this.post(CREATE_DIRECTORY_PATH, body) + body: backendModule.CreateDirectoryRequestBody + ): Promise { + const response = await this.post( + CREATE_DIRECTORY_PATH, + body + ) if (!responseIsSuccessful(response)) { return this.throw(`Unable to create directory with name '${body.title}'.`) } else { @@ -324,11 +394,11 @@ export class RemoteBackend extends backend.Backend { * * @throws An error if a non-successful status code (not 200-299) was received. */ async updateDirectory( - directoryId: backend.DirectoryId, - body: backend.UpdateDirectoryRequestBody, + directoryId: backendModule.DirectoryId, + body: backendModule.UpdateDirectoryRequestBody, title: string | null ) { - const response = await this.put( + const response = await this.put( updateDirectoryPath(directoryId), body ) @@ -346,7 +416,7 @@ export class RemoteBackend extends backend.Backend { /** Change the name of a directory. * * @throws An error if a non-successful status code (not 200-299) was received. */ - async deleteDirectory(directoryId: backend.DirectoryId, title: string | null) { + async deleteDirectory(directoryId: backendModule.DirectoryId, title: string | null) { const response = await this.delete(deleteDirectoryPath(directoryId)) if (!responseIsSuccessful(response)) { return this.throw( @@ -362,7 +432,7 @@ export class RemoteBackend extends backend.Backend { /** Return a list of projects belonging to the current user. * * @throws An error if a non-successful status code (not 200-299) was received. */ - async listProjects(): Promise { + async listProjects(): Promise { const response = await this.get(LIST_PROJECTS_PATH) if (!responseIsSuccessful(response)) { return this.throw('Unable to list projects.') @@ -370,9 +440,13 @@ export class RemoteBackend extends backend.Backend { return (await response.json()).projects.map(project => ({ ...project, jsonAddress: - project.address != null ? backend.Address(`${project.address}json`) : null, + project.address != null + ? backendModule.Address(`${project.address}json`) + : null, binaryAddress: - project.address != null ? backend.Address(`${project.address}binary`) : null, + project.address != null + ? backendModule.Address(`${project.address}binary`) + : null, })) } } @@ -380,8 +454,10 @@ export class RemoteBackend extends backend.Backend { /** Create a project. * * @throws An error if a non-successful status code (not 200-299) was received. */ - async createProject(body: backend.CreateProjectRequestBody): Promise { - const response = await this.post(CREATE_PROJECT_PATH, body) + async createProject( + body: backendModule.CreateProjectRequestBody + ): Promise { + const response = await this.post(CREATE_PROJECT_PATH, body) if (!responseIsSuccessful(response)) { return this.throw(`Unable to create project with name '${body.projectName}'.`) } else { @@ -392,7 +468,7 @@ export class RemoteBackend extends backend.Backend { /** Close a project. * * @throws An error if a non-successful status code (not 200-299) was received. */ - async closeProject(projectId: backend.ProjectId, title: string | null): Promise { + async closeProject(projectId: backendModule.ProjectId, title: string | null): Promise { const response = await this.post(closeProjectPath(projectId), {}) if (!responseIsSuccessful(response)) { return this.throw( @@ -409,10 +485,10 @@ export class RemoteBackend extends backend.Backend { * * @throws An error if a non-successful status code (not 200-299) was received. */ async getProjectDetails( - projectId: backend.ProjectId, + projectId: backendModule.ProjectId, title: string | null - ): Promise { - const response = await this.get(getProjectDetailsPath(projectId)) + ): Promise { + const response = await this.get(getProjectDetailsPath(projectId)) if (!responseIsSuccessful(response)) { return this.throw( `Unable to get details of project ${ @@ -425,7 +501,7 @@ export class RemoteBackend extends backend.Backend { project.ide_version ?? ( await this.listVersions({ - versionType: backend.VersionType.ide, + versionType: backendModule.VersionType.ide, default: true, }) )[0]?.number @@ -437,10 +513,12 @@ export class RemoteBackend extends backend.Backend { ideVersion, engineVersion: project.engine_version, jsonAddress: - project.address != null ? backend.Address(`${project.address}json`) : null, + project.address != null + ? backendModule.Address(`${project.address}json`) + : null, binaryAddress: project.address != null - ? backend.Address(`${project.address}binary`) + ? backendModule.Address(`${project.address}binary`) : null, } } @@ -451,8 +529,8 @@ export class RemoteBackend extends backend.Backend { * * @throws An error if a non-successful status code (not 200-299) was received. */ async openProject( - projectId: backend.ProjectId, - body: backend.OpenProjectRequestBody | null, + projectId: backendModule.ProjectId, + body: backendModule.OpenProjectRequestBody | null, title: string | null ): Promise { const response = await this.post( @@ -472,11 +550,14 @@ export class RemoteBackend extends backend.Backend { * * @throws An error if a non-successful status code (not 200-299) was received. */ async projectUpdate( - projectId: backend.ProjectId, - body: backend.ProjectUpdateRequestBody, + projectId: backendModule.ProjectId, + body: backendModule.ProjectUpdateRequestBody, title: string | null - ): Promise { - const response = await this.put(projectUpdatePath(projectId), body) + ): Promise { + const response = await this.put( + projectUpdatePath(projectId), + body + ) if (!responseIsSuccessful(response)) { return this.throw( `Unable to update project ${ @@ -491,7 +572,7 @@ export class RemoteBackend extends backend.Backend { /** Delete a project. * * @throws An error if a non-successful status code (not 200-299) was received. */ - async deleteProject(projectId: backend.ProjectId, title: string | null): Promise { + async deleteProject(projectId: backendModule.ProjectId, title: string | null): Promise { const response = await this.delete(deleteProjectPath(projectId)) if (!responseIsSuccessful(response)) { return this.throw( @@ -508,10 +589,10 @@ export class RemoteBackend extends backend.Backend { * * @throws An error if a non-successful status code (not 200-299) was received. */ async checkResources( - projectId: backend.ProjectId, + projectId: backendModule.ProjectId, title: string | null - ): Promise { - const response = await this.get(checkResourcesPath(projectId)) + ): Promise { + const response = await this.get(checkResourcesPath(projectId)) if (!responseIsSuccessful(response)) { return this.throw( `Unable to get resource usage for project ${ @@ -526,7 +607,7 @@ export class RemoteBackend extends backend.Backend { /** Return a list of files accessible by the current user. * * @throws An error if a non-successful status code (not 200-299) was received. */ - async listFiles(): Promise { + async listFiles(): Promise { const response = await this.get(LIST_FILES_PATH) if (!responseIsSuccessful(response)) { return this.throw('Unable to list files.') @@ -539,10 +620,10 @@ export class RemoteBackend extends backend.Backend { * * @throws An error if a non-successful status code (not 200-299) was received. */ async uploadFile( - params: backend.UploadFileRequestParams, + params: backendModule.UploadFileRequestParams, body: Blob - ): Promise { - const response = await this.postBinary( + ): Promise { + const response = await this.postBinary( UPLOAD_FILE_PATH + '?' + new URLSearchParams({ @@ -581,7 +662,7 @@ export class RemoteBackend extends backend.Backend { /** Delete a file. * * @throws An error if a non-successful status code (not 200-299) was received. */ - async deleteFile(fileId: backend.FileId, title: string | null): Promise { + async deleteFile(fileId: backendModule.FileId, title: string | null): Promise { const response = await this.delete(deleteFilePath(fileId)) if (!responseIsSuccessful(response)) { return this.throw( @@ -595,8 +676,10 @@ export class RemoteBackend extends backend.Backend { /** Create a secret environment variable. * * @throws An error if a non-successful status code (not 200-299) was received. */ - async createSecret(body: backend.CreateSecretRequestBody): Promise { - const response = await this.post(CREATE_SECRET_PATH, body) + async createSecret( + body: backendModule.CreateSecretRequestBody + ): Promise { + const response = await this.post(CREATE_SECRET_PATH, body) if (!responseIsSuccessful(response)) { return this.throw(`Unable to create secret with name '${body.secretName}'.`) } else { @@ -607,8 +690,11 @@ export class RemoteBackend extends backend.Backend { /** Return a secret environment variable. * * @throws An error if a non-successful status code (not 200-299) was received. */ - async getSecret(secretId: backend.SecretId, title: string | null): Promise { - const response = await this.get(getSecretPath(secretId)) + async getSecret( + secretId: backendModule.SecretId, + title: string | null + ): Promise { + const response = await this.get(getSecretPath(secretId)) if (!responseIsSuccessful(response)) { return this.throw( `Unable to get secret ${title != null ? `'${title}'` : `with ID '${secretId}'`}.` @@ -621,7 +707,7 @@ export class RemoteBackend extends backend.Backend { /** Return the secret environment variables accessible by the user. * * @throws An error if a non-successful status code (not 200-299) was received. */ - async listSecrets(): Promise { + async listSecrets(): Promise { const response = await this.get(LIST_SECRETS_PATH) if (!responseIsSuccessful(response)) { return this.throw('Unable to list secrets.') @@ -633,7 +719,7 @@ export class RemoteBackend extends backend.Backend { /** Delete a secret environment variable. * * @throws An error if a non-successful status code (not 200-299) was received. */ - async deleteSecret(secretId: backend.SecretId, title: string | null): Promise { + async deleteSecret(secretId: backendModule.SecretId, title: string | null): Promise { const response = await this.delete(deleteSecretPath(secretId)) if (!responseIsSuccessful(response)) { return this.throw( @@ -647,8 +733,8 @@ export class RemoteBackend extends backend.Backend { /** Create a file tag or project tag. * * @throws An error if a non-successful status code (not 200-299) was received. */ - async createTag(body: backend.CreateTagRequestBody): Promise { - const response = await this.post(CREATE_TAG_PATH, { + async createTag(body: backendModule.CreateTagRequestBody): Promise { + const response = await this.post(CREATE_TAG_PATH, { /* eslint-disable @typescript-eslint/naming-convention */ tag_name: body.name, tag_value: body.value, @@ -666,7 +752,7 @@ export class RemoteBackend extends backend.Backend { /** Return file tags or project tags accessible by the user. * * @throws An error if a non-successful status code (not 200-299) was received. */ - async listTags(params: backend.ListTagsRequestParams): Promise { + async listTags(params: backendModule.ListTagsRequestParams): Promise { const response = await this.get( LIST_TAGS_PATH + '?' + @@ -685,7 +771,7 @@ export class RemoteBackend extends backend.Backend { /** Delete a secret environment variable. * * @throws An error if a non-successful status code (not 200-299) was received. */ - async deleteTag(tagId: backend.TagId): Promise { + async deleteTag(tagId: backendModule.TagId): Promise { const response = await this.delete(deleteTagPath(tagId)) if (!responseIsSuccessful(response)) { return this.throw(`Unable to delete tag with ID '${tagId}'.`) @@ -697,7 +783,9 @@ export class RemoteBackend extends backend.Backend { /** Return list of backend or IDE versions. * * @throws An error if a non-successful status code (not 200-299) was received. */ - async listVersions(params: backend.ListVersionsRequestParams): Promise { + async listVersions( + params: backendModule.ListVersionsRequestParams + ): Promise { const response = await this.get( LIST_VERSIONS_PATH + '?' + From 33a9b6784b63918108d2d01594ada887b3a294fe Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Fri, 25 Aug 2023 20:47:37 +1000 Subject: [PATCH 9/9] Trigger CI