From a05b897e776e801f02ea3c1acd996858da3c8195 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Fri, 19 May 2023 22:06:46 +1000 Subject: [PATCH 01/16] Disable cloud backend for unverified users --- .../lib/content/src/serviceWorker.ts | 2 +- .../src/authentication/providers/auth.tsx | 56 +- .../authentication/src/dashboard/backend.ts | 3 + .../src/dashboard/components/dashboard.tsx | 810 ++++++++++-------- .../src/dashboard/components/userMenu.tsx | 34 +- .../lib/dashboard/src/serviceWorker.ts | 2 +- 6 files changed, 474 insertions(+), 433 deletions(-) diff --git a/app/ide-desktop/lib/content/src/serviceWorker.ts b/app/ide-desktop/lib/content/src/serviceWorker.ts index 4ccd2ddb0028..e51a59ef3e95 100644 --- a/app/ide-desktop/lib/content/src/serviceWorker.ts +++ b/app/ide-desktop/lib/content/src/serviceWorker.ts @@ -16,7 +16,7 @@ declare const self: ServiceWorkerGlobalScope self.addEventListener('fetch', event => { const url = new URL(event.request.url) if (url.hostname === 'localhost' && url.pathname !== '/esbuild') { - const responsePromise = /\/[^.]+$/.test(event.request.url) + const responsePromise = /\/[^.]+$/.test(new URL(event.request.url).pathname) ? fetch('/index.html') : fetch(event.request.url) event.respondWith( diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx index e1adaee206f6..cb9c5df83519 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx @@ -35,23 +35,28 @@ const MESSAGES = { pleaseWait: 'Please wait...', } as const -// ============= -// === Types === -// ============= - +// =================== // === UserSession === +// =================== -export type UserSession = FullUserSession | PartialUserSession +/** Possible types of {@link BaseUserSession}. */ +export enum UserSessionType { + partial = 'partial', + awaitingAcceptance = 'awaitingAcceptance', + full = 'full', +} -/** Object containing the currently signed-in user's session data. */ -export interface FullUserSession { - /** A discriminator for TypeScript to be able to disambiguate between this interface and other - * `UserSession` variants. */ - variant: 'full' +interface BaseUserSession { + /** A discriminator for TypeScript to be able to disambiguate between `UserSession` variants. */ + type: Type /** User's JSON Web Token (JWT), used for authenticating and authorizing requests to the API. */ accessToken: string /** User's email address. */ email: string +} + +/** Object containing the currently signed-in user's session data. */ +export interface FullUserSession extends BaseUserSession { /** User's organization information. */ organization: backendModule.UserOrOrganization } @@ -62,15 +67,9 @@ export interface FullUserSession { * If a user has not yet set their username, they do not yet have an organization associated with * their account. Otherwise, this type is identical to the `Session` type. This type should ONLY be * used by the `SetUsername` component. */ -export interface PartialUserSession { - /** A discriminator for TypeScript to be able to disambiguate between this interface and other - * `UserSession` variants. */ - variant: 'partial' - /** User's JSON Web Token (JWT), used for authenticating and authorizing requests to the API. */ - accessToken: string - /** User's email address. */ - email: string -} +export interface PartialUserSession extends BaseUserSession {} + +export type UserSession = FullUserSession | PartialUserSession // =================== // === AuthContext === @@ -172,17 +171,16 @@ export function AuthProvider(props: AuthProviderProps) { setBackend(backend) const organization = await backend.usersMe().catch(() => null) let newUserSession: UserSession + const sharedSessionData = { email, accessToken } if (!organization) { newUserSession = { - variant: 'partial', - email, - accessToken, + type: UserSessionType.partial, + ...sharedSessionData, } } else { newUserSession = { - variant: 'full', - email, - accessToken, + type: UserSessionType.full, + ...sharedSessionData, organization, } @@ -386,7 +384,7 @@ export function ProtectedLayout() { if (!session) { return - } else if (session.variant === 'partial') { + } else if (session.type === UserSessionType.partial) { return } else { return @@ -400,7 +398,7 @@ export function ProtectedLayout() { export function SemiProtectedLayout() { const { session } = useAuth() - if (session?.variant === 'full') { + if (session?.type === UserSessionType.full) { return } else { return @@ -414,9 +412,9 @@ export function SemiProtectedLayout() { export function GuestLayout() { const { session } = useAuth() - if (session?.variant === 'partial') { + if (session?.type === UserSessionType.partial) { return - } else if (session?.variant === 'full') { + } else if (session?.type === UserSessionType.full) { return } else { return 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 6c41777e1bb0..6f4d48800e56 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 @@ -48,6 +48,9 @@ export interface UserOrOrganization { id: UserOrOrganizationId name: string email: EmailAddress + /** If `false`, this account is awaiting acceptance from an admin, and endpoints other than + * `usersMe` will not work. */ + isEnabled: boolean } /** Possible states that a project can be in. */ 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 f7b979b0fd84..72873cf6200a 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 @@ -669,7 +669,7 @@ function Dashboard(props: DashboardProps) { return (
{ @@ -726,403 +726,451 @@ function Dashboard(props: DashboardProps) { query={query} setQuery={setQuery} /> - -
-

Drive

-
-
- {directory && ( - <> - - {svg.SMALL_RIGHT_ARROW_ICON} - - )} - {directory?.title ?? '/'} -
-
-
Shared with
-
-
-
- - + {backend.platform === platformModule.Platform.cloud && !organization.isEnabled ? ( +
+
+ We will review your user details and enable the cloud experience for you + shortly.
- {EXPERIMENTAL.columnModeSwitcher && ( - <> -
- - +
+ ) : ( + <> + +
+

Drive

+
+
+ {directory && ( + <> + + {svg.SMALL_RIGHT_ARROW_ICON} + + )} + {directory?.title ?? '/'} +
+
+
Shared with
+
+
+
- - )} -
-
- - - - {columnsFor(columnDisplayMode, backend.platform).map(column => ( - - > - items={visibleProjectAssets} - getKey={proj => proj.id} - isLoading={isLoadingAssets} - placeholder={ - - You have no project yet. Go ahead and create one using the form - above. - - } - columns={columnsFor(columnDisplayMode, backend.platform).map(column => ({ - id: column, - heading: ColumnHeading(column, backendModule.AssetType.project), - render: renderer(column, backendModule.AssetType.project), - }))} - onClick={(projectAsset, event) => { - event.stopPropagation() - setSelectedAssets( - event.shiftKey ? [...selectedAssets, projectAsset] : [projectAsset] - ) - }} - onContextMenu={(projectAsset, event) => { - event.preventDefault() - event.stopPropagation() - function doOpenForEditing() { - // FIXME[sb]: Switch to IDE tab - // once merged with `show-and-open-workspace` branch. - } - function doOpenAsFolder() { - // FIXME[sb]: Uncomment once backend support - // is in place. - // The following code does not typecheck - // since `ProjectId`s are not `DirectoryId`s. - // enterDirectory(projectAsset) - } - // This is not a React component even though it contains JSX. - // eslint-disable-next-line no-restricted-syntax - function doRename() { - setModal(() => ( - { - await backend.projectUpdate(projectAsset.id, { - ami: null, - ideVersion: null, - projectName: name, - }) - }} - onSuccess={doRefresh} - /> - )) - } - // This is not a React component even though it contains JSX. - // eslint-disable-next-line no-restricted-syntax - function doDelete() { - setModal(() => ( - backend.deleteProject(projectAsset.id)} - onSuccess={doRefresh} - /> - )) - } - setModal(() => ( - - - Open for editing - - - Open as folder - - Rename - - Delete - - - )) - }} - /> - {backend.platform === platformModule.Platform.cloud && - (remoteBackend => ( - <> - - > - items={visibleDirectoryAssets} - getKey={dir => dir.id} - isLoading={isLoadingAssets} - placeholder={ - - This directory does not contain any subdirectories - {query ? ' matching your query' : ''}. - + {EXPERIMENTAL.columnModeSwitcher && ( + <> +
+ + + + +
+ + )} + + +
- ))} -
+ + + {columnsFor(columnDisplayMode, backend.platform).map(column => ( + + > + items={visibleProjectAssets} + getKey={proj => proj.id} + isLoading={isLoadingAssets} + placeholder={ + + You have no project yet. Go ahead and create one using the + form above. + + } + columns={columnsFor(columnDisplayMode, backend.platform).map( + column => ({ + id: column, + heading: ColumnHeading( + column, + backendModule.AssetType.project + ), + render: renderer(column, backendModule.AssetType.project), + }) + )} + onClick={(projectAsset, event) => { + event.stopPropagation() + setSelectedAssets( + event.shiftKey + ? [...selectedAssets, projectAsset] + : [projectAsset] + ) + }} + onContextMenu={(projectAsset, event) => { + event.preventDefault() + event.stopPropagation() + function doOpenForEditing() { + // FIXME[sb]: Switch to IDE tab + // once merged with `show-and-open-workspace` branch. } - columns={columnsFor(columnDisplayMode, backend.platform).map( - column => ({ - id: column, - heading: ColumnHeading( - column, - backendModule.AssetType.directory - ), - render: renderer( - column, - backendModule.AssetType.directory - ), - }) - )} - onClick={(directoryAsset, event) => { - event.stopPropagation() - setSelectedAssets( - event.shiftKey - ? [...selectedAssets, directoryAsset] - : [directoryAsset] - ) - }} - onContextMenu={(_directory, event) => { - event.preventDefault() - event.stopPropagation() - setModal(() => ) - }} - /> - - > - items={visibleSecretAssets} - getKey={secret => secret.id} - isLoading={isLoadingAssets} - placeholder={ - - This directory does not contain any secrets - {query ? ' matching your query' : ''}. - + function doOpenAsFolder() { + // FIXME[sb]: Uncomment once backend support + // is in place. + // The following code does not typecheck + // since `ProjectId`s are not `DirectoryId`s. + // enterDirectory(projectAsset) } - columns={columnsFor(columnDisplayMode, backend.platform).map( - column => ({ - id: column, - heading: ColumnHeading( - column, - backendModule.AssetType.secret - ), - render: renderer( - column, - backendModule.AssetType.secret - ), - }) - )} - onClick={(secret, event) => { - event.stopPropagation() - setSelectedAssets( - event.shiftKey ? [...selectedAssets, secret] : [secret] - ) - }} - onContextMenu={(secret, event) => { - event.preventDefault() - event.stopPropagation() - // This is not a React component even though it contains JSX. - // eslint-disable-next-line no-restricted-syntax - function doDelete() { - setModal(() => ( - - remoteBackend.deleteSecret(secret.id) - } - onSuccess={doRefresh} - /> - )) - } + // This is not a React component even though it contains JSX. + // eslint-disable-next-line no-restricted-syntax + function doRename() { setModal(() => ( - - - Delete - - + { + await backend.projectUpdate(projectAsset.id, { + ami: null, + ideVersion: null, + projectName: name, + }) + }} + onSuccess={doRefresh} + /> )) - }} - /> - - > - items={visibleFileAssets} - getKey={file => file.id} - isLoading={isLoadingAssets} - placeholder={ - - This directory does not contain any files - {query ? ' matching your query' : ''}. - } - columns={columnsFor(columnDisplayMode, backend.platform).map( - column => ({ - id: column, - heading: ColumnHeading( - column, - backendModule.AssetType.file - ), - render: renderer(column, backendModule.AssetType.file), - }) - )} - onClick={(file, event) => { - event.stopPropagation() - setSelectedAssets( - event.shiftKey ? [...selectedAssets, file] : [file] - ) - }} - onContextMenu={(file, event) => { - event.preventDefault() - event.stopPropagation() - function doCopy() { - /** TODO: Wait for backend endpoint. */ - } - function doCut() { - /** TODO: Wait for backend endpoint. */ - } - // This is not a React component even though it contains JSX. - // eslint-disable-next-line no-restricted-syntax - function doDelete() { - setModal(() => ( - - remoteBackend.deleteFile(file.id) - } - onSuccess={doRefresh} - /> - )) - } - function doDownload() { - /** TODO: Wait for backend endpoint. */ - } + // This is not a React component even though it contains JSX. + // eslint-disable-next-line no-restricted-syntax + function doDelete() { setModal(() => ( - - - Copy - - - Cut - - - Delete - - - Download - - + + backend.deleteProject(projectAsset.id) + } + onSuccess={doRefresh} + /> )) - }} - /> - - ))(backend)} - -
+ ))} +
- {isFileBeingDragged && backend.platform === platformModule.Platform.cloud ? ( -
{ - setIsFileBeingDragged(false) - }} - onDragOver={event => { - event.preventDefault() - }} - onDrop={async event => { - event.preventDefault() - setIsFileBeingDragged(false) - await uploadMultipleFiles.uploadMultipleFiles( - backend, - directoryId, - Array.from(event.dataTransfer.files) - ) - doRefresh() - }} - > - Drop to upload files. -
- ) : null} + } + setModal(() => ( + + + Open for editing + + + Open as folder + + + Rename + + + Delete + + + )) + }} + /> + {backend.platform === platformModule.Platform.cloud && + (remoteBackend => ( + <> + + + > + items={visibleDirectoryAssets} + getKey={dir => dir.id} + isLoading={isLoadingAssets} + placeholder={ + + This directory does not contain any + subdirectories + {query ? ' matching your query' : ''}. + + } + columns={columnsFor( + columnDisplayMode, + backend.platform + ).map(column => ({ + id: column, + heading: ColumnHeading( + column, + backendModule.AssetType.directory + ), + render: renderer( + column, + backendModule.AssetType.directory + ), + }))} + onClick={(directoryAsset, event) => { + event.stopPropagation() + setSelectedAssets( + event.shiftKey + ? [...selectedAssets, directoryAsset] + : [directoryAsset] + ) + }} + onContextMenu={(_directory, event) => { + event.preventDefault() + event.stopPropagation() + setModal(() => ( + + )) + }} + /> + + > + items={visibleSecretAssets} + getKey={secret => secret.id} + isLoading={isLoadingAssets} + placeholder={ + + This directory does not contain any secrets + {query ? ' matching your query' : ''}. + + } + columns={columnsFor( + columnDisplayMode, + backend.platform + ).map(column => ({ + id: column, + heading: ColumnHeading( + column, + backendModule.AssetType.secret + ), + render: renderer( + column, + backendModule.AssetType.secret + ), + }))} + onClick={(secret, event) => { + event.stopPropagation() + setSelectedAssets( + event.shiftKey + ? [...selectedAssets, secret] + : [secret] + ) + }} + onContextMenu={(secret, event) => { + event.preventDefault() + event.stopPropagation() + // This is not a React component even though it contains JSX. + // eslint-disable-next-line no-restricted-syntax + function doDelete() { + setModal(() => ( + + remoteBackend.deleteSecret( + secret.id + ) + } + onSuccess={doRefresh} + /> + )) + } + setModal(() => ( + + + + Delete + + + + )) + }} + /> + + > + items={visibleFileAssets} + getKey={file => file.id} + isLoading={isLoadingAssets} + placeholder={ + + This directory does not contain any files + {query ? ' matching your query' : ''}. + + } + columns={columnsFor( + columnDisplayMode, + backend.platform + ).map(column => ({ + id: column, + heading: ColumnHeading( + column, + backendModule.AssetType.file + ), + render: renderer( + column, + backendModule.AssetType.file + ), + }))} + onClick={(file, event) => { + event.stopPropagation() + setSelectedAssets( + event.shiftKey + ? [...selectedAssets, file] + : [file] + ) + }} + onContextMenu={(file, event) => { + event.preventDefault() + event.stopPropagation() + function doCopy() { + /** TODO: Wait for backend endpoint. */ + } + function doCut() { + /** TODO: Wait for backend endpoint. */ + } + // This is not a React component even though it contains JSX. + // eslint-disable-next-line no-restricted-syntax + function doDelete() { + setModal(() => ( + + remoteBackend.deleteFile(file.id) + } + onSuccess={doRefresh} + /> + )) + } + function doDownload() { + /** TODO: Wait for backend endpoint. */ + } + setModal(() => ( + + + Copy + + + Cut + + + + Delete + + + + Download + + + )) + }} + /> + + ))(backend)} + + + {isFileBeingDragged && backend.platform === platformModule.Platform.cloud ? ( +
{ + setIsFileBeingDragged(false) + }} + onDragOver={event => { + event.preventDefault() + }} + onDrop={async event => { + event.preventDefault() + setIsFileBeingDragged(false) + await uploadMultipleFiles.uploadMultipleFiles( + backend, + directoryId, + Array.from(event.dataTransfer.files) + ) + doRefresh() + }} + > + Drop to upload files. +
+ ) : null} + + )} {/* This should be just `{modal}`, however TypeScript incorrectly throws an error. */} {project && } {modal && <>{modal}} diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/userMenu.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/userMenu.tsx index 61b205bc9401..2c909911b8bb 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/userMenu.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/userMenu.tsx @@ -54,28 +54,20 @@ function UserMenu() { event.stopPropagation() }} > - {/* FIXME[sb]: Figure out whether this conditional is *actually* needed. */} - {/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */} - {organization ? ( - <> - - Signed in as {organization.name} - - Your profile - {canChangePassword && ( - { - setModal(() => ) - }} - > - Change your password - - )} - Sign out - - ) : ( - Not logged in currently. + + Signed in as {organization.name} + + Your profile + {canChangePassword && ( + { + setModal(() => ) + }} + > + Change your password + )} + Sign out
) } diff --git a/app/ide-desktop/lib/dashboard/src/serviceWorker.ts b/app/ide-desktop/lib/dashboard/src/serviceWorker.ts index 8c087b349962..c671d75f31a9 100644 --- a/app/ide-desktop/lib/dashboard/src/serviceWorker.ts +++ b/app/ide-desktop/lib/dashboard/src/serviceWorker.ts @@ -16,7 +16,7 @@ declare const self: ServiceWorkerGlobalScope self.addEventListener('fetch', event => { const url = new URL(event.request.url) if (url.hostname === 'localhost' && url.pathname !== '/esbuild') { - const responsePromise = /\/[^.]+$/.test(event.request.url) + const responsePromise = /\/[^.]+$/.test(new URL(event.request.url).pathname) ? fetch('/index.html') : fetch(event.request.url) event.respondWith( From df6fc6ed4ad445367f323a7361709a24030ecf77 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Fri, 19 May 2023 22:30:50 +1000 Subject: [PATCH 02/16] Use local backend as default backend --- .../src/authentication/src/authentication/providers/auth.tsx | 2 -- .../lib/dashboard/src/authentication/src/components/app.tsx | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx index cb9c5df83519..b3fc8e57d261 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx @@ -145,7 +145,6 @@ export function AuthProvider(props: AuthProviderProps) { const { authService, children } = props const { cognito } = authService const { session } = sessionProvider.useSession() - const { setBackend } = backendProvider.useSetBackend() const logger = loggerProvider.useLogger() const navigate = router.useNavigate() const onAuthenticated = react.useCallback(props.onAuthenticated, []) @@ -168,7 +167,6 @@ export function AuthProvider(props: AuthProviderProps) { headers.append('Authorization', `Bearer ${accessToken}`) const client = new http.Client(headers) const backend = new remoteBackend.RemoteBackend(client, logger) - setBackend(backend) const organization = await backend.usersMe().catch(() => null) let newUserSession: UserSession const sharedSessionData = { email, accessToken } diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx index cc1f34e22ffc..3788c1c59087 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx @@ -39,6 +39,7 @@ import * as router from 'react-router-dom' import * as toast from 'react-hot-toast' import * as authService from '../authentication/service' +import * as localBackend from '../dashboard/localBackend' import * as platformModule from '../platform' import * as authProvider from '../authentication/providers/auth' @@ -163,8 +164,7 @@ function AppRouter(props: AppProps) { userSession={userSession} registerAuthEventListener={registerAuthEventListener} > - {/* @ts-expect-error Auth will always set this before dashboard is rendered. */} - + Date: Fri, 19 May 2023 23:05:58 +1000 Subject: [PATCH 03/16] Try to fix Project Manager reconnection logic --- .../src/dashboard/projectManager.ts | 69 +++++++++++-------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/projectManager.ts b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/projectManager.ts index 749a870038cf..43e272ab7ecf 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/projectManager.ts +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/projectManager.ts @@ -12,8 +12,9 @@ import GLOBAL_CONFIG from '../../../../../../../gui/config.yaml' assert { type: /** Duration before the {@link ProjectManager} tries to create a WebSocket again. */ const RETRY_INTERVAL_MS = 1000 -/** Duration after which the {@link ProjectManager} stops re-trying to create a WebSocket. */ -const STOP_TRYING_AFTER_MS = 10000 +/** The number of times in a row the {@link ProjectManager} will try to connect, + * before it throws an error. */ +const MAXIMUM_CONSECUTIVE_FAILS = 10 // ============= // === Types === @@ -136,43 +137,51 @@ export class ProjectManager { /** Create a {@link ProjectManager} */ private constructor(protected readonly connectionUrl: string) { + let lastConnectionStartMs = 0 + let consecutiveFails = 0 + let justErrored = false const createSocket = () => { + lastConnectionStartMs = Number(new Date()) this.resolvers = new Map() const oldRejecters = this.rejecters this.rejecters = new Map() for (const reject of oldRejecters.values()) { reject() } - this.socketPromise = new Promise((resolve, reject) => { - const handle = setInterval(() => { - try { - const socket = new WebSocket(this.connectionUrl) - clearInterval(handle) - socket.onmessage = event => { - // There is no way to avoid this as `JSON.parse` returns `any`. - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument - const message: JSONRPCResponse = JSON.parse(event.data) - if ('result' in message) { - this.resolvers.get(message.id)?.(message.result) - } else { - this.rejecters.get(message.id)?.(message.error) - } - } - socket.onopen = () => { - resolve(socket) - } - socket.onerror = createSocket - socket.onclose = createSocket - } catch { - // Ignored; the `setInterval` will retry again eventually. + return new Promise((resolve, reject) => { + const socket = new WebSocket(this.connectionUrl) + socket.onmessage = event => { + // There is no way to avoid this as `JSON.parse` returns `any`. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument + const message: JSONRPCResponse = JSON.parse(event.data) + if ('result' in message) { + this.resolvers.get(message.id)?.(message.result) + } else { + this.rejecters.get(message.id)?.(message.error) } - }, RETRY_INTERVAL_MS) - setTimeout(() => { - clearInterval(handle) - reject() - }, STOP_TRYING_AFTER_MS) + } + socket.onopen = () => { + consecutiveFails = 0 + resolve(socket) + } + socket.onerror = () => { + justErrored = true + consecutiveFails += 1 + if (consecutiveFails > MAXIMUM_CONSECUTIVE_FAILS) { + reject() + } + const delay = RETRY_INTERVAL_MS - (Number(new Date()) - lastConnectionStartMs) + setTimeout(() => { + void createSocket().then(resolve) + }, Math.max(0, delay)) + } + socket.onclose = () => { + if (!justErrored) { + this.socketPromise = createSocket() + } + justErrored = false + } }) - return this.socketPromise } this.socketPromise = createSocket() } From 39cbc40acba6b147c77844d37a09cb17f46d9f1d Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Fri, 19 May 2023 23:14:43 +1000 Subject: [PATCH 04/16] Set default backend to remote backend when local backend is not available --- .../src/authentication/providers/auth.tsx | 11 ++++++++--- .../src/authentication/src/components/app.tsx | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx index b3fc8e57d261..aef5e882bd0d 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx @@ -15,7 +15,7 @@ import * as errorModule from '../../error' import * as http from '../../http' import * as loggerProvider from '../../providers/logger' import * as newtype from '../../newtype' -import * as platform from '../../platform' +import * as platformModule from '../../platform' import * as remoteBackend from '../../dashboard/remoteBackend' import * as sessionProvider from './session' @@ -136,15 +136,17 @@ const AuthContext = react.createContext({} as AuthContextType) export interface AuthProviderProps { authService: authServiceModule.AuthService + platform: platformModule.Platform /** Callback to execute once the user has authenticated successfully. */ onAuthenticated: () => void children: react.ReactNode } export function AuthProvider(props: AuthProviderProps) { - const { authService, children } = props + const { authService, platform, children } = props const { cognito } = authService const { session } = sessionProvider.useSession() + const { setBackend } = backendProvider.useSetBackend() const logger = loggerProvider.useLogger() const navigate = router.useNavigate() const onAuthenticated = react.useCallback(props.onAuthenticated, []) @@ -167,6 +169,9 @@ export function AuthProvider(props: AuthProviderProps) { headers.append('Authorization', `Bearer ${accessToken}`) const client = new http.Client(headers) const backend = new remoteBackend.RemoteBackend(client, logger) + if (platform === platformModule.Platform.cloud) { + setBackend(backend) + } const organization = await backend.usersMe().catch(() => null) let newUserSession: UserSession const sharedSessionData = { email, accessToken } @@ -262,7 +267,7 @@ export function AuthProvider(props: AuthProviderProps) { username: string, email: string ) => { - if (backend.platform === platform.Platform.desktop) { + if (backend.platform === platformModule.Platform.desktop) { toast.error('You cannot set your username on the local backend.') return false } else { diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx index 3788c1c59087..34888dc6007a 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx @@ -122,7 +122,7 @@ function App(props: AppProps) { * because the {@link AppRouter} relies on React hooks, which can't be used in the same React * component as the component that defines the provider. */ function AppRouter(props: AppProps) { - const { logger, showDashboard, onAuthenticated } = props + const { logger, platform, showDashboard, onAuthenticated } = props const navigate = router.useNavigate() const mainPageUrl = new URL(window.location.href) const memoizedAuthService = react.useMemo(() => { @@ -164,10 +164,20 @@ function AppRouter(props: AppProps) { userSession={userSession} registerAuthEventListener={registerAuthEventListener} > - + {routes} From 40c2b13073ed7ad87f5053cb2a277402e1ae6f83 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Fri, 19 May 2023 23:17:24 +1000 Subject: [PATCH 05/16] Attempt to suppress WebSocket error console message --- .../src/authentication/src/dashboard/projectManager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/projectManager.ts b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/projectManager.ts index 43e272ab7ecf..649005cbc3b7 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/projectManager.ts +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/projectManager.ts @@ -164,7 +164,8 @@ export class ProjectManager { consecutiveFails = 0 resolve(socket) } - socket.onerror = () => { + socket.onerror = event => { + event.preventDefault() justErrored = true consecutiveFails += 1 if (consecutiveFails > MAXIMUM_CONSECUTIVE_FAILS) { From aa43e4a12c522ead0a276f45485b113f17a5a624 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Sat, 20 May 2023 01:03:25 +1000 Subject: [PATCH 06/16] Minor fix for loading spinner --- .../src/authentication/src/dashboard/components/dashboard.tsx | 1 - 1 file changed, 1 deletion(-) 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 72873cf6200a..9c0edb47a8ba 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 @@ -600,7 +600,6 @@ function Dashboard(props: DashboardProps) { hooks.useAsyncEffect( null, async signal => { - setIsLoadingAssets(true) const assets = await backend.listDirectory({ parentId: directoryId }) if (!signal.aborted) { setIsLoadingAssets(false) From 29bd3ff15aa60b0a17ff062a48d3a6f8c4ebfc41 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Sat, 20 May 2023 01:07:31 +1000 Subject: [PATCH 07/16] Hide "Open as folder" context menu option, when on local backend --- .../src/dashboard/components/dashboard.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 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 9c0edb47a8ba..9e79cea0df82 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 @@ -936,9 +936,12 @@ function Dashboard(props: DashboardProps) { Open for editing - - Open as folder - + {backend.platform !== + platformModule.Platform.desktop && ( + + Open as folder + + )} Rename From 17fd22a511a96e23085673c7e1a5ba853631d459 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Sat, 20 May 2023 01:20:16 +1000 Subject: [PATCH 08/16] Remove duplicate `onSubmit` handler --- .../src/dashboard/components/changePasswordModal.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/changePasswordModal.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/changePasswordModal.tsx index 74a4ebaa8a03..ffe28f7133b2 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/changePasswordModal.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/changePasswordModal.tsx @@ -38,10 +38,6 @@ function ChangePasswordModal() { onClick={event => { event.stopPropagation() }} - onSubmit={async event => { - event.preventDefault() - await onSubmit() - }} className="flex flex-col bg-white shadow-md px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md w-full max-w-md" >
From 5d6e8f5c359a3528060ed1f12c5c442340dd04cb Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Sat, 20 May 2023 01:49:47 +1000 Subject: [PATCH 09/16] Fix live-reload --- app/ide-desktop/lib/content/src/index.ts | 4 +++- app/ide-desktop/lib/dashboard/src/index.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/ide-desktop/lib/content/src/index.ts b/app/ide-desktop/lib/content/src/index.ts index 3d43ce325b42..16c79f401b51 100644 --- a/app/ide-desktop/lib/content/src/index.ts +++ b/app/ide-desktop/lib/content/src/index.ts @@ -34,7 +34,9 @@ const FETCH_TIMEOUT = 300 if (IS_DEV_MODE) { new EventSource(ESBUILD_PATH).addEventListener(ESBUILD_EVENT_NAME, () => { - location.reload() + // This acts like `location.reload`, but it preserves the query-string. + // The `toString()` is to bypass a lint without using a comment. + location.href = location.href.toString() }) void navigator.serviceWorker.register(SERVICE_WORKER_PATH) } diff --git a/app/ide-desktop/lib/dashboard/src/index.tsx b/app/ide-desktop/lib/dashboard/src/index.tsx index 3b4a3176f9b1..56b4e41b9bb5 100644 --- a/app/ide-desktop/lib/dashboard/src/index.tsx +++ b/app/ide-desktop/lib/dashboard/src/index.tsx @@ -21,7 +21,9 @@ const SERVICE_WORKER_PATH = '/serviceWorker.js' if (IS_DEV_MODE) { new EventSource(ESBUILD_PATH).addEventListener(ESBUILD_EVENT_NAME, () => { - location.reload() + // This acts like `location.reload`, but it preserves the query-string. + // The `toString()` is to bypass a lint without using a comment. + location.href = location.href.toString() }) void navigator.serviceWorker.register(SERVICE_WORKER_PATH) } From 74dedee4ed84b0232e9f5be4b4af75358c554404 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Sat, 20 May 2023 02:08:25 +1000 Subject: [PATCH 10/16] Remove debug `console.log` --- .../src/authentication/src/dashboard/components/renameModal.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/renameModal.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/renameModal.tsx index e9d4ce260892..03889d0a9a7b 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/renameModal.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/renameModal.tsx @@ -39,8 +39,6 @@ function RenameModal(props: RenameModalProps) { } } - console.log('what', namePattern, title) - return (
Date: Sat, 20 May 2023 02:28:16 +1000 Subject: [PATCH 11/16] Minor refactor; properly unset modals on click --- .../src/dashboard/components/dashboard.tsx | 112 +++++++++--------- 1 file changed, 58 insertions(+), 54 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 9e79cea0df82..a9bf9275c9a4 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 @@ -778,66 +778,66 @@ function Dashboard(props: DashboardProps) { disabled={true} onClick={event => { event.stopPropagation() + unsetModal() /* TODO */ }} > {svg.DOWNLOAD_ICON}
- {EXPERIMENTAL.columnModeSwitcher && ( - <> -
- - - - -
- - )} +
@@ -869,6 +869,7 @@ function Dashboard(props: DashboardProps) { )} onClick={(projectAsset, event) => { event.stopPropagation() + unsetModal() setSelectedAssets( event.shiftKey ? [...selectedAssets, projectAsset] @@ -985,6 +986,7 @@ function Dashboard(props: DashboardProps) { }))} onClick={(directoryAsset, event) => { event.stopPropagation() + unsetModal() setSelectedAssets( event.shiftKey ? [...selectedAssets, directoryAsset] @@ -1026,6 +1028,7 @@ function Dashboard(props: DashboardProps) { }))} onClick={(secret, event) => { event.stopPropagation() + unsetModal() setSelectedAssets( event.shiftKey ? [...selectedAssets, secret] @@ -1089,6 +1092,7 @@ function Dashboard(props: DashboardProps) { }))} onClick={(file, event) => { event.stopPropagation() + unsetModal() setSelectedAssets( event.shiftKey ? [...selectedAssets, file] From a4d6fc2439fffe1132d6d18771c9e867e208088d Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Sat, 20 May 2023 04:48:52 +1000 Subject: [PATCH 12/16] "created" project state for local backend, for parity with remote backend --- .../src/authentication/src/dashboard/localBackend.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/localBackend.ts b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/localBackend.ts index fb53e65c43c1..19a375b0d05b 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/localBackend.ts +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/localBackend.ts @@ -4,7 +4,6 @@ * The functions are asynchronous and return a {@link Promise} that resolves to the response from * the API. */ import * as backend from './backend' -import * as dateTime from './dateTime' import * as newtype from '../newtype' import * as platformModule from '../platform' import * as projectManager from './projectManager' @@ -119,7 +118,9 @@ export class LocalBackend implements Partial { type: projectId === LocalBackend.currentlyOpeningProjectId ? backend.ProjectState.openInProgress - : backend.ProjectState.closed, + : project.lastOpened != null + ? backend.ProjectState.closed + : backend.ProjectState.created, }, }) } From 39346160e2502b5e6d6115970a35398aab76649a Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Mon, 22 May 2023 17:08:58 +1000 Subject: [PATCH 13/16] Hide directory path when on local backend --- .../src/dashboard/components/dashboard.tsx | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 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 980519bb4a40..4c512212aab7 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 @@ -742,21 +742,25 @@ function Dashboard(props: DashboardProps) {

Drive

-
- {directory && ( - <> - - {svg.SMALL_RIGHT_ARROW_ICON} - - )} - {directory?.title ?? '/'} -
-
-
Shared with
-
-
+ {backend.platform === platformModule.Platform.cloud && ( + <> +
+ {directory && ( + <> + + {svg.SMALL_RIGHT_ARROW_ICON} + + )} + {directory?.title ?? '/'} +
+
+
Shared with
+
+
+ + )}
- + {EXPERIMENTAL.columnModeSwitcher && ( + <> +
+ + + + +
+ + )}
From 5974170866e62d29ae4f719a68b33acd70a4da9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buchowski?= Date: Mon, 22 May 2023 11:36:17 +0200 Subject: [PATCH 15/16] set newDashboard & authentication flags to true --- app/ide-desktop/lib/content-config/src/config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ide-desktop/lib/content-config/src/config.json b/app/ide-desktop/lib/content-config/src/config.json index c513b5e80a99..eff732d50106 100644 --- a/app/ide-desktop/lib/content-config/src/config.json +++ b/app/ide-desktop/lib/content-config/src/config.json @@ -1,7 +1,7 @@ { "options": { "authentication": { - "value": false, + "value": true, "description": "Determines whether user authentication is enabled. This option is always true when executed in the cloud." }, "dataCollection": { @@ -116,7 +116,7 @@ "primary": false }, "newDashboard": { - "value": false, + "value": true, "description": "Determines whether the new dashboard with cloud integration is enabled." }, "profiling": { From aabd0c8dbc652e24e98da1f9b2aaf0184f2db639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buchowski?= Date: Mon, 22 May 2023 11:36:41 +0200 Subject: [PATCH 16/16] Revert "set newDashboard & authentication flags to true" This reverts commit 5974170866e62d29ae4f719a68b33acd70a4da9d. --- app/ide-desktop/lib/content-config/src/config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ide-desktop/lib/content-config/src/config.json b/app/ide-desktop/lib/content-config/src/config.json index eff732d50106..c513b5e80a99 100644 --- a/app/ide-desktop/lib/content-config/src/config.json +++ b/app/ide-desktop/lib/content-config/src/config.json @@ -1,7 +1,7 @@ { "options": { "authentication": { - "value": true, + "value": false, "description": "Determines whether user authentication is enabled. This option is always true when executed in the cloud." }, "dataCollection": { @@ -116,7 +116,7 @@ "primary": false }, "newDashboard": { - "value": true, + "value": false, "description": "Determines whether the new dashboard with cloud integration is enabled." }, "profiling": {