[Infra UI] Handle no metrics data gracefully (#29424)
* Use metric_time prefix for metric_time actions * Add refetch capabilities to with_metrics * Pass through refetch to index * Add no data message and refetch button to metrics/index Next stage: Extract into shared component between metrics and waffle map * Remove "refetching" and just use "loading" No need for the explictness, just reuse the loading state, as there's no value to differentiating refetching vs loading here * Add no_data component * Amend for Prettier * Use no_data component * Change naming to better reflect current components * Introduce notion of two empty states: no indices and no data Also changes nodes_overview and logs to use moved component * Add no_data functionality * Remove metrics specific no data component, use the new shared component * Change import order * Use no_data component in nodes_overview * Refactor logs to use shared no data component * Import intl * Revert testing value * Pass handler correctly to avoid circular references * Satisfy type check * Remove one last unused component
This commit is contained in:
parent
93fef68647
commit
732915ee2a
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { NoIndices } from './no_indices';
|
||||
export { NoData } from './no_data';
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface NoDataProps {
|
||||
titleText: string;
|
||||
bodyText: string;
|
||||
refetchText: string;
|
||||
onRefetch: () => void;
|
||||
testString?: string;
|
||||
}
|
||||
|
||||
export const NoData: React.SFC<NoDataProps> = ({
|
||||
titleText,
|
||||
bodyText,
|
||||
refetchText,
|
||||
onRefetch,
|
||||
testString,
|
||||
}) => (
|
||||
<CenteredEmptyPrompt
|
||||
title={<h2>{titleText}</h2>}
|
||||
titleSize="m"
|
||||
body={<p>{bodyText}</p>}
|
||||
actions={
|
||||
<EuiButton iconType="refresh" color="primary" fill onClick={onRefetch}>
|
||||
{refetchText}
|
||||
</EuiButton>
|
||||
}
|
||||
data-test-subj={testString}
|
||||
/>
|
||||
);
|
||||
|
||||
const CenteredEmptyPrompt = styled(EuiEmptyPrompt)`
|
||||
align-self: center;
|
||||
`;
|
|
@ -8,7 +8,7 @@ import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface EmptyPageProps {
|
||||
interface NoIndicesProps {
|
||||
message: string;
|
||||
title: string;
|
||||
actionLabel: string;
|
||||
|
@ -16,7 +16,7 @@ interface EmptyPageProps {
|
|||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
export const EmptyPage: React.SFC<EmptyPageProps> = ({
|
||||
export const NoIndices: React.SFC<NoIndicesProps> = ({
|
||||
actionLabel,
|
||||
actionUrl,
|
||||
message,
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import * as React from 'react';
|
||||
|
||||
interface LogTextStreamEmptyViewProps {
|
||||
reload: () => void;
|
||||
}
|
||||
|
||||
export class LogTextStreamEmptyView extends React.PureComponent<LogTextStreamEmptyViewProps> {
|
||||
public render() {
|
||||
const { reload } = this.props;
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logs.emptyView.noLogMessageTitle"
|
||||
defaultMessage="There are no log messages to display."
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
titleSize="m"
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logs.emptyView.noLogMessageDescription"
|
||||
defaultMessage="Try adjusting your filter."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
actions={
|
||||
<EuiButton iconType="refresh" color="primary" fill onClick={reload}>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logs.emptyView.checkForNewDataButtonLabel"
|
||||
defaultMessage="Check for new data"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,14 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { TextScale } from '../../../../common/log_text_scale';
|
||||
import { TimeKey } from '../../../../common/time';
|
||||
import { callWithoutRepeats } from '../../../utils/handlers';
|
||||
import { NoData } from '../../empty_states';
|
||||
import { InfraLoadingPanel } from '../../loading';
|
||||
import { LogTextStreamEmptyView } from './empty_view';
|
||||
import { getStreamItemBeforeTimeKey, getStreamItemId, parseStreamItemId, StreamItem } from './item';
|
||||
import { LogTextStreamItemView } from './item_view';
|
||||
import { LogTextStreamLoadingItemView } from './loading_item_view';
|
||||
|
@ -44,6 +44,7 @@ interface ScrollableLogTextStreamViewProps {
|
|||
loadNewerItems: () => void;
|
||||
setFlyoutItem: (id: string) => void;
|
||||
showFlyout: () => void;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
interface ScrollableLogTextStreamViewState {
|
||||
|
@ -51,7 +52,7 @@ interface ScrollableLogTextStreamViewState {
|
|||
targetId: string | null;
|
||||
}
|
||||
|
||||
export class ScrollableLogTextStreamView extends React.PureComponent<
|
||||
class ScrollableLogTextStreamViewClass extends React.PureComponent<
|
||||
ScrollableLogTextStreamViewProps,
|
||||
ScrollableLogTextStreamViewState
|
||||
> {
|
||||
|
@ -100,6 +101,7 @@ export class ScrollableLogTextStreamView extends React.PureComponent<
|
|||
hasMoreAfterEnd,
|
||||
isStreaming,
|
||||
lastLoadedTime,
|
||||
intl,
|
||||
} = this.props;
|
||||
const { targetId } = this.state;
|
||||
const hasItems = items.length > 0;
|
||||
|
@ -117,7 +119,24 @@ export class ScrollableLogTextStreamView extends React.PureComponent<
|
|||
/>
|
||||
);
|
||||
} else if (!hasItems) {
|
||||
return <LogTextStreamEmptyView reload={this.handleReload} />;
|
||||
return (
|
||||
<NoData
|
||||
titleText={intl.formatMessage({
|
||||
id: 'xpack.infra.logs.emptyView.noLogMessageTitle',
|
||||
defaultMessage: 'There are no log messages to display.',
|
||||
})}
|
||||
bodyText={intl.formatMessage({
|
||||
id: 'xpack.infra.logs.emptyView.noLogMessageDescription',
|
||||
defaultMessage: 'Try adjusting your filter.',
|
||||
})}
|
||||
refetchText={intl.formatMessage({
|
||||
id: 'xpack.infra.logs.emptyView.checkForNewDataButtonLabel',
|
||||
defaultMessage: 'Check for new data',
|
||||
})}
|
||||
onRefetch={this.handleReload}
|
||||
testString="logsNoDataPrompt"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<VerticalScrollPanel
|
||||
|
@ -215,3 +234,5 @@ export class ScrollableLogTextStreamView extends React.PureComponent<
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const ScrollableLogTextStreamView = injectI18n(ScrollableLogTextStreamViewClass);
|
||||
|
|
|
@ -11,6 +11,7 @@ import React from 'react';
|
|||
import { InfraMetricData } from '../../graphql/types';
|
||||
import { InfraMetricLayout, InfraMetricLayoutSection } from '../../pages/metrics/layouts/types';
|
||||
import { metricTimeActions } from '../../store';
|
||||
import { NoData } from '../empty_states';
|
||||
import { InfraLoadingPanel } from '../loading';
|
||||
import { Section } from './section';
|
||||
|
||||
|
@ -18,6 +19,7 @@ interface Props {
|
|||
metrics: InfraMetricData[];
|
||||
layouts: InfraMetricLayout[];
|
||||
loading: boolean;
|
||||
refetch: () => void;
|
||||
nodeId: string;
|
||||
label: string;
|
||||
onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void;
|
||||
|
@ -37,6 +39,7 @@ export const Metrics = injectI18n(
|
|||
|
||||
public render() {
|
||||
const { intl } = this.props;
|
||||
|
||||
if (this.props.loading) {
|
||||
return (
|
||||
<InfraLoadingPanel
|
||||
|
@ -48,10 +51,34 @@ export const Metrics = injectI18n(
|
|||
})}
|
||||
/>
|
||||
);
|
||||
} else if (!this.props.loading && this.props.metrics && this.props.metrics.length === 0) {
|
||||
return (
|
||||
<NoData
|
||||
titleText={intl.formatMessage({
|
||||
id: 'xpack.infra.metrics.emptyViewTitle',
|
||||
defaultMessage: 'There is no data to display.',
|
||||
})}
|
||||
bodyText={intl.formatMessage({
|
||||
id: 'xpack.infra.metrics.emptyViewDescription',
|
||||
defaultMessage: 'Try adjusting your time or filter.',
|
||||
})}
|
||||
refetchText={intl.formatMessage({
|
||||
id: 'xpack.infra.metrics.refetchButtonLabel',
|
||||
defaultMessage: 'Check for new data',
|
||||
})}
|
||||
onRefetch={this.handleRefetch}
|
||||
testString="metricsEmptyViewState"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <React.Fragment>{this.props.layouts.map(this.renderLayout)}</React.Fragment>;
|
||||
}
|
||||
|
||||
private handleRefetch = () => {
|
||||
this.props.refetch();
|
||||
};
|
||||
|
||||
private renderLayout = (layout: InfraMetricLayout) => {
|
||||
return (
|
||||
<React.Fragment key={layout.id}>
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { get, max, min } from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
@ -18,6 +17,7 @@ import {
|
|||
import { InfraFormatterType, InfraWaffleMapBounds, InfraWaffleMapOptions } from '../../lib/lib';
|
||||
import { KueryFilterQuery } from '../../store/local/waffle_filter';
|
||||
import { createFormatter } from '../../utils/formatters';
|
||||
import { NoData } from '../empty_states';
|
||||
import { InfraLoadingPanel } from '../loading';
|
||||
import { Map } from '../waffle/map';
|
||||
import { ViewSwitcher } from '../waffle/view_switcher';
|
||||
|
@ -93,40 +93,23 @@ export const NodesOverview = injectI18n(
|
|||
);
|
||||
} else if (!loading && nodes && nodes.length === 0) {
|
||||
return (
|
||||
<CenteredEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.waffle.noDataTitle"
|
||||
defaultMessage="There is no data to display."
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
titleSize="m"
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.waffle.noDataDescription"
|
||||
defaultMessage="Try adjusting your time or filter."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
iconType="refresh"
|
||||
color="primary"
|
||||
fill
|
||||
onClick={() => {
|
||||
reload();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.waffle.checkNewDataButtonLabel"
|
||||
defaultMessage="Check for new data"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
data-test-subj="noMetricsDataPrompt"
|
||||
<NoData
|
||||
titleText={intl.formatMessage({
|
||||
id: 'xpack.infra.waffle.noDataTitle',
|
||||
defaultMessage: 'There is no data to display.',
|
||||
})}
|
||||
bodyText={intl.formatMessage({
|
||||
id: 'xpack.infra.waffle.noDataDescription',
|
||||
defaultMessage: 'Try adjusting your time or filter.',
|
||||
})}
|
||||
refetchText={intl.formatMessage({
|
||||
id: 'xpack.infra.waffle.checkNewDataButtonLabel',
|
||||
defaultMessage: 'Check for new data',
|
||||
})}
|
||||
onRefetch={() => {
|
||||
reload();
|
||||
}}
|
||||
testString="noMetricsDataPrompt"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -197,10 +180,6 @@ export const NodesOverview = injectI18n(
|
|||
}
|
||||
);
|
||||
|
||||
const CenteredEmptyPrompt = styled(EuiEmptyPrompt)`
|
||||
align-self: center;
|
||||
`;
|
||||
|
||||
const MainContainer = styled.div`
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
|
|
|
@ -20,6 +20,7 @@ interface WithMetricsArgs {
|
|||
metrics: InfraMetricData[];
|
||||
error?: string | undefined;
|
||||
loading: boolean;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
interface WithMetricsProps {
|
||||
|
@ -50,6 +51,7 @@ export const WithMetrics = ({
|
|||
<Query<MetricsQuery.Query, MetricsQuery.Variables>
|
||||
query={metricsQuery}
|
||||
fetchPolicy="no-cache"
|
||||
notifyOnNetworkStatusChange
|
||||
variables={{
|
||||
sourceId,
|
||||
metrics,
|
||||
|
@ -58,11 +60,12 @@ export const WithMetrics = ({
|
|||
timerange,
|
||||
}}
|
||||
>
|
||||
{({ data, error, loading }) => {
|
||||
{({ data, error, loading, refetch }) => {
|
||||
return children({
|
||||
metrics: filterOnlyInfraMetricData(data && data.source && data.source.metrics),
|
||||
error: error && error.message,
|
||||
loading,
|
||||
refetch,
|
||||
});
|
||||
}}
|
||||
</Query>
|
||||
|
|
|
@ -11,7 +11,7 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
|||
import { HomePageContent } from './page_content';
|
||||
import { HomeToolbar } from './toolbar';
|
||||
|
||||
import { EmptyPage } from '../../components/empty_page';
|
||||
import { NoIndices } from '../../components/empty_states/no_indices';
|
||||
import { Header } from '../../components/header';
|
||||
import { ColumnarPage } from '../../components/page';
|
||||
|
||||
|
@ -64,7 +64,7 @@ export const HomePage = injectI18n(
|
|||
) : (
|
||||
<WithKibanaChrome>
|
||||
{({ basePath }) => (
|
||||
<EmptyPage
|
||||
<NoIndices
|
||||
title={intl.formatMessage({
|
||||
id: 'xpack.infra.homePage.noMetricsIndicesTitle',
|
||||
defaultMessage: "Looks like you don't have any metrics indices.",
|
||||
|
|
|
@ -10,7 +10,7 @@ import React from 'react';
|
|||
import { LogsPageContent } from './page_content';
|
||||
import { LogsToolbar } from './toolbar';
|
||||
|
||||
import { EmptyPage } from '../../components/empty_page';
|
||||
import { NoIndices } from '../../components/empty_states/no_indices';
|
||||
import { Header } from '../../components/header';
|
||||
import { LogFlyout } from '../../components/logging/log_flyout';
|
||||
import { ColumnarPage } from '../../components/page';
|
||||
|
@ -99,7 +99,7 @@ export const LogsPage = injectI18n(
|
|||
) : (
|
||||
<WithKibanaChrome>
|
||||
{({ basePath }) => (
|
||||
<EmptyPage
|
||||
<NoIndices
|
||||
title={intl.formatMessage({
|
||||
id: 'xpack.infra.logsPage.noLoggingIndicesTitle',
|
||||
defaultMessage: "Looks like you don't have any logging indices.",
|
||||
|
|
|
@ -117,7 +117,7 @@ export const MetricDetail = withTheme(
|
|||
nodeType={nodeType}
|
||||
nodeId={nodeId}
|
||||
>
|
||||
{({ metrics, error, loading }) => {
|
||||
{({ metrics, error, loading, refetch }) => {
|
||||
if (error) {
|
||||
return <ErrorPageBody message={error} />;
|
||||
}
|
||||
|
@ -164,6 +164,7 @@ export const MetricDetail = withTheme(
|
|||
? false
|
||||
: loading
|
||||
}
|
||||
refetch={refetch}
|
||||
onChangeRangeTime={setRangeTime}
|
||||
/>
|
||||
</EuiPageContentWithRelative>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import actionCreatorFactory from 'typescript-fsa';
|
||||
|
||||
const actionCreator = actionCreatorFactory('x-pack/infra/local/waffle_time');
|
||||
const actionCreator = actionCreatorFactory('x-pack/infra/local/metric_time');
|
||||
|
||||
export interface MetricRangeTimeState {
|
||||
to: number;
|
||||
|
|
Loading…
Reference in a new issue