[Canvas] Loading Indicator + Elements Panel (#32369) (#32448)

* [Canvas] Loading Indicator + Elements Panel
This commit is contained in:
Clint Andrew Hall 2019-03-04 22:13:51 -06:00 committed by GitHub
parent 2633ed6ef4
commit bb3e14348b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 192 additions and 13 deletions

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiStat, EuiTitle } from '@elastic/eui';
import PropTypes from 'prop-types';
import React, { Fragment } from 'react';
export const ElementConfig = ({ elementStats }) => {
if (!elementStats) {
return null;
}
const { total, ready, error } = elementStats;
const progress = Math.round(((ready + error) / total) * 100);
return (
<Fragment>
<EuiTitle size="xs">
<h4>Elements</h4>
</EuiTitle>
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiStat title={total} description="Total" titleSize="s" />
</EuiFlexItem>
<EuiFlexItem>
<EuiStat title={ready} description="Loaded" titleSize="s" />
</EuiFlexItem>
<EuiFlexItem>
<EuiStat title={error} description="Failed" titleSize="s" />
</EuiFlexItem>
<EuiFlexItem>
<EuiStat title={progress + '%'} description="Progress" titleSize="s" />
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
);
};
ElementConfig.propTypes = {
elementStats: PropTypes.object,
};

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { connect } from 'react-redux';
import { getElementStats } from '../../state/selectors/workpad';
import { ElementConfig as Component } from './element_config';
const mapStateToProps = state => ({
elementStats: getElementStats(state),
});
export const ElementConfig = connect(mapStateToProps)(Component);

View file

