From fe160ac287dd58331e5035a46433c2346b53bd2a Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Sat, 29 Jul 2023 18:19:31 +1000 Subject: [PATCH] Fix dashboard regressions (#7414) * Fix dashboard regressions * Restore templates spinner * Fix bug * Fix bug * Faster reactive event implementation * Fix event implementation; fix "New Project" button; fix `uniqueString` * Fix infinitely loading project spinner * Fix infinitely loading template spinner * Minor documentation change * Stop template spinners when switching backends --- .../src/dashboard/components/assetsTable.tsx | 22 +++---- .../src/dashboard/components/dashboard.tsx | 25 +++++-- .../components/directoryNameColumn.tsx | 4 +- .../src/dashboard/components/driveBar.tsx | 6 +- .../src/dashboard/components/driveView.tsx | 14 ++-- .../dashboard/components/fileNameColumn.tsx | 6 +- .../src/dashboard/components/projectIcon.tsx | 66 +++++++++++++++---- .../components/projectNameColumn.tsx | 16 +++-- .../dashboard/components/secretNameColumn.tsx | 6 +- .../src/dashboard/components/templates.tsx | 45 +++++++++++-- .../src/dashboard/events/assetEvent.ts | 3 + .../src/dashboard/events/assetListEvent.ts | 3 + .../src/dashboard/localBackend.ts | 2 + .../src/authentication/src/hooks.tsx | 39 +++++------ .../src/authentication/src/uniqueString.ts | 2 +- 15 files changed, 178 insertions(+), 81 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 fad2d1af347..981a67d98a4 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 @@ -88,7 +88,7 @@ function AssetRow(props: AssetRowProps) { item: rawItem, initialRowState, state: { - assetEvent, + assetEvents, dispatchAssetEvent, dispatchAssetListEvent, markItemAsHidden, @@ -122,7 +122,7 @@ function AssetRow(props: AssetRowProps) { } } - hooks.useEventHandler(assetEvent, async event => { + hooks.useEventHandler(assetEvents, async event => { switch (event.type) { // These events are handled in the specific NameColumn files. case assetEventModule.AssetEventType.createProject: @@ -215,7 +215,7 @@ function AssetRow(props: AssetRowProps) { /** State passed through from a {@link AssetsTable} to every cell. */ export interface AssetsTableState { appRunner: AppRunner | null - assetEvent: assetEventModule.AssetEvent | null + assetEvents: assetEventModule.AssetEvent[] dispatchAssetEvent: (event: assetEventModule.AssetEvent) => void dispatchAssetListEvent: (event: assetListEventModule.AssetListEvent) => void markItemAsHidden: (key: string) => void @@ -253,9 +253,9 @@ export interface AssetsTableProps { items: backendModule.AnyAsset[] filter: ((item: backendModule.AnyAsset) => boolean) | null isLoading: boolean - assetEvent: assetEventModule.AssetEvent | null + assetEvents: assetEventModule.AssetEvent[] dispatchAssetEvent: (event: assetEventModule.AssetEvent) => void - assetListEvent: assetListEventModule.AssetListEvent | null + assetListEvents: assetListEventModule.AssetListEvent[] dispatchAssetListEvent: (event: assetListEventModule.AssetListEvent) => void doOpenIde: (project: backendModule.ProjectAsset) => void doCloseIde: () => void @@ -268,9 +268,9 @@ export default function AssetsTable(props: AssetsTableProps) { items: rawItems, filter, isLoading, - assetEvent, + assetEvents, dispatchAssetEvent, - assetListEvent, + assetListEvents, dispatchAssetListEvent, doOpenIde, doCloseIde: rawDoCloseIde, @@ -288,7 +288,6 @@ export default function AssetsTable(props: AssetsTableProps) { React.useEffect(() => { setInitialized(true) - const extraColumnsJson = localStorage.getItem(EXTRA_COLUMNS_KEY) if (extraColumnsJson != null) { const savedExtraColumns: unknown = JSON.parse(extraColumnsJson) @@ -452,7 +451,7 @@ export default function AssetsTable(props: AssetsTableProps) { [items] ) - hooks.useEventHandler(assetListEvent, event => { + hooks.useEventHandler(assetListEvents, event => { switch (event.type) { case assetListEventModule.AssetListEventType.createDirectory: { const directoryIndices = items @@ -513,6 +512,7 @@ export default function AssetsTable(props: AssetsTableProps) { type: assetEventModule.AssetEventType.createProject, placeholderId: dummyId, templateId: event.templateId, + onSpinnerStateChange: event.onSpinnerStateChange, }) break } @@ -607,7 +607,7 @@ export default function AssetsTable(props: AssetsTableProps) { // The type MUST be here to trigger excess property errors at typecheck time. (): AssetsTableState => ({ appRunner, - assetEvent, + assetEvents, dispatchAssetEvent, dispatchAssetListEvent, markItemAsHidden, @@ -620,7 +620,7 @@ export default function AssetsTable(props: AssetsTableProps) { }), [ appRunner, - assetEvent, + assetEvents, doOpenManually, doOpenIde, doCloseIde, 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 fe5c9de7d94..2be49cdf165 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 @@ -19,6 +19,7 @@ import * as backendProvider from '../../providers/backend' import * as loggerProvider from '../../providers/logger' import * as modalProvider from '../../providers/modal' +import * as spinner from './spinner' import Chat, * as chat from './chat' import DirectoryView from './driveView' import Ide from './ide' @@ -54,7 +55,10 @@ export default function Dashboard(props: DashboardProps) { const { setBackend } = backendProvider.useSetBackend() const { unsetModal } = modalProvider.useSetModal() const [directoryId, setDirectoryId] = React.useState( - session.organization != null ? backendModule.rootDirectoryId(session.organization.id) : null + session.organization != null + ? backendModule.rootDirectoryId(session.organization.id) + : // The local backend uses the empty string as the sole directory ID. + backendModule.DirectoryId('') ) const [query, setQuery] = React.useState('') const [isHelpChatOpen, setIsHelpChatOpen] = React.useState(false) @@ -64,7 +68,7 @@ export default function Dashboard(props: DashboardProps) { const [project, setProject] = React.useState(null) const [nameOfProjectToImmediatelyOpen, setNameOfProjectToImmediatelyOpen] = React.useState(initialProjectName) - const [assetListEvent, dispatchAssetListEvent] = + const [assetListEvents, dispatchAssetListEvent] = hooks.useEvent() const isListingLocalDirectoryAndWillFail = @@ -116,6 +120,7 @@ export default function Dashboard(props: DashboardProps) { backendModule.BackendType.remote ) { setBackend(new localBackend.LocalBackend()) + setDirectoryId(backendModule.DirectoryId('')) } // This hook MUST only run once, on mount. // eslint-disable-next-line react-hooks/exhaustive-deps @@ -184,26 +189,36 @@ export default function Dashboard(props: DashboardProps) { switch (newBackendType) { case backendModule.BackendType.local: setBackend(new localBackend.LocalBackend()) + setDirectoryId(backendModule.DirectoryId('')) break case backendModule.BackendType.remote: { const headers = new Headers() headers.append('Authorization', `Bearer ${session.accessToken ?? ''}`) const client = new http.Client(headers) setBackend(new remoteBackendModule.RemoteBackend(client, logger)) + setDirectoryId( + session.organization != null + ? backendModule.rootDirectoryId(session.organization.id) + : backendModule.DirectoryId('') + ) break } } } }, - [backend.type, logger, session.accessToken, setBackend] + [backend.type, logger, session.accessToken, session.organization, setBackend] ) const doCreateProject = React.useCallback( - (templateId?: string) => { + ( + templateId: string | null, + onSpinnerStateChange?: (state: spinner.SpinnerState) => void + ) => { dispatchAssetListEvent({ type: assetListEventModule.AssetListEventType.createProject, parentId: directoryId, templateId: templateId ?? null, + onSpinnerStateChange: onSpinnerStateChange ?? null, }) }, [directoryId, /* should never change */ dispatchAssetListEvent] @@ -282,7 +297,7 @@ export default function Dashboard(props: DashboardProps) { setNameOfProjectToImmediatelyOpen={setNameOfProjectToImmediatelyOpen} directoryId={directoryId} setDirectoryId={setDirectoryId} - assetListEvent={assetListEvent} + assetListEvents={assetListEvents} dispatchAssetListEvent={dispatchAssetListEvent} query={query} doCreateProject={doCreateProject} diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/directoryNameColumn.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/directoryNameColumn.tsx index 7527bd2fef1..c36ab49bd3b 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/directoryNameColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/directoryNameColumn.tsx @@ -33,7 +33,7 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) { setItem, selected, setSelected, - state: { assetEvent, dispatchAssetListEvent, doToggleDirectoryExpansion, getDepth }, + state: { assetEvents, dispatchAssetListEvent, doToggleDirectoryExpansion, getDepth }, rowState, setRowState, } = props @@ -52,7 +52,7 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) { } } - hooks.useEventHandler(assetEvent, async event => { + hooks.useEventHandler(assetEvents, async event => { switch (event.type) { case assetEventModule.AssetEventType.createProject: case assetEventModule.AssetEventType.uploadFiles: diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/driveBar.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/driveBar.tsx index 8582032470e..506dc53c882 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/driveBar.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/driveBar.tsx @@ -16,7 +16,7 @@ import * as backendProvider from '../../providers/backend' /** Props for a {@link DriveBar}. */ export interface DriveBarProps { - doCreateProject: () => void + doCreateProject: (templateId: string | null) => void doCreateDirectory: () => void doUploadFiles: (files: FileList) => void } @@ -42,7 +42,9 @@ export default function DriveBar(props: DriveBarProps) {
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 bc73f285f5f..93f5d243963 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 @@ -43,11 +43,11 @@ export interface DirectoryViewProps { nameOfProjectToImmediatelyOpen: string | null setNameOfProjectToImmediatelyOpen: (nameOfProjectToImmediatelyOpen: string | null) => void directoryId: backendModule.DirectoryId | null - setDirectoryId: (directoryId: backendModule.DirectoryId | null) => void - assetListEvent: assetListEventModule.AssetListEvent | null + setDirectoryId: (directoryId: backendModule.DirectoryId) => void + assetListEvents: assetListEventModule.AssetListEvent[] dispatchAssetListEvent: (directoryEvent: assetListEventModule.AssetListEvent) => void query: string - doCreateProject: (templateId?: string) => void + doCreateProject: (templateId: string | null) => void doOpenIde: (project: backendModule.ProjectAsset) => void doCloseIde: () => void appRunner: AppRunner | null @@ -67,7 +67,7 @@ export default function DirectoryView(props: DirectoryViewProps) { directoryId, setDirectoryId, query, - assetListEvent, + assetListEvents, dispatchAssetListEvent, doCreateProject, doOpenIde, @@ -87,7 +87,7 @@ export default function DirectoryView(props: DirectoryViewProps) { const [isLoadingAssets, setIsLoadingAssets] = React.useState(true) const [directoryStack, setDirectoryStack] = React.useState([]) const [isFileBeingDragged, setIsFileBeingDragged] = React.useState(false) - const [assetEvent, dispatchAssetEvent] = hooks.useEvent() + const [assetEvents, dispatchAssetEvent] = hooks.useEvent() const assetFilter = React.useMemo(() => { if (query === '') { @@ -275,9 +275,9 @@ export default function DirectoryView(props: DirectoryViewProps) { filter={assetFilter} isLoading={isLoadingAssets} appRunner={appRunner} - assetEvent={assetEvent} + assetEvents={assetEvents} dispatchAssetEvent={dispatchAssetEvent} - assetListEvent={assetListEvent} + assetListEvents={assetListEvents} dispatchAssetListEvent={dispatchAssetListEvent} doOpenIde={doOpenIde} doCloseIde={doCloseIde} diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/fileNameColumn.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/fileNameColumn.tsx index cfcf60268fe..266d231c487 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/fileNameColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/fileNameColumn.tsx @@ -29,7 +29,7 @@ export default function FileNameColumn(props: FileNameColumnProps) { item, setItem, selected, - state: { assetEvent, dispatchAssetListEvent, getDepth }, + state: { assetEvents, dispatchAssetListEvent, getDepth }, rowState, setRowState, } = props @@ -43,7 +43,7 @@ export default function FileNameColumn(props: FileNameColumnProps) { return await Promise.resolve(null) } - hooks.useEventHandler(assetEvent, async event => { + hooks.useEventHandler(assetEvents, async event => { switch (event.type) { case assetEventModule.AssetEventType.createProject: case assetEventModule.AssetEventType.createDirectory: @@ -52,7 +52,7 @@ export default function FileNameColumn(props: FileNameColumnProps) { case assetEventModule.AssetEventType.cancelOpeningAllProjects: case assetEventModule.AssetEventType.deleteMultiple: { // Ignored. These events should all be unrelated to projects. - // `deleteMultiple` is handled in `AssetRow`. + // `deleteMultiple` is handled by `AssetRow`. break } case assetEventModule.AssetEventType.uploadFiles: { 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 1c70c694e8d..0f666ea9534 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 @@ -71,10 +71,11 @@ export enum CheckState { /** Props for a {@link ProjectIcon}. */ export interface ProjectIconProps { + keyProp: string project: backendModule.ProjectAsset rowState: assetsTable.AssetRowState setRowState: React.Dispatch> - assetEvent: assetEventModule.AssetEvent | null + assetEvents: assetEventModule.AssetEvent[] /** Called when the project is opened via the {@link ProjectIcon}. */ doOpenManually: (projectId: backendModule.ProjectId) => void onClose: () => void @@ -85,10 +86,11 @@ export interface ProjectIconProps { /** An interactive icon indicating the status of a project. */ export default function ProjectIcon(props: ProjectIconProps) { const { + keyProp: key, project, rowState, setRowState, - assetEvent, + assetEvents, appRunner, doOpenManually, onClose, @@ -98,9 +100,9 @@ export default function ProjectIcon(props: ProjectIconProps) { const { unsetModal } = modalProvider.useSetModal() const shouldCheckIfActuallyOpen = - backend.type === backendModule.BackendType.remote && - (project.projectState.type === backendModule.ProjectState.opened || - project.projectState.type === backendModule.ProjectState.openInProgress) + project.projectState.type === backendModule.ProjectState.openInProgress || + (backend.type === backendModule.BackendType.remote && + project.projectState.type === backendModule.ProjectState.opened) const [state, setState] = React.useState(() => { if (shouldCheckIfActuallyOpen) { @@ -111,6 +113,9 @@ export default function ProjectIcon(props: ProjectIconProps) { }) const [checkState, setCheckState] = React.useState(CheckState.notChecking) const [spinnerState, setSpinnerState] = React.useState(REMOTE_SPINNER_STATE[state]) + const [onSpinnerStateChange, setOnSpinnerStateChange] = React.useState< + ((state: spinner.SpinnerState | null) => void) | null + >(null) const [shouldOpenWhenReady, setShouldOpenWhenReady] = React.useState(false) const [toastId, setToastId] = React.useState(null) @@ -165,8 +170,18 @@ export default function ProjectIcon(props: ProjectIconProps) { ? REMOTE_SPINNER_STATE[state] : LOCAL_SPINNER_STATE[state] setSpinnerState(newSpinnerState) + onSpinnerStateChange?.( + state === backendModule.ProjectState.closed ? null : newSpinnerState + ) }) - }, [state, backend.type]) + }, [state, backend.type, onSpinnerStateChange]) + + React.useEffect(() => { + onSpinnerStateChange?.(spinner.SpinnerState.initial) + return () => { + onSpinnerStateChange?.(null) + } + }, [onSpinnerStateChange]) React.useEffect(() => { if (toastId != null && state !== backendModule.ProjectState.openInProgress) { @@ -181,15 +196,22 @@ export default function ProjectIcon(props: ProjectIconProps) { } }, [shouldCheckIfActuallyOpen]) - hooks.useEventHandler(assetEvent, event => { + hooks.useEventHandler(assetEvents, event => { switch (event.type) { - default: { - // Ignore; all other events are handled by `ProjectRow`. + case assetEventModule.AssetEventType.createDirectory: + case assetEventModule.AssetEventType.uploadFiles: + case assetEventModule.AssetEventType.createSecret: + case assetEventModule.AssetEventType.deleteMultiple: { + // Ignored. Any missing project-related events should be handled by + // `ProjectNameColumn`. `deleteMultiple` is handled by `AssetRow`. break } case assetEventModule.AssetEventType.openProject: { if (event.id !== project.id) { setShouldOpenWhenReady(false) + if (state === backendModule.ProjectState.opened) { + void closeProject(false) + } } else { setShouldOpenWhenReady(true) void openProject() @@ -201,11 +223,16 @@ export default function ProjectIcon(props: ProjectIconProps) { // to actually cancel an open action. Instead, the project should not be opened // automatically. setShouldOpenWhenReady(false) + onSpinnerStateChange?.(null) + setOnSpinnerStateChange(null) break } case assetEventModule.AssetEventType.createProject: { - if (event.placeholderId === project.id) { + if (event.placeholderId === key) { setState(backendModule.ProjectState.openInProgress) + setOnSpinnerStateChange(() => event.onSpinnerStateChange) + } else if (event.onSpinnerStateChange === onSpinnerStateChange) { + setOnSpinnerStateChange(null) } break } @@ -268,9 +295,8 @@ export default function ProjectIcon(props: ProjectIconProps) { let previousTimestamp = 0 const checkProjectResources = async () => { if (backend.type === backendModule.BackendType.local) { - // This should never happen, but still should be handled. + await backend.openProject(project.id, null, project.title) setState(backendModule.ProjectState.opened) - setCheckState(CheckState.done) } else { try { // This call will error if the VM is not ready yet. @@ -306,13 +332,25 @@ export default function ProjectIcon(props: ProjectIconProps) { } }, [checkState, project.id, project.title, backend]) - const closeProject = async () => { - onClose() + const closeProject = async (triggerOnClose = true) => { + if (triggerOnClose) { + onClose() + } setShouldOpenWhenReady(false) setState(backendModule.ProjectState.closed) + onSpinnerStateChange?.(null) + setOnSpinnerStateChange(null) appRunner?.stopApp() setCheckState(CheckState.notChecking) try { + if ( + backend.type === backendModule.BackendType.local && + state === backendModule.ProjectState.openInProgress + ) { + // Projects that are not opened cannot be closed. + // This is the only way to wait until the project is open. + await backend.openProject(project.id, null, project.title) + } await backend.closeProject(project.id, project.title) } finally { // This is not 100% correct, but it is better than never setting `isRunning` to `false`, 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 c25d483c9f9..e29d9cf8fe6 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 @@ -35,7 +35,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) { setRowState, state: { appRunner, - assetEvent, + assetEvents, dispatchAssetEvent, dispatchAssetListEvent, doOpenManually, @@ -65,7 +65,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) { } } - hooks.useEventHandler(assetEvent, async event => { + hooks.useEventHandler(assetEvents, async event => { switch (event.type) { case assetEventModule.AssetEventType.createDirectory: case assetEventModule.AssetEventType.uploadFiles: @@ -73,12 +73,15 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) { case assetEventModule.AssetEventType.openProject: case assetEventModule.AssetEventType.cancelOpeningAllProjects: case assetEventModule.AssetEventType.deleteMultiple: { - // Ignored. Any missing project-related events should be handled by - // `ProjectIcon`. `deleteMultiple` is handled in `AssetRow`. + // Ignored. Any missing project-related events should be handled by `ProjectIcon`. + // `deleteMultiple` is handled by `AssetRow`. break } case assetEventModule.AssetEventType.createProject: { - if (key === event.placeholderId) { + // This should only run before this project gets replaced with the actual project + // by this event handler. In both cases `key` will match, so using `key` here + // is a mistake. + if (item.id === event.placeholderId) { rowState.setPresence(presence.Presence.inserting) try { const createdProject = await backend.createProject({ @@ -142,10 +145,11 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) { }} > { diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/secretNameColumn.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/secretNameColumn.tsx index 7eb45c53e49..936e329147a 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/secretNameColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/secretNameColumn.tsx @@ -30,7 +30,7 @@ export default function SecretNameColumn(props: SecretNameColumnProps) { item, setItem, selected, - state: { assetEvent, dispatchAssetListEvent, getDepth }, + state: { assetEvents, dispatchAssetListEvent, getDepth }, rowState, setRowState, } = props @@ -44,7 +44,7 @@ export default function SecretNameColumn(props: SecretNameColumnProps) { await Promise.resolve(null) } - hooks.useEventHandler(assetEvent, async event => { + hooks.useEventHandler(assetEvents, async event => { switch (event.type) { case assetEventModule.AssetEventType.createProject: case assetEventModule.AssetEventType.createDirectory: @@ -53,7 +53,7 @@ export default function SecretNameColumn(props: SecretNameColumnProps) { case assetEventModule.AssetEventType.cancelOpeningAllProjects: case assetEventModule.AssetEventType.deleteMultiple: { // Ignored. These events should all be unrelated to secrets. - // `deleteMultiple` is handled in `AssetRow`. + // `deleteMultiple` is handled by `AssetRow`. break } case assetEventModule.AssetEventType.createSecret: { diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/templates.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/templates.tsx index 8a85a794750..0ee4ed40b9f 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/templates.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/templates.tsx @@ -26,6 +26,8 @@ const MAX_WIDTH_NEEDING_SCROLL = 1031 const PADDING_HEIGHT = 16 /** The size (both width and height) of the spinner, in pixels. */ const SPINNER_SIZE = 64 +/** The duration of the "spinner done" animation. */ +const SPINNER_DONE_DURATION_MS = 1000 // ============= // === Types === @@ -101,7 +103,10 @@ export const TEMPLATES: [Template, ...Template[]] = [ /** Props for an {@link EmptyProjectButton}. */ interface InternalEmptyProjectButtonProps { - onTemplateClick: () => void + onTemplateClick: ( + name: null, + onSpinnerStateChange: (spinnerState: spinner.SpinnerState | null) => void + ) => void } /** A button that, when clicked, creates and opens a new blank project. */ @@ -113,7 +118,14 @@ function EmptyProjectButton(props: InternalEmptyProjectButtonProps) {