Skip to content

Commit

Permalink
[feat] loading indicator (#2936)
Browse files Browse the repository at this point in the history
- setLoadingIndicator action
- show Loading... text when async dataset loading is in progress

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>
  • Loading branch information
igorDykhta authored Jan 24, 2025
1 parent 1a68d1b commit 029bcc5
Show file tree
Hide file tree
Showing 13 changed files with 149 additions and 11 deletions.
1 change: 1 addition & 0 deletions src/actions/src/action-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export const ActionTypes = {
SYNC_TIME_FILTER_WITH_LAYER_TIMELINE: `${ACTION_PREFIX}SYNC_TIME_FILTER_WITH_LAYER_TIMELINE`,
SYNC_TIME_FILTER_TIMELINE_MODE: `${ACTION_PREFIX}SYNC_TIME_FILTER_TIMELINE_MODE`,
TOGGLE_PANEL_LIST_VIEW: `${ACTION_PREFIX}TOGGLE_PANEL_LIST_VIEW`,
SET_LOADING_INDICATOR: `${ACTION_PREFIX}SET_LOADING_INDICATOR`,

// uiState > export image
SET_EXPORT_IMAGE_SETTING: `${ACTION_PREFIX}SET_EXPORT_IMAGE_SETTING`,
Expand Down
22 changes: 22 additions & 0 deletions src/actions/src/ui-state-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,3 +495,25 @@ export const togglePanelListView: (
// @ts-ignore
const uiStateActions = null;
/* eslint-enable @typescript-eslint/no-unused-vars */

/** SET_LOADING_INDICATOR */
export type SetLoadingIndicatorAction = {
payload: {
change: number;
};
};

/**
* Change of number of active loading actions.
* @memberof uiStateActions
* @param payload
* @param payload.change Change of number of active loading actions.
* @public
*/
export const setLoadingIndicator: (
payload: SetLoadingIndicatorAction['payload']
) => Merge<SetLoadingIndicatorAction, {type: typeof ActionTypes.SET_LOADING_INDICATOR}> =
createAction(
ActionTypes.SET_LOADING_INDICATOR,
(payload: SetLoadingIndicatorAction['payload']) => ({payload})
);
2 changes: 2 additions & 0 deletions src/components/src/kepler-gl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ export const mapFieldsSelector = (props: KeplerGLProps, index = 0) => ({
mapControls: props.uiState.mapControls,
readOnly: props.uiState.readOnly,
locale: props.uiState.locale,
isLoadingIndicatorVisible: Number(props.uiState.loadingIndicatorValue) > 0,
sidePanelWidth: props.sidePanelWidth ? props.sidePanelWidth : DEFAULT_KEPLER_GL_PROPS.width,

// mapStyle
topMapContainerProps: props.topMapContainerProps,
Expand Down
49 changes: 49 additions & 0 deletions src/components/src/loading-indicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT
// Copyright contributors to the kepler.gl project

import React, {PropsWithChildren} from 'react';
import styled, {withTheme} from 'styled-components';

type StyledContainerProps = {
isVisible?: boolean;
left: number;
};

export const StyledContainer = styled.div<StyledContainerProps>`
position: absolute;
left: ${props => props.left}px;
bottom: ${props => props.theme.sidePanel.margin.left}px;
z-index: 1;
color: ${props => props.theme.textColorHl};
opacity: ${props => (props.isVisible ? 1 : 0)};
transition: opacity 0.5s ease-in-out;
background-color: ${props => props.theme.sidePanelBg};
border-radius: 0px;
padding-left: 3px;
padding-right: 3px;
font-size: 12px;
`;

type LoadingIndicatorProps = {
isVisible?: boolean;
activeSidePanel?: boolean;
sidePanelWidth?: number;
};

const LoadingIndicator: React.FC<PropsWithChildren<LoadingIndicatorProps> & {theme: any}> = ({
isVisible,
children,
activeSidePanel,
sidePanelWidth,
theme
}) => {
const left = (activeSidePanel ? sidePanelWidth || 0 : 0) + theme.sidePanel.margin.left;

return (
<StyledContainer isVisible={isVisible} left={left}>
{children}
</StyledContainer>
);
};

export default withTheme(LoadingIndicator) as React.FC<PropsWithChildren<LoadingIndicatorProps>>;
20 changes: 19 additions & 1 deletion src/components/src/map-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ import {
} from '@kepler.gl/reducers';
import {VisState} from '@kepler.gl/schemas';

import LoadingIndicator from './loading-indicator';

// Debounce the propagation of viewport change and mouse moves to redux store.
// This is to avoid too many renders of other components when the map is
// being panned/zoomed (leading to laggy basemap/deck syncing).
Expand Down Expand Up @@ -324,6 +326,10 @@ export interface MapContainerProps {
deleteMapLabels?: (containerId: string, layerId: string) => void;
containerId?: number;

isLoadingIndicatorVisible?: boolean;
activeSidePanel: string | null;
sidePanelWidth?: number;

locale?: any;
theme?: any;
editor?: any;
Expand Down Expand Up @@ -1022,7 +1028,10 @@ export default function MapContainerFactory(
topMapContainerProps,
theme,
datasetAttributions = [],
containerId = 0
containerId = 0,
isLoadingIndicatorVisible,
activeSidePanel,
sidePanelWidth
} = this.props;

const {layers, datasets, editor, interactionConfig} = visState;
Expand Down Expand Up @@ -1144,6 +1153,15 @@ export default function MapContainerFactory(
)
: null}
{this._renderMapPopover()}
{primary !== isSplit ? (
<LoadingIndicator
isVisible={Boolean(isLoadingIndicatorVisible)}
activeSidePanel={Boolean(activeSidePanel)}
sidePanelWidth={sidePanelWidth}
>
Loading...
</LoadingIndicator>
) : null}
{this.props.primary ? (
<Attribution
showBaseMapLibLogo={this.state.showBaseMapAttribution}
Expand Down
2 changes: 1 addition & 1 deletion src/constants/src/default-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export const KEPLER_GL_WEBSITE = 'http://kepler.gl/';
export const DIMENSIONS = {
sidePanel: {
width: 300,
margin: {top: 20, left: 20, bottom: 30, right: 20},
margin: {top: 12, left: 12, bottom: 12, right: 20},
headerHeight: 96
},
mapControl: {
Expand Down
24 changes: 24 additions & 0 deletions src/reducers/src/ui-state-updaters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -902,3 +902,27 @@ export const togglePanelListViewUpdater = (
[stateProp]: listView
};
};

/**
* Update state of the loading indicator.
* @memberof uiStateUpdaters
* @param state uiState
* @param action
* @param action.payload Payload with change of number of active loading actions.
* @returns nextState
* @public
*/
export const setLoadingIndicatorUpdater = (
state: UiState,
{payload: {change}}: UIStateActions.SetLoadingIndicatorAction
): UiState => {
let {loadingIndicatorValue} = state;
if (!loadingIndicatorValue) {
loadingIndicatorValue = 0;
}

return {
...state,
loadingIndicatorValue: Math.max(loadingIndicatorValue + change, 0)
};
};
4 changes: 3 additions & 1 deletion src/reducers/src/ui-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ const actionHandler = {
[ActionTypes.TOGGLE_SPLIT_MAP]: uiStateUpdaters.toggleSplitMapUpdater,
[ActionTypes.SHOW_DATASET_TABLE]: uiStateUpdaters.showDatasetTableUpdater,
[ActionTypes.SET_LOCALE]: uiStateUpdaters.setLocaleUpdater,
[ActionTypes.TOGGLE_PANEL_LIST_VIEW]: uiStateUpdaters.togglePanelListViewUpdater
[ActionTypes.TOGGLE_PANEL_LIST_VIEW]: uiStateUpdaters.togglePanelListViewUpdater,

[ActionTypes.SET_LOADING_INDICATOR]: uiStateUpdaters.setLoadingIndicatorUpdater
};

/* Reducer */
Expand Down
22 changes: 19 additions & 3 deletions src/reducers/src/vis-state-updaters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
setFilter,
processFileContent,
fitBounds as fitMapBounds,
setLoadingIndicator,
toggleLayerForMap,
applyFilterConfig
} from '@kepler.gl/actions';
Expand Down Expand Up @@ -2327,17 +2328,23 @@ export const updateVisDataUpdater = (
})
: state;

// indicate that something is in progress
const setIsLoadingTask = ACTION_TASK().map(() => {
return setLoadingIndicator({change: 1});
});
const updatedState = withTask(previousState, setIsLoadingTask);

const datasets = toArray(action.datasets);

const allCreateDatasetsTasks = datasets.map(
({info = {}, ...rest}) => createNewDataEntry({info, ...rest}, state.datasets) || {}
);
// call all Tasks
const tasks = Task.allSettled(allCreateDatasetsTasks).map(results =>
const datasetTasks = Task.allSettled(allCreateDatasetsTasks).map(results =>
createNewDatasetSuccess({results, addToMapOptions: options})
);

return withTask(previousState, tasks);
return withTask(updatedState, datasetTasks);
};

export const createNewDatasetSuccessUpdater = (
Expand Down Expand Up @@ -2376,7 +2383,16 @@ export const createNewDatasetSuccessUpdater = (
layerMergers
};

return applyMergersUpdater(mergedState, {mergers: datasetMergers, postMergerPayload});
const updatedState = applyMergersUpdater(mergedState, {
mergers: datasetMergers,
postMergerPayload
});

// resolve active loading initiated by updateVisDataUpdater
const setIsLoadingTask = ACTION_TASK().map(() => {
return setLoadingIndicator({change: -1});
});
return withTask(updatedState, setIsLoadingTask);
};

/**
Expand Down
2 changes: 2 additions & 0 deletions src/types/reducers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,8 @@ export type UiState = {
filterPanelListView: PanelListView;
// side panel close button visibility
isSidePanelCloseButtonVisible: boolean | null;
// positive value indicates that something is loading
loadingIndicatorValue?: number;
};

/** Width of viewport */
Expand Down
4 changes: 2 additions & 2 deletions test/node/reducers/composer-state-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ test('#composerStateReducer - addDataToMapUpdater: mapState should be centered (

// layers should generate a fit bounds task
const tasks = drainTasksForTesting();
t.equal(tasks.length, 1, 'One fit bounds task should be present');
t.equal(tasks.length, 2, 'Should create one fit bounds task and one setLoadingIndicator task');

newState.mapState = mapStateReducer(newState.mapState, succeedTaskWithValues(tasks[0], {}));

Expand Down Expand Up @@ -441,7 +441,7 @@ test('#composerStateReducer - replaceDataInMapUpdater', t => {

// layers should generate a fit bounds task
const tasks = drainTasksForTesting();
t.equal(tasks.length, 1, 'A fit bounds Task should be present');
t.equal(tasks.length, 2, 'Should create one fit bounds task and one setLoadingIndicator task');
nextState = {
...nextState,
mapState: mapStateReducer(nextState.mapState, succeedTaskWithValues(tasks[0], {}))
Expand Down
2 changes: 1 addition & 1 deletion test/node/reducers/vis-state-merger-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2217,7 +2217,7 @@ test('VisStateMerger -> asynс mergers', t => {

t.ok(nextState.test.visState.datasets[testCsvDataId], 'should add csv data');
const tasks = drainTasksForTesting();
t.equal(tasks.length, 1, 'should create 1 task');
t.equal(tasks.length, 2, 'Should create one fit bounds task and one setLoadingIndicator task');
t.equal(tasks[0].type, 'MOCK_MERGE_TASK', 'should create merger task');

// add another dataset will async merger is in process
Expand Down
6 changes: 4 additions & 2 deletions test/node/reducers/vis-state-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5626,6 +5626,8 @@ test('#visStateReducer -> PIN_TABLE_COLUMN', t => {
});

test('#visStateReducer -> LOAD_FILES', async t => {
drainTasksForTesting();

const loadFilesSuccessSpy = sinon.spy(VisStateActions, 'loadFilesSuccess');
const loadFileErrSpy = sinon.spy(Console, 'warn');
const initialState = CloneDeep(InitialState).visState;
Expand Down Expand Up @@ -5653,8 +5655,8 @@ test('#visStateReducer -> LOAD_FILES', async t => {
const nextState = reducer(initialState, VisStateActions.loadFiles(mockFiles));

const tasks = drainTasksForTesting();
t.equal(tasks.length, 2, 'should ceate 2 tasks');
const task1 = tasks[1];
t.equal(tasks.length, 1, 'should create 1 task');
const task1 = tasks[0];

const expectedTask1 = {
type: 'LOAD_FILE_TASK',
Expand Down

0 comments on commit 029bcc5

Please sign in to comment.