[Uptime] Update no data available state (#112403)

* wip

* update component

* update paths

* fix i18n

* fix tests

* revert uneeded
This commit is contained in:
Shahzad 2021-09-21 13:15:05 +02:00 committed by GitHub
parent b2bc5a592d
commit 322c5e26f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 151 additions and 380 deletions

View file

@ -26137,13 +26137,7 @@
"xpack.uptime.createPackagePolicy.stepConfigure.tlsSettings.label": "TLS設定",
"xpack.uptime.durationChart.emptyPrompt.description": "このモニターは選択された時間範囲で一度も{emphasizedText}していません。",
"xpack.uptime.durationChart.emptyPrompt.title": "利用可能な期間データがありません",
"xpack.uptime.emptyState.configureHeartbeatIndexSettings": "Heartbeatがすでに設定されている場合は、データがElasticsearchに送信されていることを確認してから、Heartbeat構成に合わせてインデックスパターン設定を更新します。",
"xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage": "サービスの監視を開始するには、Heartbeatを設定します。",
"xpack.uptime.emptyState.loadingMessage": "読み込み中…",
"xpack.uptime.emptyState.noDataMessage": "インデックス{indexName}にはアップタイムデータが見つかりません",
"xpack.uptime.emptyState.noIndexTitle": "パターン{indexName}のインデックスが見つかりません",
"xpack.uptime.emptyState.updateIndexPattern": "インデックスパターン設定を更新",
"xpack.uptime.emptyState.viewSetupInstructions": "セットアップの手順を表示",
"xpack.uptime.emptyStateError.notAuthorized": "アップタイムデータの表示が承認されていません。システム管理者にお問い合わせください。",
"xpack.uptime.emptyStateError.notFoundPage": "ページが見つかりません",
"xpack.uptime.emptyStateError.title": "エラー",

View file

@ -26572,13 +26572,7 @@
"xpack.uptime.createPackagePolicy.stepConfigure.tlsSettings.label": "TLS 设置",
"xpack.uptime.durationChart.emptyPrompt.description": "在选定时间范围内此监测从未{emphasizedText}。",
"xpack.uptime.durationChart.emptyPrompt.title": "没有持续时间数据",
"xpack.uptime.emptyState.configureHeartbeatIndexSettings": "如果已设置 Heartbeat请确认其正向 Elasticsearch 发送数据,然后更新索引模式设置以匹配 Heartbeat 配置。",
"xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage": "设置 Heartbeat 以开始监测您的服务。",
"xpack.uptime.emptyState.loadingMessage": "正在加载……",
"xpack.uptime.emptyState.noDataMessage": "在索引 {indexName} 中找不到运行时间数据",
"xpack.uptime.emptyState.noIndexTitle": "找不到模式 {indexName} 的索引",
"xpack.uptime.emptyState.updateIndexPattern": "更新索引模式设置",
"xpack.uptime.emptyState.viewSetupInstructions": "查看设置说明",
"xpack.uptime.emptyStateError.notAuthorized": "您无权查看 Uptime 数据,请联系系统管理员。",
"xpack.uptime.emptyStateError.notFoundPage": "未找到页面",
"xpack.uptime.emptyStateError.title": "错误",

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { EuiPageHeaderProps } from '@elastic/eui';
import { OVERVIEW_ROUTE } from '../../common/constants';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import { ClientPluginsStart } from './plugin';
import { useNoDataConfig } from './use_no_data_config';
import { EmptyStateLoading } from '../components/overview/empty_state/empty_state_loading';
import { EmptyStateError } from '../components/overview/empty_state/empty_state_error';
import { useHasData } from '../components/overview/empty_state/use_has_data';
interface Props {
path: string;
pageHeader?: EuiPageHeaderProps;
}
export const UptimePageTemplateComponent: React.FC<Props> = ({ path, pageHeader, children }) => {
const {
services: { observability },
} = useKibana<ClientPluginsStart>();
const PageTemplateComponent = observability.navigation.PageTemplate;
const StyledPageTemplateComponent = useMemo(() => {
return styled(PageTemplateComponent)`
.euiPageHeaderContent > .euiFlexGroup {
flex-wrap: wrap;
}
`;
}, [PageTemplateComponent]);
const noDataConfig = useNoDataConfig();
const { loading, error } = useHasData();
if (error) {
return <EmptyStateError errors={[error]} />;
}
return (
<>
<div data-test-subj={noDataConfig ? 'data-missing' : undefined} />
<StyledPageTemplateComponent
pageHeader={pageHeader}
noDataConfig={path === OVERVIEW_ROUTE && !loading ? noDataConfig : undefined}
>
{loading && path === OVERVIEW_ROUTE && <EmptyStateLoading />}
<div
style={{ visibility: loading && path === OVERVIEW_ROUTE ? 'hidden' : 'initial' }}
data-test-subj={noDataConfig ? 'data-missing' : undefined}
>
{children}
</div>
</StyledPageTemplateComponent>
</>
);
};

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { useContext } from 'react';
import { useSelector } from 'react-redux';
import { KibanaPageTemplateProps, useKibana } from '../../../../../src/plugins/kibana_react/public';
import { UptimeSettingsContext } from '../contexts';
import { ClientPluginsStart } from './plugin';
import { indexStatusSelector } from '../state/selectors';
export function useNoDataConfig(): KibanaPageTemplateProps['noDataConfig'] {
const { basePath } = useContext(UptimeSettingsContext);
const {
services: { docLinks },
} = useKibana<ClientPluginsStart>();
const { data } = useSelector(indexStatusSelector);
// Returns no data config when there is no historical data
if (data && !data.indexExists) {
return {
solution: i18n.translate('xpack.uptime.noDataConfig.solutionName', {
defaultMessage: 'Observability',
}),
actions: {
beats: {
title: i18n.translate('xpack.uptime.noDataConfig.beatsCard.title', {
defaultMessage: 'Add monitors with Heartbeat',
}),
description: i18n.translate('xpack.uptime.noDataConfig.beatsCard.description', {
defaultMessage:
'Proactively monitor the availability of your sites and services. Receive alerts and resolve issues faster to optimize your users experience.',
}),
href: basePath + `/app/home#/tutorial/uptimeMonitors`,
},
},
docsLink: docLinks!.links.observability.guide,
};
}
}

View file

@ -1,26 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { screen } from '@testing-library/react';
import { FormattedMessage } from '@kbn/i18n/react';
import { render } from '../../../lib/helper/rtl_helpers';
import { DataOrIndexMissing } from './data_or_index_missing';
describe('DataOrIndexMissing component', () => {
it('renders headingMessage', () => {
const headingMessage = (
<FormattedMessage
id="xpack.uptime.emptyState.noIndexTitle"
defaultMessage="Uptime index {indexName} not found"
values={{ indexName: <em>heartbeat-*</em> }}
/>
);
render(<DataOrIndexMissing headingMessage={headingMessage} />);
expect(screen.getByText(/heartbeat-*/)).toBeInTheDocument();
});
});

View file

@ -1,87 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiFlexGroup,
EuiEmptyPrompt,
EuiFlexItem,
EuiSpacer,
EuiPanel,
EuiTitle,
EuiButton,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useContext } from 'react';
import { UptimeSettingsContext } from '../../../contexts';
import { DynamicSettings } from '../../../../common/runtime_types';
interface DataMissingProps {
headingMessage: JSX.Element;
settings?: DynamicSettings;
}
export const DataOrIndexMissing = ({ headingMessage, settings }: DataMissingProps) => {
const { basePath } = useContext(UptimeSettingsContext);
return (
<EuiFlexGroup justifyContent="center" data-test-subj="data-missing">
<EuiFlexItem grow={false} style={{ flexBasis: 700 }}>
<EuiSpacer size="m" />
<EuiPanel hasBorder>
<EuiEmptyPrompt
iconType="logoUptime"
title={
<EuiTitle size="l">
<h3>{headingMessage}</h3>
</EuiTitle>
}
body={
<>
<p>
<FormattedMessage
id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage"
defaultMessage="Set up Heartbeat to start monitoring your services."
/>
</p>
<p>
<FormattedMessage
id="xpack.uptime.emptyState.configureHeartbeatIndexSettings"
defaultMessage="If Heartbeat is already set up, confirm it's sending data to Elasticsearch,
then update the index pattern settings to match the Heartbeat config."
/>
</p>
</>
}
actions={
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton
fill
color="primary"
href={`${basePath}/app/home#/tutorial/uptimeMonitors`}
>
<FormattedMessage
id="xpack.uptime.emptyState.viewSetupInstructions"
defaultMessage="View setup instructions"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton color="primary" href={`${basePath}/app/uptime/settings`}>
<FormattedMessage
id="xpack.uptime.emptyState.updateIndexPattern"
defaultMessage="Update index pattern settings"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -1,99 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { screen } from '@testing-library/react';
import { EmptyStateComponent } from './empty_state';
import { StatesIndexStatus } from '../../../../common/runtime_types';
import { HttpFetchError, IHttpFetchError } from 'src/core/public';
import { render } from '../../../lib/helper/rtl_helpers';
describe('EmptyState component', () => {
let statesIndexStatus: StatesIndexStatus;
beforeEach(() => {
statesIndexStatus = {
indexExists: true,
docCount: 1,
indices: 'heartbeat-*,synthetics-*',
};
});
it('renders child components when count is truthy', () => {
render(
<EmptyStateComponent statesIndexStatus={statesIndexStatus} loading={false}>
<div>Foo</div>
<div>Bar</div>
<div>Baz</div>
</EmptyStateComponent>
);
expect(screen.getByText('Foo')).toBeInTheDocument();
expect(screen.getByText('Bar')).toBeInTheDocument();
expect(screen.getByText('Baz')).toBeInTheDocument();
});
it(`doesn't render child components when count is falsy`, () => {
render(
<EmptyStateComponent statesIndexStatus={null} loading={false}>
<div>Should not be rendered</div>
</EmptyStateComponent>
);
expect(screen.queryByText('Should not be rendered')).toBeNull();
});
it(`renders error message when an error occurs`, () => {
const errors: IHttpFetchError[] = [
new HttpFetchError('There was an error fetching your data.', 'error', {} as any, {} as any, {
body: { message: 'There was an error fetching your data.' },
}),
];
render(
<EmptyStateComponent statesIndexStatus={null} errors={errors} loading={false}>
<div>Should not appear...</div>
</EmptyStateComponent>
);
expect(screen.queryByText('Should not appear...')).toBeNull();
});
it('renders loading state if no errors or doc count', () => {
render(
<EmptyStateComponent loading={true} statesIndexStatus={null}>
<div>Should appear even while loading...</div>
</EmptyStateComponent>
);
expect(screen.queryByText('Should appear even while loading...')).toBeInTheDocument();
});
it('does not render empty state with appropriate base path and no docs', () => {
statesIndexStatus = {
docCount: 0,
indexExists: true,
indices: 'heartbeat-*,synthetics-*',
};
const text = 'If this is in the snapshot the test should fail';
render(
<EmptyStateComponent statesIndexStatus={statesIndexStatus} loading={false}>
<div>{text}</div>
</EmptyStateComponent>
);
expect(screen.queryByText(text)).toBeNull();
});
it('notifies when index does not exist', () => {
statesIndexStatus.indexExists = false;
const text = 'This text should not render';
render(
<EmptyStateComponent statesIndexStatus={statesIndexStatus} loading={false}>
<div>{text}</div>
</EmptyStateComponent>
);
expect(screen.queryByText(text)).toBeNull();
});
});

View file

@ -1,73 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { IHttpFetchError } from 'src/core/public';
import { EmptyStateError } from './empty_state_error';
import { EmptyStateLoading } from './empty_state_loading';
import { DataOrIndexMissing } from './data_or_index_missing';
import { DynamicSettings, StatesIndexStatus } from '../../../../common/runtime_types';
interface EmptyStateProps {
children: JSX.Element[] | JSX.Element;
statesIndexStatus: StatesIndexStatus | null;
loading: boolean;
errors?: IHttpFetchError[];
settings?: DynamicSettings;
}
export const EmptyStateComponent = ({
children,
statesIndexStatus,
loading,
errors,
settings,
}: EmptyStateProps) => {
if (errors?.length) {
return <EmptyStateError errors={errors} />;
}
const { indexExists, docCount } = statesIndexStatus ?? {};
const isLoading = loading && (!indexExists || docCount === 0 || !statesIndexStatus);
const noIndicesMessage = (
<FormattedMessage
id="xpack.uptime.emptyState.noIndexTitle"
defaultMessage="No indices found for the pattern {indexName}"
values={{ indexName: <em>{settings?.heartbeatIndices}</em> }}
/>
);
const noUptimeDataMessage = (
<FormattedMessage
id="xpack.uptime.emptyState.noDataMessage"
defaultMessage="No uptime data found in index {indexName}"
values={{ indexName: <em>{settings?.heartbeatIndices}</em> }}
/>
);
if (!indexExists && !isLoading) {
return <DataOrIndexMissing settings={settings} headingMessage={noIndicesMessage} />;
} else if (indexExists && docCount === 0 && !isLoading) {
return <DataOrIndexMissing settings={settings} headingMessage={noUptimeDataMessage} />;
}
/**
* We choose to render the children any time the count > 0, even if
* the component is loading. If we render the loading state for this component,
* it will blow away the state of child components and trigger an ugly
* jittery UX any time the components refresh. This way we'll keep the stale
* state displayed during the fetching process.
*/
return (
<Fragment>
{isLoading && <EmptyStateLoading />}
<div style={{ visibility: isLoading ? 'hidden' : 'initial' }}>{children}</div>
</Fragment>
);
// }
};

View file

@ -1,54 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useContext, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { indexStatusAction } from '../../../state/actions';
import { indexStatusSelector, selectDynamicSettings } from '../../../state/selectors';
import { EmptyStateComponent } from './index';
import { UptimeRefreshContext } from '../../../contexts';
import { getDynamicSettings } from '../../../state/actions/dynamic_settings';
export const EmptyState: React.FC = ({ children }) => {
const { data, loading, error } = useSelector(indexStatusSelector);
const { lastRefresh } = useContext(UptimeRefreshContext);
const { settings } = useSelector(selectDynamicSettings);
const heartbeatIndices = settings?.heartbeatIndices || '';
const dispatch = useDispatch();
const noDataInfo = !data || data?.docCount === 0 || data?.indexExists === false;
useEffect(() => {
if (noDataInfo) {
// only call when we haven't fetched it already
dispatch(indexStatusAction.get());
}
}, [dispatch, lastRefresh, noDataInfo]);
useEffect(() => {
// using separate side effect, we want to call index status,
// every statue indices setting changes
dispatch(indexStatusAction.get());
}, [dispatch, heartbeatIndices]);
useEffect(() => {
dispatch(getDynamicSettings());
}, [dispatch]);
return (
<EmptyStateComponent
statesIndexStatus={data}
loading={loading}
errors={error ? [error] : undefined}
children={children as React.ReactElement}
settings={settings}
/>
);
};

View file

@ -1,9 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { EmptyStateComponent } from './empty_state';
export { EmptyState } from './empty_state_container';

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useContext, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { indexStatusAction } from '../../../state/actions';
import { indexStatusSelector, selectDynamicSettings } from '../../../state/selectors';
import { UptimeRefreshContext } from '../../../contexts';
import { getDynamicSettings } from '../../../state/actions/dynamic_settings';
export const useHasData = () => {
const { loading, error } = useSelector(indexStatusSelector);
const { lastRefresh } = useContext(UptimeRefreshContext);
const { settings } = useSelector(selectDynamicSettings);
const dispatch = useDispatch();
useEffect(() => {
dispatch(indexStatusAction.get());
}, [dispatch, lastRefresh]);
useEffect(() => {
dispatch(getDynamicSettings());
}, [dispatch]);
return {
error,
loading,
settings,
};
};

View file

@ -6,6 +6,5 @@
*/
export * from './monitor_list';
export * from './empty_state';
export * from './alerts';
export * from './snapshot';

View file

@ -12,7 +12,6 @@ import styled from 'styled-components';
import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
import { useTrackPageview } from '../../../observability/public';
import { MonitorList } from '../components/overview/monitor_list/monitor_list_container';
import { EmptyState } from '../components/overview';
import { StatusPanel } from '../components/overview/status_panel';
import { QueryBar } from '../components/overview/query_bar/query_bar';
import { MONITORING_OVERVIEW_LABEL } from '../routes';
@ -37,7 +36,7 @@ export const OverviewPageComponent = () => {
useBreadcrumbs([{ text: MONITORING_OVERVIEW_LABEL }]); // No extra breadcrumbs on overview
return (
<EmptyState>
<>
<EuiFlexGroup gutterSize="xs" wrap responsive={false}>
<QueryBar />
<EuiFlexItemStyled grow={true}>
@ -48,6 +47,6 @@ export const OverviewPageComponent = () => {
<StatusPanel />
<EuiSpacer size="s" />
<MonitorList />
</EmptyState>
</>
);
};

View file

@ -6,7 +6,6 @@
*/
import React, { FC, useEffect } from 'react';
import styled from 'styled-components';
import { Route, Switch } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@ -27,10 +26,8 @@ import {
SyntheticsCheckStepsPageHeader,
SyntheticsCheckStepsPageRightSideItem,
} from './pages/synthetics/synthetics_checks';
import { ClientPluginsStart } from './apps/plugin';
import { MonitorPageTitle, MonitorPageTitleContent } from './components/monitor/monitor_title';
import { UptimeDatePicker } from './components/common/uptime_date_picker';
import { useKibana } from '../../../../src/plugins/kibana_react/public';
import { CertRefreshBtn } from './components/certificates/cert_refresh_btn';
import { CertificateTitle } from './components/certificates/certificate_title';
import { SyntheticsCallout } from './components/overview/synthetics_callout';
@ -40,6 +37,7 @@ import {
StepDetailPageHeader,
StepDetailPageRightSideItem,
} from './pages/synthetics/step_detail_page';
import { UptimePageTemplateComponent } from './apps/uptime_page_template';
interface RouteProps {
path: string;
@ -159,17 +157,6 @@ const RouteInit: React.FC<Pick<RouteProps, 'path' | 'title' | 'telemetryId'>> =
};
export const PageRouter: FC = () => {
const {
services: { observability },
} = useKibana<ClientPluginsStart>();
const PageTemplateComponent = observability.navigation.PageTemplate;
const StyledPageTemplateComponent = styled(PageTemplateComponent)`
.euiPageHeaderContent > .euiFlexGroup {
flex-wrap: wrap;
}
`;
return (
<Switch>
{Routes.map(
@ -178,9 +165,9 @@ export const PageRouter: FC = () => {
<div className={APP_WRAPPER_CLASS} data-test-subj={dataTestSubj}>
<SyntheticsCallout />
<RouteInit title={title} path={path} telemetryId={telemetryId} />
<StyledPageTemplateComponent pageHeader={pageHeader}>
<UptimePageTemplateComponent path={path} pageHeader={pageHeader}>
<RouteComponent />
</StyledPageTemplateComponent>
</UptimePageTemplateComponent>
</div>
</Route>
)