diff --git a/src/analysis/Analyses.test.ts b/src/analysis/Analyses.test.ts index 7f0dfb4cff..42169f2c83 100644 --- a/src/analysis/Analyses.test.ts +++ b/src/analysis/Analyses.test.ts @@ -80,6 +80,7 @@ const defaultAnalysesData: AnalysesData = { lastRefresh: null, runtimes: [], refreshRuntimes: () => Promise.resolve(), + isLoadingCloudEnvironments: false, appDataDisks: [], persistentDisks: [], }; diff --git a/src/analysis/Analyses.ts b/src/analysis/Analyses.ts index c6b5d69942..d474fd3f98 100644 --- a/src/analysis/Analyses.ts +++ b/src/analysis/Analyses.ts @@ -428,7 +428,15 @@ export interface SortOrderInfo { export const BaseAnalyses = ( { workspace, - analysesData: { apps, refreshApps, runtimes, refreshRuntimes, appDataDisks, persistentDisks }, + analysesData: { + apps, + refreshApps, + runtimes, + refreshRuntimes, + appDataDisks, + persistentDisks, + isLoadingCloudEnvironments, + }, storageDetails: { googleBucketLocation, azureContainerRegion }, onRequesterPaysError, }: AnalysesProps, @@ -752,6 +760,7 @@ export const BaseAnalyses = ( runtimes, persistentDisks, refreshRuntimes, + isLoadingCloudEnvironments, appDataDisks, refreshAnalyses, analyses, diff --git a/src/analysis/AnalysisLauncher.js b/src/analysis/AnalysisLauncher.js index 83f65e152b..5e755a7742 100644 --- a/src/analysis/AnalysisLauncher.js +++ b/src/analysis/AnalysisLauncher.js @@ -71,7 +71,7 @@ const AnalysisLauncher = _.flow( analysisName, workspace, workspace: { accessLevel, canCompute }, - analysesData: { runtimes, refreshRuntimes, persistentDisks }, + analysesData: { runtimes, refreshRuntimes, persistentDisks, isLoadingCloudEnvironments }, storageDetails: { googleBucketLocation, azureContainerRegion }, }, _ref @@ -117,6 +117,7 @@ const AnalysisLauncher = _.flow( workspace, setCreateOpen, refreshRuntimes, + isLoadingCloudEnvironments, readOnlyAccess: !(canWrite(accessLevel) && canCompute), }), h(AnalysisPreviewFrame, { styles: iframeStyles, analysisName, toolLabel: currentFileToolLabel, workspace }), @@ -166,7 +167,7 @@ const AnalysisLauncher = _.flow( setCreateOpen(false); }, }), - busy && spinnerOverlay, + (busy || isLoadingCloudEnvironments) && spinnerOverlay, ]), ]); } diff --git a/src/analysis/ContextBar.test.ts b/src/analysis/ContextBar.test.ts index 6a9a652516..933428b435 100644 --- a/src/analysis/ContextBar.test.ts +++ b/src/analysis/ContextBar.test.ts @@ -412,6 +412,7 @@ const contextBarProps: ContextBarProps = { refreshRuntimes: () => Promise.resolve(), storageDetails: defaultGoogleBucketOptions, refreshApps: () => Promise.resolve(), + isLoadingCloudEnvironments: false, workspace: defaultGoogleWorkspace, }; @@ -423,6 +424,7 @@ const contextBarPropsForAzure: ContextBarProps = { refreshRuntimes: () => Promise.resolve(), storageDetails: { ...defaultGoogleBucketOptions, ...populatedAzureStorageOptions }, refreshApps: () => Promise.resolve(), + isLoadingCloudEnvironments: false, workspace: defaultAzureWorkspace, }; diff --git a/src/analysis/ContextBar.ts b/src/analysis/ContextBar.ts index c3966d3371..2fc22905f0 100644 --- a/src/analysis/ContextBar.ts +++ b/src/analysis/ContextBar.ts @@ -85,6 +85,7 @@ export interface ContextBarProps { refreshRuntimes: (maybeStale?: boolean) => Promise; storageDetails: StorageDetails; refreshApps: (maybeStale?: boolean) => Promise; + isLoadingCloudEnvironments: boolean; workspace: BaseWorkspace; persistentDisks: PersistentDisk[]; } @@ -96,6 +97,7 @@ export const ContextBar = ({ refreshRuntimes, storageDetails, refreshApps, + isLoadingCloudEnvironments, workspace, persistentDisks, }: ContextBarProps) => { @@ -248,6 +250,7 @@ export const ContextBar = ({ appDataDisks, refreshRuntimes, refreshApps, + isLoadingCloudEnvironments, workspace, canCompute, persistentDisks, diff --git a/src/analysis/modals/AnalysisModal.test.ts b/src/analysis/modals/AnalysisModal.test.ts index 4b29e72c1b..bb3d5e8398 100644 --- a/src/analysis/modals/AnalysisModal.test.ts +++ b/src/analysis/modals/AnalysisModal.test.ts @@ -26,6 +26,7 @@ const defaultGcpModalProps: AnalysisModalProps = { apps: [] as App[], appDataDisks: [], persistentDisks: [], + isLoadingCloudEnvironments: false, onDismiss: () => {}, onError: () => {}, onSuccess: () => {}, diff --git a/src/analysis/modals/AnalysisModal.ts b/src/analysis/modals/AnalysisModal.ts index 97052f64f1..00412e8985 100644 --- a/src/analysis/modals/AnalysisModal.ts +++ b/src/analysis/modals/AnalysisModal.ts @@ -79,6 +79,7 @@ export interface AnalysisModalProps { apps: App[] | undefined; appDataDisks: AppDataDisk[] | undefined; persistentDisks: PersistentDisk[] | undefined; + isLoadingCloudEnvironments: boolean; onDismiss: () => void; onError: () => void; onSuccess: () => void; @@ -94,6 +95,7 @@ export const AnalysisModal = withDisplayName('AnalysisModal')( onError, onSuccess, runtimes, + isLoadingCloudEnvironments, apps, appDataDisks, persistentDisks, @@ -110,18 +112,17 @@ export const AnalysisModal = withDisplayName('AnalysisModal')( const prevAnalysisName = usePrevious(analysisName); const [currentToolObj, setCurrentToolObj] = useState(); const [fileExt, setFileExt] = useState(''); - const currentTool: ToolLabel | undefined = currentToolObj?.label; + const { loadedState, createAnalysis, pendingCreate } = analysisFileStore; + const loading = + isLoadingCloudEnvironments || loadedState.status === 'Loading' || pendingCreate.status === 'Loading'; + + const currentTool: ToolLabel | undefined = currentToolObj?.label; const currentRuntime: Runtime | undefined = getCurrentRuntime(runtimes); const currentDisk = getCurrentPersistentDisk(runtimes, persistentDisks); const currentRuntimeToolLabel = currentRuntime && getToolLabelFromCloudEnv(currentRuntime); const currentApp = (toolLabel: AppToolLabel): App | undefined => getCurrentApp(toolLabel, apps); - // TODO: Bring in as props from Analyses OR bring entire AnalysisFileStore from props. - const { loadedState, createAnalysis, pendingCreate } = analysisFileStore; - // TODO: When the above is done, this check below may not be necessary. - const analyses = loadedState.status !== 'None' ? loadedState.state : null; - const status = loadedState.status; const resetView = () => { setViewMode(undefined); setAnalysisName(''); @@ -220,6 +221,7 @@ export const AnalysisModal = withDisplayName('AnalysisModal')( tool: currentTool, currentRuntime, currentDisk, + isLoadingCloudEnvironments, onDismiss, onError, onSuccess, @@ -232,6 +234,7 @@ export const AnalysisModal = withDisplayName('AnalysisModal')( tool: currentTool, currentRuntime, currentDisk: persistentDisks ? persistentDisks[0] : undefined, + isLoadingCloudEnvironments, onDismiss, onSuccess, }); @@ -297,6 +300,8 @@ export const AnalysisModal = withDisplayName('AnalysisModal')( Clickable, { style: styles.toolCard, + disabled: loading, + tooltip: loading ? 'Loading' : runtimeTool.label, onClick: () => { setCurrentToolObj(runtimeTool); setFileExt(runtimeTool.defaultExt); @@ -325,8 +330,8 @@ export const AnalysisModal = withDisplayName('AnalysisModal')( enterNextViewMode(appTool.label); }, hover: !currentApp ? styles.hover : undefined, - disabled: !!currentApp, - tooltip: currentApp ? appDisabledMessages[appTool.label] : '', + disabled: !!currentApp || loading, + tooltip: currentApp ? appDisabledMessages[appTool.label] : loading ? 'Loading' : '', key: appTool.label, }, [toolImages[appTool.label]] @@ -355,6 +360,7 @@ export const AnalysisModal = withDisplayName('AnalysisModal')( accept: `.${runtimeTools.Jupyter.ext.join(', .')}, .${runtimeTools.RStudio.ext.join(', .')}`, style: { flexGrow: 1, backgroundColor: colors.light(), height: '100%' }, activeStyle: { backgroundColor: colors.accent(0.2), cursor: 'copy' }, + disabled: loading, onDropRejected: () => reportError( 'Not a valid analysis file', @@ -446,7 +452,9 @@ export const AnalysisModal = withDisplayName('AnalysisModal')( const errors = validate( { analysisName: `${analysisName}.${fileExt}`, notebookKernel }, { - analysisName: analysisNameValidator(_.map(({ name }) => getFileName(name), analyses)), + analysisName: analysisNameValidator( + _.map(({ name }) => getFileName(name), loadedState.status === 'None' ? [] : loadedState.state) + ), notebookKernel: { presence: { allowEmpty: true } }, } ); @@ -509,9 +517,8 @@ export const AnalysisModal = withDisplayName('AnalysisModal')( h( ButtonPrimary, { - // TODO: See spinner overlay comment. Change to pendingCreate.status === 'Loading' || errors. - disabled: status === 'Loading' || pendingCreate.status === 'Loading' || errors, - tooltip: Utils.summarizeErrors(errors), + disabled: loading || errors, + tooltip: errors ? Utils.summarizeErrors(errors) : loading ? 'Loading' : '', onClick: async () => { try { const contents = Utils.cond( @@ -536,10 +543,7 @@ export const AnalysisModal = withDisplayName('AnalysisModal')( ['Create Analysis'] ), ]), - // TODO: Once Analyses.js is converted to implement useAnalysisFiles and refresh is called within create, - // change next line to pendingCreate.status === 'Loading' && spinnerOverlay - // Currently this will be close enough to the desired functionality. - (status === 'Loading' || pendingCreate.status === 'Loading') && spinnerOverlay, + loading && spinnerOverlay, ]); }; diff --git a/src/analysis/modals/CloudEnvironmentModal.ts b/src/analysis/modals/CloudEnvironmentModal.ts index bf0200bf8f..d46256cbf1 100644 --- a/src/analysis/modals/CloudEnvironmentModal.ts +++ b/src/analysis/modals/CloudEnvironmentModal.ts @@ -72,6 +72,7 @@ export const CloudEnvironmentModal = ({ appDataDisks, refreshRuntimes, refreshApps, + isLoadingCloudEnvironments, workspace, persistentDisks, // Note: for Azure environments `location` and `computeRegion` are identical @@ -88,6 +89,7 @@ export const CloudEnvironmentModal = ({ appDataDisks: PersistentDisk[]; refreshRuntimes: () => Promise; refreshApps: () => Promise; + isLoadingCloudEnvironments: boolean; workspace: BaseWorkspace; persistentDisks: PersistentDisk[]; location: string; @@ -115,6 +117,7 @@ export const CloudEnvironmentModal = ({ tool, currentRuntime, currentDisk, + isLoadingCloudEnvironments, location, onDismiss, onSuccess, @@ -128,6 +131,7 @@ export const CloudEnvironmentModal = ({ workspace, currentRuntime, currentDisk: getReadyPersistentDisk(persistentDisks), + isLoadingCloudEnvironments, location, tool, onDismiss, diff --git a/src/analysis/modals/ComputeModal/AzureComputeModal/AzureComputeModal.js b/src/analysis/modals/ComputeModal/AzureComputeModal/AzureComputeModal.js index 429cdce193..3666d20b05 100644 --- a/src/analysis/modals/ComputeModal/AzureComputeModal/AzureComputeModal.js +++ b/src/analysis/modals/ComputeModal/AzureComputeModal/AzureComputeModal.js @@ -1,5 +1,5 @@ import _ from 'lodash/fp'; -import { Fragment, useState } from 'react'; +import { Fragment, useEffect, useState } from 'react'; import { div, h, label, p, span } from 'react-hyperscript-helpers'; import { AboutPersistentDiskView } from 'src/analysis/modals/ComputeModal/AboutPersistentDiskView'; import { AzurePersistentDiskSection } from 'src/analysis/modals/ComputeModal/AzureComputeModal/AzurePersistentDiskSection'; @@ -48,11 +48,13 @@ export const AzureComputeModalBase = ({ workspace, currentRuntime, currentDisk, + isLoadingCloudEnvironments, location, tool, hideCloseButton = false, }) => { - const [loading, setLoading] = useState(false); + const [_loading, setLoading] = useState(false); + const loading = _loading || isLoadingCloudEnvironments; const [viewMode, setViewMode] = useState(undefined); const [currentRuntimeDetails, setCurrentRuntimeDetails] = useState(currentRuntime); const [currentPersistentDiskDetails] = useState(currentDisk); @@ -64,15 +66,21 @@ export const AzureComputeModalBase = ({ const hasGpu = () => !!azureMachineTypes[computeConfig.machineType]?.hasGpu; // Lifecycle useOnMount(() => { - const loadCloudEnvironment = _.flow( - withErrorReportingInModal('Error loading cloud environment', onError), - Utils.withBusyState(setLoading) - )(async () => { - const runtimeDetails = currentRuntime ? await Ajax().Runtimes.runtimeV2(workspaceId, currentRuntime.runtimeName).details() : null; + const announceModalOpen = async () => { Ajax().Metrics.captureEvent(Events.cloudEnvironmentConfigOpen, { existingConfig: !!currentRuntime, ...extractWorkspaceDetails(workspace.workspace), }); + }; + announceModalOpen(); + }); + + useEffect(() => { + const refreshRuntime = _.flow( + withErrorReportingInModal('Error loading cloud environment', onError), + Utils.withBusyState(setLoading) + )(async () => { + const runtimeDetails = currentRuntime ? await Ajax().Runtimes.runtimeV2(workspaceId, currentRuntime.runtimeName).details() : null; setCurrentRuntimeDetails(runtimeDetails); setComputeConfig({ machineType: runtimeDetails?.runtimeConfig?.machineType || defaultAzureMachineType, @@ -83,8 +91,8 @@ export const AzureComputeModalBase = ({ autopauseThreshold: runtimeDetails ? runtimeDetails.autopauseThreshold || autopauseDisabledValue : defaultAutopauseThreshold, }); }); - loadCloudEnvironment(); - }); + refreshRuntime(); + }, [currentRuntime, location, onError, workspaceId, setCurrentRuntimeDetails, setComputeConfig]); const renderTitleAndTagline = () => { return h(Fragment, [ @@ -106,6 +114,7 @@ export const AzureComputeModalBase = ({ ButtonOutline, { onClick: () => setViewMode('deleteEnvironment'), + disabled: loading, }, [ Utils.cond( @@ -280,8 +289,11 @@ export const AzureComputeModalBase = ({ const renderActionButton = () => { const commonButtonProps = { tooltipSide: 'left', - disabled: Utils.cond([viewMode === 'deleteEnvironment', () => getIsRuntimeBusy(currentRuntimeDetails)], () => doesRuntimeExist()), + disabled: Utils.cond([loading, true], [viewMode === 'deleteEnvironment', () => getIsRuntimeBusy(currentRuntimeDetails)], () => + doesRuntimeExist() + ), tooltip: Utils.cond( + [loading, 'Loading cloud environments'], [viewMode === 'deleteEnvironment', () => (getIsRuntimeBusy(currentRuntimeDetails) ? 'Cannot delete a runtime while it is busy' : undefined)], [doesRuntimeExist(), () => 'Update not supported for azure runtimes'], () => undefined diff --git a/src/analysis/modals/ComputeModal/AzureComputeModal/AzureComputeModal.test.js b/src/analysis/modals/ComputeModal/AzureComputeModal/AzureComputeModal.test.js index b15e4aef0e..ff0464586a 100644 --- a/src/analysis/modals/ComputeModal/AzureComputeModal/AzureComputeModal.test.js +++ b/src/analysis/modals/ComputeModal/AzureComputeModal/AzureComputeModal.test.js @@ -61,6 +61,7 @@ const defaultAjaxImpl = { }; const verifyEnabled = (item) => expect(item).not.toHaveAttribute('disabled'); +const verifyDisabled = (item) => expect(item).toHaveAttribute('disabled'); describe('AzureComputeModal', () => { beforeAll(() => {}); @@ -93,6 +94,19 @@ describe('AzureComputeModal', () => { expect(deleteButton).toBeNull(); }); + it('disables submit while loading', async () => { + // Arrange + + // Act + await act(async () => { + render(h(AzureComputeModalBase, { ...defaultModalProps, isLoadingCloudEnvironments: true })); + }); + + // Assert + verifyDisabled(getCreateButton()); + screen.getByText('Azure Cloud Environment'); + }); + it('sends the proper leo API call in default create case (no runtimes or disks)', async () => { // Arrange const user = userEvent.setup(); diff --git a/src/analysis/modals/ComputeModal/GcpComputeModal/GcpComputeModal.js b/src/analysis/modals/ComputeModal/GcpComputeModal/GcpComputeModal.js index c634d22c67..331db1c694 100644 --- a/src/analysis/modals/ComputeModal/GcpComputeModal/GcpComputeModal.js +++ b/src/analysis/modals/ComputeModal/GcpComputeModal/GcpComputeModal.js @@ -215,6 +215,7 @@ export const GcpComputeModalBase = ({ onSuccess, currentRuntime, currentDisk, + isLoadingCloudEnvironments, tool, workspace, location, @@ -222,7 +223,8 @@ export const GcpComputeModalBase = ({ }) => { // State -- begin const [showDebugger, setShowDebugger] = useState(false); - const [loading, setLoading] = useState(false); + const [_loading, setLoading] = useState(false); + const loading = _loading || isLoadingCloudEnvironments; const [currentRuntimeDetails, setCurrentRuntimeDetails] = useState(currentRuntime); const [currentPersistentDiskDetails, setCurrentPersistentDiskDetails] = useState(currentDisk); const [viewMode, setViewMode] = useState(undefined); @@ -839,6 +841,7 @@ export const GcpComputeModalBase = ({ const { runtime: existingRuntime, hasGpu } = getExistingEnvironmentConfig(); const { runtime: desiredRuntime } = getDesiredEnvironmentConfig(); const commonButtonProps = Utils.cond( + [loading, () => ({ disabled: true, tooltip: 'Loading cloud environments' })], [ hasGpu && viewMode !== 'deleteEnvironment', () => ({ disabled: true, tooltip: 'Cloud compute with GPU(s) cannot be updated. Please delete it and create a new one.' }), @@ -1589,6 +1592,7 @@ export const GcpComputeModalBase = ({ ButtonOutline, { onClick: () => setViewMode('deleteEnvironment'), + disabled: isLoadingCloudEnvironments, }, [ Utils.cond( diff --git a/src/analysis/modals/ComputeModal/GcpComputeModal/GcpComputeModal.test.js b/src/analysis/modals/ComputeModal/GcpComputeModal/GcpComputeModal.test.js index 6b1f26f753..d5158c90e8 100644 --- a/src/analysis/modals/ComputeModal/GcpComputeModal/GcpComputeModal.test.js +++ b/src/analysis/modals/ComputeModal/GcpComputeModal/GcpComputeModal.test.js @@ -62,6 +62,7 @@ const defaultModalProps = { onError: jest.fn(), currentRuntime: undefined, currentDisk: undefined, + isLoadingCloudEnvironments: false, tool: runtimeToolLabels.Jupyter, workspace: defaultGoogleWorkspace, location: testDefaultLocation, @@ -135,6 +136,19 @@ describe('GcpComputeModal', () => { screen.getByText('Jupyter Cloud Environment'); }); + it('disables submit while loading', async () => { + // Arrange + + // Act + await act(async () => { + render(h(GcpComputeModalBase, { ...defaultModalProps, isLoadingCloudEnvironments: true })); + }); + + // Assert + verifyDisabled(getCreateButton()); + screen.getByText('Jupyter Cloud Environment'); + }); + it('passes the TERRA_DEPLOYMENT_ENV and DRS_RESOLVER_ENDPOINT env vars through to the notebook through custom env vars', async () => { // Arrange const user = userEvent.setup(); diff --git a/src/pages/workspaces/workspace/workflows/WorkflowView.test.js b/src/pages/workspaces/workspace/workflows/WorkflowView.test.js index 17c87150e7..46b0579a48 100644 --- a/src/pages/workspaces/workspace/workflows/WorkflowView.test.js +++ b/src/pages/workspaces/workspace/workflows/WorkflowView.test.js @@ -350,5 +350,5 @@ describe('Workflow View (GCP)', () => { name: 'echo_to_file-configured', namespace: 'gatk', }); - }); + }, 10000); }); diff --git a/src/workflows-app/FeaturedWorkflows.test.ts b/src/workflows-app/FeaturedWorkflows.test.ts index e2f34ab49d..d90495bb09 100644 --- a/src/workflows-app/FeaturedWorkflows.test.ts +++ b/src/workflows-app/FeaturedWorkflows.test.ts @@ -17,6 +17,7 @@ const defaultAnalysesData: AnalysesData = { refreshRuntimes: () => Promise.resolve(), appDataDisks: [], persistentDisks: [], + isLoadingCloudEnvironments: false, }; jest.mock('src/libs/config', () => ({ diff --git a/src/workflows-app/WorkflowsInWorkspace.test.ts b/src/workflows-app/WorkflowsInWorkspace.test.ts index 13f12b258d..8b84694e88 100644 --- a/src/workflows-app/WorkflowsInWorkspace.test.ts +++ b/src/workflows-app/WorkflowsInWorkspace.test.ts @@ -18,6 +18,7 @@ const defaultAnalysesData: AnalysesData = { refreshRuntimes: () => Promise.resolve(), appDataDisks: [], persistentDisks: [], + isLoadingCloudEnvironments: false, }; jest.mock('src/libs/config', () => ({ diff --git a/src/workflows-app/components/WorkflowsAppNavPanel.test.ts b/src/workflows-app/components/WorkflowsAppNavPanel.test.ts index 77cdd5169c..8a87560f60 100644 --- a/src/workflows-app/components/WorkflowsAppNavPanel.test.ts +++ b/src/workflows-app/components/WorkflowsAppNavPanel.test.ts @@ -15,6 +15,7 @@ const defaultAnalysesData: AnalysesData = { refreshRuntimes: () => Promise.resolve(), appDataDisks: [], persistentDisks: [], + isLoadingCloudEnvironments: false, }; const defaultAnalysesDataWithAppsRefreshed: AnalysesData = { diff --git a/src/workspace-data/data-table/shared/DataTable.test.ts b/src/workspace-data/data-table/shared/DataTable.test.ts index ec29033392..1b85b03687 100644 --- a/src/workspace-data/data-table/shared/DataTable.test.ts +++ b/src/workspace-data/data-table/shared/DataTable.test.ts @@ -199,7 +199,7 @@ describe('DataTable', () => { // Get the checkboxes on this page const newPageChecks = screen.getAllByRole('checkbox', { checked: true }); expect(newPageChecks.length).toEqual(101); - }, 10000); + }, 20000); it('selects page', async () => { // Arrange diff --git a/src/workspaces/common/state/useCloudEnvironmentPolling.ts b/src/workspaces/common/state/useCloudEnvironmentPolling.ts index 3369b53417..9a12e86333 100644 --- a/src/workspaces/common/state/useCloudEnvironmentPolling.ts +++ b/src/workspaces/common/state/useCloudEnvironmentPolling.ts @@ -13,6 +13,7 @@ export interface CloudEnvironmentDetails { refreshRuntimes: (maybeStale?: boolean) => Promise; persistentDisks?: PersistentDisk[]; appDataDisks?: PersistentDisk[]; + isLoadingCloudEnvironments: boolean; } export const useCloudEnvironmentPolling = ( @@ -29,6 +30,7 @@ export const useCloudEnvironmentPolling = ( const timeout = useRef(); const [runtimes, setRuntimes] = useState(); + const [isLoadingCloudEnvironments, setIsLoadingCloudEnvironments] = useState(true); const [persistentDisks, setPersistentDisks] = useState(); const [appDataDisks, setAppDataDisks] = useState(); @@ -41,6 +43,7 @@ export const useCloudEnvironmentPolling = ( }; const load = async (maybeStale?: boolean): Promise => { try { + setIsLoadingCloudEnvironments(true); const cloudEnvFilters = _.pickBy((l) => !_.isUndefined(l), { role: 'creator', saturnWorkspaceName, @@ -63,6 +66,7 @@ export const useCloudEnvironmentPolling = ( setAppDataDisks(_.remove((disk) => _.isUndefined(getDiskAppType(disk)), newDisks)); setPersistentDisks(_.filter((disk) => _.isUndefined(getDiskAppType(disk)), newDisks)); const runtime = getCurrentRuntime(newRuntimes); + setIsLoadingCloudEnvironments(false); reschedule( maybeStale || ['Creating', 'Starting', 'Stopping', 'Updating', 'LeoReconfiguring'].includes( @@ -94,5 +98,5 @@ export const useCloudEnvironmentPolling = ( }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [name, namespace, workspace]); - return { runtimes, refreshRuntimes, persistentDisks, appDataDisks }; + return { runtimes, refreshRuntimes, persistentDisks, appDataDisks, isLoadingCloudEnvironments }; }; diff --git a/src/workspaces/container/WorkspaceContainer.test.ts b/src/workspaces/container/WorkspaceContainer.test.ts index 3996a1f59f..9c403751ef 100644 --- a/src/workspaces/container/WorkspaceContainer.test.ts +++ b/src/workspaces/container/WorkspaceContainer.test.ts @@ -84,6 +84,7 @@ describe('WorkspaceContainer', () => { refreshApps: () => Promise.resolve(), lastRefresh: null, refreshRuntimes: () => Promise.resolve(), + isLoadingCloudEnvironments: false, }, }; // Act @@ -112,6 +113,7 @@ describe('WorkspaceContainer', () => { refreshApps: () => Promise.resolve(), refreshRuntimes: () => Promise.resolve(), lastRefresh: null, + isLoadingCloudEnvironments: false, }, }; // Act @@ -156,6 +158,7 @@ describe('WorkspaceContainer', () => { refreshApps: () => Promise.resolve(), refreshRuntimes: () => Promise.resolve(), lastRefresh: null, + isLoadingCloudEnvironments: false, }, }; @@ -209,6 +212,7 @@ describe('WorkspaceContainer', () => { refreshApps: () => Promise.resolve(), refreshRuntimes: () => Promise.resolve(), lastRefresh: null, + isLoadingCloudEnvironments: false, }, }; @@ -282,6 +286,7 @@ describe('WorkspaceContainer', () => { refreshApps: () => Promise.resolve(), refreshRuntimes: () => Promise.resolve(), lastRefresh: null, + isLoadingCloudEnvironments: false, }, }; @@ -354,6 +359,7 @@ describe('WorkspaceContainer', () => { refreshApps: () => Promise.resolve(), refreshRuntimes: () => Promise.resolve(), lastRefresh: null, + isLoadingCloudEnvironments: false, }, }; @@ -399,6 +405,7 @@ describe('WorkspaceContainer', () => { refreshApps: () => Promise.resolve(), refreshRuntimes: () => Promise.resolve(), lastRefresh: null, + isLoadingCloudEnvironments: false, }, }; diff --git a/src/workspaces/container/WorkspaceContainer.ts b/src/workspaces/container/WorkspaceContainer.ts index cd2b3febcf..4766a9217d 100644 --- a/src/workspaces/container/WorkspaceContainer.ts +++ b/src/workspaces/container/WorkspaceContainer.ts @@ -79,7 +79,15 @@ export const WorkspaceContainer = (props: WorkspaceContainerProps) => { breadcrumbs, title, activeTab, - analysesData: { apps = [], refreshApps, runtimes = [], refreshRuntimes, appDataDisks = [], persistentDisks = [] }, + analysesData: { + apps = [], + refreshApps, + runtimes = [], + refreshRuntimes, + appDataDisks = [], + persistentDisks = [], + isLoadingCloudEnvironments, + }, storageDetails, refresh, workspace, @@ -140,6 +148,7 @@ export const WorkspaceContainer = (props: WorkspaceContainerProps) => { runtimes, persistentDisks, refreshRuntimes, + isLoadingCloudEnvironments, storageDetails, }), ]), @@ -253,11 +262,8 @@ export const wrapWorkspace = (opts: WrapWorkspaceOptions): WrapWorkspaceFn => { namespace, name ); - const { runtimes, refreshRuntimes, persistentDisks, appDataDisks } = useCloudEnvironmentPolling( - name, - namespace, - workspace - ); + const { runtimes, refreshRuntimes, persistentDisks, appDataDisks, isLoadingCloudEnvironments } = + useCloudEnvironmentPolling(name, namespace, workspace); const { apps, refreshApps, lastRefresh } = useAppPolling(name, namespace, workspace); if (accessError) { @@ -274,7 +280,16 @@ export const wrapWorkspace = (opts: WrapWorkspaceOptions): WrapWorkspaceFn => { refreshWorkspace, title: _.isFunction(title) ? title(props) : title, breadcrumbs: breadcrumbs(props), - analysesData: { apps, refreshApps, lastRefresh, runtimes, refreshRuntimes, appDataDisks, persistentDisks }, + analysesData: { + apps, + refreshApps, + lastRefresh, + runtimes, + refreshRuntimes, + appDataDisks, + persistentDisks, + isLoadingCloudEnvironments, + }, storageDetails, refresh: async () => { await refreshWorkspace(); @@ -297,6 +312,7 @@ export const wrapWorkspace = (opts: WrapWorkspaceOptions): WrapWorkspaceFn => { refreshRuntimes, appDataDisks, persistentDisks, + isLoadingCloudEnvironments, }, storageDetails, ...props,