[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:
Kerry Gallagher 2019-02-04 10:27:26 +00:00 committed by GitHub
parent 93fef68647
commit 732915ee2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 133 additions and 102 deletions

View file

@ -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';

View file

@ -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;
`;

View file

@ -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,

View file

@ -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>
}
/>
);
}
}

View file

@ -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);

View file

@ -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}>

View file

@ -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;

View file

@ -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>

View file

@ -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.",

View file

@ -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.",

View file

@ -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>

View file

@ -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;