@ -8,12 +8,13 @@ import { connect } from 'react-redux';
import { fetchAllRenderables } from '../../state/actions/elements';
import { setRefreshInterval } from '../../state/actions/workpad';
import { getInFlight } from '../../state/selectors/resolved_args';
import { getRefreshInterval } from '../../state/selectors/workpad';
import { getRefreshInterval, getElementStats } from '../../state/selectors/workpad';
import { RefreshControl as Component } from './refresh_control';
const mapStateToProps = state => ({
inFlight: getInFlight(state),
refreshInterval: getRefreshInterval(state),
elementStats: getElementStats(state),
});
const mapDispatchToProps = {

View file

@ -4,10 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { EuiButtonEmpty, EuiLoadingSpinner } from '@elastic/eui';
import { EuiButtonEmpty } from '@elastic/eui';
import { Popover } from '../popover';
import { loadingIndicator } from '../../lib/loading_indicator';
import { AutoRefreshControls } from './auto_refresh_controls';
const getRefreshInterval = (val = '') => {
@ -36,19 +37,26 @@ const getRefreshInterval = (val = '') => {
}
};
export const RefreshControl = ({ inFlight, setRefreshInterval, refreshInterval, doRefresh }) => {
export const RefreshControl = ({
inFlight,
elementStats,
setRefreshInterval,
refreshInterval,
doRefresh,
}) => {
const { pending } = elementStats;
if (inFlight || pending > 0) {
loadingIndicator.show();
} else {
loadingIndicator.hide();
}
const setRefresh = val => setRefreshInterval(getRefreshInterval(val));
const popoverButton = handleClick => (
<EuiButtonEmpty size="s" onClick={handleClick}>
<div style={{ display: 'flex', alignItems: 'center' }}>
{inFlight && (
<Fragment>
<EuiLoadingSpinner size="m" /> &nbsp;
</Fragment>
)}
Refresh
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>Refresh</div>
</EuiButtonEmpty>
);

View file

@ -5,6 +5,7 @@
*/
import React from 'react';
import { ElementConfig } from '../element_config';
import { PageConfig } from '../page_config';
import { WorkpadConfig } from '../workpad_config';
import { SidebarSection } from './sidebar_section';
@ -17,5 +18,8 @@ export const GlobalConfig = () => (
<SidebarSection>
<PageConfig />
</SidebarSection>
<SidebarSection>
<ElementConfig />
</SidebarSection>
</div>
);

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-ignore
import { loadingCount } from 'ui/chrome';
let isActive = false;
export const loadingIndicator = {
show: () => {
if (!isActive) {
loadingCount.increment();
isActive = true;
}
},
hide: () => {
if (isActive) {
loadingCount.decrement();
isActive = false;
}
},
};

View file

@ -186,7 +186,11 @@ export const fetchAllRenderables = createThunk(
fetchElementsOnPages([currentPage]).then(() => dispatch(args.inFlightComplete()));
} else {
fetchElementsOnPages([currentPage])
.then(() => fetchElementsOnPages(otherPages))
.then(() => {
return otherPages.reduce((chain, page) => {
return chain.then(() => fetchElementsOnPages([page]));
}, Promise.resolve());
})
.then(() => dispatch(args.inFlightComplete()));
}
}

View file

@ -10,3 +10,4 @@ export const setCanUserWrite = createAction('setCanUserWrite');
export const setFullscreen = createAction('setFullscreen');
export const selectElement = createAction('selectElement');
export const setFirstLoad = createAction('setFirstLoad');
export const setElementStats = createAction('setElementStats');

View file

@ -14,6 +14,12 @@ export const getInitialState = path => {
transient: {
isFirstLoad: true,
canUserWrite: true,
elementStats: {
total: 0,
ready: 0,
pending: 0,
error: 0,
},
fullscreen: false,
selectedElement: null,
resolvedArgs: {},

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { setElementStats } from '../actions/transient';
import { getAllElements, getElementCounts, getElementStats } from '../selectors/workpad';
export const elementStats = ({ dispatch, getState }) => next => action => {
// execute the action
next(action);
// read the new state
const state = getState();
const stats = getElementStats(state);
const total = getAllElements(state).length;
const counts = getElementCounts(state);
const { ready, error } = counts;
// TODO: this should come from getElementStats, once we've gotten element status fixed
const pending = total - ready - error;
if (
total > 0 &&
(ready !== stats.ready ||
pending !== stats.pending ||
error !== stats.error ||
total !== stats.total)
) {
dispatch(setElementStats({ total, ready, pending, error }));
}
};

View file

@ -15,10 +15,12 @@ import { inFlight } from './in_flight';
import { workpadUpdate } from './workpad_update';
import { workpadRefresh } from './workpad_refresh';
import { appReady } from './app_ready';
import { elementStats } from './element_stats';
const middlewares = [
applyMiddleware(
thunkMiddleware,
elementStats,
esPersistMiddleware,
historyMiddleware,
aeroelastic,

View file

@ -47,6 +47,7 @@ function _getValue(hasError, value, oldVal) {
}
function getContext(value, loading = false, oldVal = null) {
// TODO: this is no longer correct.
const hasError = value instanceof Error;
return {
state: _getState(hasError, loading),

View file

@ -40,6 +40,10 @@ export const transientReducer = handleActions(
return set(transientState, 'fullscreen', Boolean(payload));
},
[actions.setElementStats]: (transientState, { payload }) => {
return set(transientState, 'elementStats', payload);
},
[actions.selectElement]: (transientState, { payload }) => {
return {
...transientState,

View file

@ -79,6 +79,35 @@ export function getAllElements(state) {
return getPages(state).reduce((elements, page) => elements.concat(page.elements), []);
}
export function getElementCounts(state) {
const resolvedArgs = get(state, 'transient.resolvedArgs');
const results = {
ready: 0,
pending: 0,
error: 0,
};
Object.keys(resolvedArgs).forEach(resolvedArg => {
const arg = resolvedArgs[resolvedArg];
const { expressionRenderable } = arg;
const { value, state } = expressionRenderable;
if (value && value.as === 'error') {
results.error++;
} else if (state === 'ready') {
results.ready++;
} else {
results.pending++;
}
});
return results;
}
export function getElementStats(state) {
return get(state, 'transient.elementStats');
}
export function getGlobalFilterExpression(state) {
return getAllElements(state)
.map(el => el.filter)