diff --git a/src/components/timeline/Row.svelte b/src/components/timeline/Row.svelte index 99026bf717..aa5549e01d 100644 --- a/src/components/timeline/Row.svelte +++ b/src/components/timeline/Row.svelte @@ -6,7 +6,7 @@ import { zoom as d3Zoom, zoomIdentity, type D3ZoomEvent, type ZoomBehavior, type ZoomTransform } from 'd3-zoom'; import { pick } from 'lodash-es'; import { createEventDispatcher } from 'svelte'; - import { allResources } from '../../stores/simulation'; + import { allResources, fetchingResources, fetchingResourcesExternal } from '../../stores/simulation'; import { selectedRow } from '../../stores/views'; import type { ActivityDirective, @@ -118,6 +118,7 @@ $: overlaySvgSelection = select(overlaySvg) as Selection; $: rowClasses = classNames('row', { 'row-collapsed': !expanded }); $: hasActivityLayer = !!layers.find(layer => layer.chartType === 'activity'); + $: hasResourceLayer = !!layers.find(layer => layer.chartType === 'line' || layer.chartType === 'x-range'); // Compute scale domains for axes since it is optionally defined in the view $: if ($allResources && yAxes) { @@ -333,6 +334,10 @@ {/if} + + {#if hasResourceLayer && ($fetchingResources || $fetchingResourcesExternal)} +
Loading
+ {/if}
{#each layers as layer (layer.id)} @@ -526,4 +531,30 @@ .active-row :global(.row-header) { background: rgba(47, 128, 237, 0.06); } + + .loading { + align-items: center; + animation: 1s delayVisibility; + color: var(--st-gray-50); + display: flex; + font-size: 10px; + height: 100%; + justify-content: center; + pointer-events: none; + position: absolute; + width: 100%; + z-index: 3; + } + + @keyframes delayVisibility { + 0% { + visibility: hidden; + } + 99% { + visibility: hidden; + } + 100% { + visibility: visible; + } + } diff --git a/src/routes/plans/[id]/+page.svelte b/src/routes/plans/[id]/+page.svelte index 7479fef318..c9669cab1c 100644 --- a/src/routes/plans/[id]/+page.svelte +++ b/src/routes/plans/[id]/+page.svelte @@ -73,6 +73,7 @@ import { enableSimulation, externalResources, + fetchingResources, resetSimulationStores, resourceTypes, resources, @@ -152,6 +153,8 @@ let simulationExtent: string | null; let selectedSimulationStatus: string | null; let windowWidth = 0; + let simulationDataAbortController: AbortController; + let resourcesExternalAbortController: AbortController; $: activityErrorCounts = $activityErrorRollups.reduce( (prevCounts, activityErrorRollup) => { @@ -275,8 +278,16 @@ } $: if ($plan) { + resourcesExternalAbortController?.abort(); + resourcesExternalAbortController = new AbortController(); effects - .getResourcesExternal($plan.id, $simulationDatasetId ?? null, $plan.start_time, data.user) + .getResourcesExternal( + $plan.id, + $simulationDatasetId ?? null, + $plan.start_time, + data.user, + resourcesExternalAbortController.signal, + ) .then(newResources => ($externalResources = newResources)); } @@ -285,16 +296,25 @@ selectActivity(null, null); } - $: if ($plan && $simulationDataset !== undefined) { - if ($simulationDataset !== null && $simulationDatasetId !== -1) { - const datasetId = $simulationDataset.dataset_id; - const startTimeYmd = $simulationDataset?.simulation_start_time ?? $plan.start_time; - effects.getResources(datasetId, startTimeYmd, data.user).then(newResources => ($resources = newResources)); - effects.getSpans(datasetId, data.user).then(newSpans => ($spans = newSpans)); - } else { - $resources = []; - $spans = []; - } + $: if ( + $plan && + $simulationDatasetId !== -1 && + $simulationDataset?.id === $simulationDatasetId && + getSimulationStatus($simulationDataset) === Status.Complete + ) { + const datasetId = $simulationDatasetId; + const startTimeYmd = $simulationDataset?.simulation_start_time ?? $plan.start_time; + simulationDataAbortController?.abort(); + simulationDataAbortController = new AbortController(); + effects + .getResources(datasetId, startTimeYmd, data.user, simulationDataAbortController.signal) + .then(newResources => ($resources = newResources)); + effects.getSpans(datasetId, data.user, simulationDataAbortController.signal).then(newSpans => ($spans = newSpans)); + } else { + simulationDataAbortController?.abort(); + fetchingResources.set(false); + $resources = []; + $spans = []; } $: { diff --git a/src/stores/simulation.ts b/src/stores/simulation.ts index f4d0a96c8d..4a1915d7d5 100644 --- a/src/stores/simulation.ts +++ b/src/stores/simulation.ts @@ -30,6 +30,8 @@ export const resources: Writable = writable([]); export const fetchingResources: Writable = writable(false); +export const fetchingResourcesExternal: Writable = writable(false); + export const resourceTypes: Writable = writable([]); export const spans: Writable = writable([]); diff --git a/src/utilities/effects.ts b/src/utilities/effects.ts index 3d5040f0bc..49718859b2 100644 --- a/src/utilities/effects.ts +++ b/src/utilities/effects.ts @@ -17,7 +17,12 @@ import { import { createModelError, createPlanError, creatingModel, creatingPlan, models } from '../stores/plan'; import { schedulingStatus, selectedSpecId } from '../stores/scheduling'; import { commandDictionaries } from '../stores/sequencing'; -import { fetchingResources, selectedSpanId, simulationDatasetId } from '../stores/simulation'; +import { + fetchingResources, + fetchingResourcesExternal, + selectedSpanId, + simulationDatasetId, +} from '../stores/simulation'; import { createTagError } from '../stores/tags'; import { applyViewUpdate, view, viewUpdateTimeline } from '../stores/views'; import type { @@ -2848,17 +2853,25 @@ const effects = { } }, - async getResources(datasetId: number, startTimeYmd: string, user: User | null): Promise { + async getResources( + datasetId: number, + startTimeYmd: string, + user: User | null, + signal: AbortSignal | undefined = undefined, + ): Promise { try { fetchingResources.set(true); - const data = await reqHasura(gql.GET_PROFILES, { datasetId }, user); + const data = await reqHasura(gql.GET_PROFILES, { datasetId }, user, signal); const { profile: profiles } = data; const sampledProfiles = sampleProfiles(profiles, startTimeYmd); fetchingResources.set(false); return sampledProfiles; } catch (e) { - catchError(e as Error); - fetchingResources.set(false); + const error = e as Error; + if (error.name !== 'AbortError') { + catchError(error); + fetchingResources.set(false); + } return []; } }, @@ -2868,8 +2881,10 @@ const effects = { simulationDatasetId: number | null, startTimeYmd: string, user: User | null, + signal: AbortSignal | undefined = undefined, ): Promise { try { + fetchingResourcesExternal.set(true); const data = await reqHasura( gql.GET_PROFILES_EXTERNAL, { @@ -2882,6 +2897,7 @@ const effects = { : { _eq: simulationDatasetId }, }, user, + signal, ); const { plan_dataset: plan_datasets } = data; if (plan_datasets != null) { @@ -2894,13 +2910,17 @@ const effects = { const sampledResources: Resource[] = sampleProfiles(profiles, startTimeYmd, offset_from_plan_start); resources = [...resources, ...sampledResources]; } - + fetchingResourcesExternal.set(false); return resources; } else { throw Error('Unable to get external resources'); } } catch (e) { - catchError(e as Error); + const error = e as Error; + if (error.name !== 'AbortError') { + catchError(error); + fetchingResourcesExternal.set(false); + } return []; } }, @@ -3004,9 +3024,9 @@ const effects = { } }, - async getSpans(datasetId: number, user: User | null): Promise { + async getSpans(datasetId: number, user: User | null, signal: AbortSignal | undefined = undefined): Promise { try { - const data = await reqHasura(gql.GET_SPANS, { datasetId }, user); + const data = await reqHasura(gql.GET_SPANS, { datasetId }, user, signal); const { span: spans } = data; if (spans != null) { return spans;