mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* [Canvas] Loading Indicator + Elements Panel
This commit is contained in:
parent
2633ed6ef4
commit
bb3e14348b
14 changed files with 192 additions and 13 deletions
|
@ -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,
|
||||
};
|
|
@ -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);
|
|
@ -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 = {
|
||||
|
|
|
@ -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" />
|
||||
</Fragment>
|
||||
)}
|
||||
Refresh
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>Refresh</div>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
25
x-pack/plugins/canvas/public/lib/loading_indicator.ts
Normal file
25
x-pack/plugins/canvas/public/lib/loading_indicator.ts
Normal 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;
|
||||
}
|
||||
},
|
||||
};
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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 }));
|
||||
}
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue