[APM] Consistent "no data" screen with other Observability solutions (#111630)

* logic to show no data screen

* fixing i18n

* fixing ts

* addressing pr comments
This commit is contained in:
Cauê Marcondes 2021-09-09 12:55:28 -04:00 committed by GitHub
parent b9e6f935c4
commit 6e9b1b57b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 194 additions and 124 deletions

View file

@ -9,7 +9,7 @@ import React from 'react';
import { act } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { Observable } from 'rxjs';
import { CoreStart } from 'src/core/public';
import { CoreStart, DocLinksStart, HttpStart } from 'src/core/public';
import { mockApmPluginContextValue } from '../context/apm_plugin/mock_apm_plugin_context';
import { createCallApmApi } from '../services/rest/createCallApmApi';
import { renderApp } from './';
@ -85,6 +85,20 @@ describe('renderApp', () => {
getEditAlertFlyout: jest.fn(),
},
usageCollection: { reportUiCounter: () => {} },
http: {
basePath: {
prepend: (path: string) => `/basepath${path}`,
get: () => `/basepath`,
},
} as HttpStart,
docLinks: ({
DOC_LINK_VERSION: '0',
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
links: {
apm: {},
observability: { guide: '' },
},
} as unknown) as DocLinksStart,
} as unknown) as ApmPluginStartDeps;
jest.spyOn(window, 'scrollTo').mockReturnValueOnce(undefined);

View file

@ -179,8 +179,6 @@ export function ServiceInventory() {
canCreateJob &&
!userHasDismissedCallout;
const isLoading = mainStatisticsStatus === FETCH_STATUS.LOADING;
return (
<>
<SearchBar showTimeComparison />
@ -192,17 +190,10 @@ export function ServiceInventory() {
)}
<EuiFlexItem>
<ServiceList
isLoading={isLoading}
isLoading={mainStatisticsStatus === FETCH_STATUS.LOADING}
items={mainStatisticsData.items}
comparisonData={comparisonData}
noItemsMessage={
!isLoading && (
<NoServicesMessage
historicalDataFound={mainStatisticsData.hasHistoricalData}
status={mainStatisticsStatus}
/>
)
}
noItemsMessage={<NoServicesMessage status={mainStatisticsStatus} />}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -22,13 +22,9 @@ describe('NoServicesMessage', () => {
describe(`when historicalDataFound is ${historicalDataFound}`, () => {
it('renders', () => {
expect(() =>
render(
<NoServicesMessage
status={status}
historicalDataFound={historicalDataFound}
/>,
{ wrapper: Wrapper }
)
render(<NoServicesMessage status={status} />, {
wrapper: Wrapper,
})
).not.toThrowError();
});
});

View file

@ -5,76 +5,35 @@
* 2.0.
*/
import { EuiEmptyPrompt, EuiLink } from '@elastic/eui';
import { EuiEmptyPrompt } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { ErrorStatePrompt } from '../../shared/ErrorStatePrompt';
import { useUpgradeAssistantHref } from '../../shared/Links/kibana';
import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink';
interface Props {
// any data submitted from APM agents found (not just in the given time range)
historicalDataFound: boolean;
status: FETCH_STATUS | undefined;
}
export function NoServicesMessage({ historicalDataFound, status }: Props) {
const upgradeAssistantHref = useUpgradeAssistantHref();
if (status === 'failure') {
return <ErrorStatePrompt />;
export function NoServicesMessage({ status }: Props) {
if (status === FETCH_STATUS.LOADING) {
return null;
}
if (historicalDataFound) {
return (
<EuiEmptyPrompt
title={
<div>
{i18n.translate('xpack.apm.servicesTable.notFoundLabel', {
defaultMessage: 'No services found',
})}
</div>
}
titleSize="s"
/>
);
if (status === FETCH_STATUS.FAILURE) {
return <ErrorStatePrompt />;
}
return (
<EuiEmptyPrompt
title={
<div>
{i18n.translate('xpack.apm.servicesTable.noServicesLabel', {
defaultMessage: `Looks like you don't have any APM services installed. Let's add some!`,
{i18n.translate('xpack.apm.servicesTable.notFoundLabel', {
defaultMessage: 'No services found',
})}
</div>
}
titleSize="s"
body={
<React.Fragment>
<p>
{i18n.translate('xpack.apm.servicesTable.7xUpgradeServerMessage', {
defaultMessage: `Upgrading from a pre-7.x version? Make sure you've also upgraded
your APM Server instance(s) to at least 7.0.`,
})}
</p>
<p>
{i18n.translate('xpack.apm.servicesTable.7xOldDataMessage', {
defaultMessage:
'You may also have old data that needs to be migrated.',
})}{' '}
<EuiLink href={upgradeAssistantHref}>
{i18n.translate('xpack.apm.servicesTable.UpgradeAssistantLink', {
defaultMessage:
'Learn more by visiting the Kibana Upgrade Assistant',
})}
</EuiLink>
.
</p>
</React.Fragment>
}
actions={<SetupInstructionsLink buttonFill={true} />}
/>
);
}

View file

@ -134,28 +134,6 @@ describe('ServiceInventory', () => {
expect(container.querySelectorAll('.euiTableRow')).toHaveLength(2);
});
it('should render getting started message, when list is empty and no historical data is found', async () => {
httpGet
.mockResolvedValueOnce({ fallbackToTransactions: false })
.mockResolvedValueOnce({
hasLegacyData: false,
hasHistoricalData: false,
items: [],
});
const { findByText } = render(<ServiceInventory />, { wrapper });
// wait for requests to be made
await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2));
// wait for elements to be rendered
const gettingStartedMessage = await findByText(
"Looks like you don't have any APM services installed. Let's add some!"
);
expect(gettingStartedMessage).not.toBeEmptyDOMElement();
});
it('should render empty message, when list is empty and historical data is found', async () => {
httpGet
.mockResolvedValueOnce({ fallbackToTransactions: false })

View file

@ -8,8 +8,10 @@
import { EuiPageHeaderProps, EuiPageTemplateProps } from '@elastic/eui';
import React from 'react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { useFetcher } from '../../../hooks/use_fetcher';
import { ApmPluginStartDeps } from '../../../plugin';
import { ApmEnvironmentFilter } from '../../shared/EnvironmentFilter';
import { getNoDataConfig } from './no_data_config';
/*
* This template contains:
@ -31,12 +33,25 @@ export function ApmMainTemplate({
children: React.ReactNode;
} & EuiPageTemplateProps) {
const { services } = useKibana<ApmPluginStartDeps>();
const { http, docLinks } = services;
const basePath = http?.basePath.get();
const ObservabilityPageTemplate =
services.observability.navigation.PageTemplate;
const { data } = useFetcher((callApmApi) => {
return callApmApi({ endpoint: 'GET /api/apm/has_data' });
}, []);
const noDataConfig = getNoDataConfig({
basePath,
docsLink: docLinks!.links.observability.guide,
hasData: data?.hasData,
});
return (
<ObservabilityPageTemplate
noDataConfig={noDataConfig}
pageHeader={{
pageTitle,
rightSideItems: [<ApmEnvironmentFilter />],

View file

@ -0,0 +1,44 @@
/*
* 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 { KibanaPageTemplateProps } from '../../../../../../../src/plugins/kibana_react/public';
export function getNoDataConfig({
docsLink,
basePath,
hasData,
}: {
docsLink: string;
basePath?: string;
hasData?: boolean;
}): KibanaPageTemplateProps['noDataConfig'] {
// Returns no data config when there is no historical data
if (hasData === false) {
return {
solution: i18n.translate('xpack.apm.noDataConfig.solutionName', {
defaultMessage: 'Observability',
}),
actions: {
beats: {
title: i18n.translate('xpack.apm.noDataConfig.beatsCard.title', {
defaultMessage: 'Add data with APM agents',
}),
description: i18n.translate(
'xpack.apm.noDataConfig.beatsCard.description',
{
defaultMessage:
'Use APM agents to collect APM data. We make it easy with agents for many popular languages.',
}
),
href: basePath + `/app/home#/tutorial/apm`,
},
},
docsLink,
};
}
}

View file

@ -11,7 +11,7 @@ import React, { ReactNode } from 'react';
import { SettingsTemplate } from './settings_template';
import { createMemoryHistory } from 'history';
import { MemoryRouter, RouteComponentProps } from 'react-router-dom';
import { CoreStart } from 'kibana/public';
import { CoreStart, DocLinksStart, HttpStart } from 'kibana/public';
import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
const { location } = createMemoryHistory();
@ -25,6 +25,20 @@ const KibanaReactContext = createKibanaReactContext({
},
},
},
http: {
basePath: {
prepend: (path: string) => `/basepath${path}`,
get: () => `/basepath`,
},
} as HttpStart,
docLinks: ({
DOC_LINK_VERSION: '0',
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
links: {
apm: {},
observability: { guide: '' },
},
} as unknown) as DocLinksStart,
} as Partial<CoreStart>);
function Wrapper({ children }: { children?: ReactNode }) {

View file

@ -8,7 +8,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { createStaticIndexPattern } from './create_static_index_pattern';
import { Setup } from '../helpers/setup_request';
import * as HistoricalAgentData from '../services/get_services/has_historical_agent_data';
import * as HistoricalAgentData from '../../routes/historical_data/has_historical_agent_data';
import { InternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client';
import { APMConfig } from '../..';

View file

@ -8,7 +8,7 @@
import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server';
import { APM_STATIC_INDEX_PATTERN_ID } from '../../../common/index_pattern_constants';
import apmIndexPattern from '../../tutorial/index_pattern.json';
import { hasHistoricalAgentData } from '../services/get_services/has_historical_agent_data';
import { hasHistoricalAgentData } from '../../routes/historical_data/has_historical_agent_data';
import { Setup } from '../helpers/setup_request';
import { APMRouteHandlerResources } from '../../routes/typings';
import { InternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client.js';

View file

@ -6,12 +6,10 @@
*/
import { Logger } from '@kbn/logging';
import { isEmpty } from 'lodash';
import { withApmSpan } from '../../../utils/with_apm_span';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
import { getLegacyDataStatus } from './get_legacy_data_status';
import { getServicesItems } from './get_services_items';
import { hasHistoricalAgentData } from './has_historical_agent_data';
export async function getServices({
environment,
@ -38,14 +36,8 @@ export async function getServices({
getLegacyDataStatus(setup),
]);
const noDataInCurrentTimeRange = isEmpty(items);
const hasHistoricalData = noDataInCurrentTimeRange
? await hasHistoricalAgentData(setup)
: true;
return {
items,
hasHistoricalData,
hasLegacyData,
};
});

View file

@ -9,7 +9,7 @@ import { getServiceAgent } from './get_service_agent';
import { getServiceTransactionTypes } from './get_service_transaction_types';
import { getServicesItems } from './get_services/get_services_items';
import { getLegacyDataStatus } from './get_services/get_legacy_data_status';
import { hasHistoricalAgentData } from './get_services/has_historical_agent_data';
import { hasHistoricalAgentData } from '../../routes/historical_data/has_historical_agent_data';
import {
SearchParamsMock,
inspectSearchParams,

View file

@ -33,6 +33,7 @@ import { sourceMapsRouteRepository } from './source_maps';
import { traceRouteRepository } from './traces';
import { transactionRouteRepository } from './transactions';
import { APMRouteHandlerResources } from './typings';
import { historicalDataRouteRepository } from './historical_data';
const getTypedGlobalApmServerRouteRepository = () => {
const repository = createApmServerRouteRepository()
@ -56,7 +57,8 @@ const getTypedGlobalApmServerRouteRepository = () => {
.merge(sourceMapsRouteRepository)
.merge(apmFleetRouteRepository)
.merge(backendsRouteRepository)
.merge(fallbackToTransactionsRouteRepository);
.merge(fallbackToTransactionsRouteRepository)
.merge(historicalDataRouteRepository);
return repository;
};

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { ProcessorEvent } from '../../../../common/processor_event';
import { Setup } from '../../helpers/setup_request';
import { ProcessorEvent } from '../../../common/processor_event';
import { Setup } from '../../lib/helpers/setup_request';
// Note: this logic is duplicated in tutorials/apm/envs/on_prem
export async function hasHistoricalAgentData(setup: Setup) {

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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { setupRequest } from '../../lib/helpers/setup_request';
import { createApmServerRoute } from '../create_apm_server_route';
import { createApmServerRouteRepository } from '../create_apm_server_route_repository';
import { hasHistoricalAgentData } from './has_historical_agent_data';
const hasDataRoute = createApmServerRoute({
endpoint: 'GET /api/apm/has_data',
options: { tags: ['access:apm'] },
handler: async (resources) => {
const setup = await setupRequest(resources);
const hasData = await hasHistoricalAgentData(setup);
return { hasData };
},
});
export const historicalDataRouteRepository = createApmServerRouteRepository().add(
hasDataRoute
);

View file

@ -31,7 +31,10 @@ export type WrappedPageTemplateProps = Pick<
| 'restrictWidth'
| 'template'
| 'isEmptyState'
>;
| 'noDataConfig'
> & {
showSolutionNav?: boolean;
};
export interface ObservabilityPageTemplateDependencies {
currentAppId$: Observable<string | undefined>;
@ -49,6 +52,7 @@ export function ObservabilityPageTemplate({
getUrlForApp,
navigateToApp,
navigationSections$,
showSolutionNav = true,
...pageTemplateProps
}: ObservabilityPageTemplateProps): React.ReactElement | null {
const sections = useObservable(navigationSections$, []);
@ -118,11 +122,15 @@ export function ObservabilityPageTemplate({
<KibanaPageTemplate
restrictWidth={false}
{...pageTemplateProps}
solutionNav={{
icon: 'logoObservability',
items: sideNavItems,
name: sideNavTitle,
}}
solutionNav={
showSolutionNav
? {
icon: 'logoObservability',
items: sideNavItems,
name: sideNavTitle,
}
: undefined
}
>
{children}
</KibanaPageTemplate>

View file

@ -6895,20 +6895,16 @@
"xpack.apm.serviceProfiling.valueTypeLabel.samples": "サンプル",
"xpack.apm.serviceProfiling.valueTypeLabel.unknown": "その他",
"xpack.apm.serviceProfiling.valueTypeLabel.wallTime": "Wall",
"xpack.apm.servicesTable.7xOldDataMessage": "また、移行が必要な古いデータがある可能性もあります。",
"xpack.apm.servicesTable.7xUpgradeServerMessage": "バージョン7.xより前からのアップグレードですかまた、\n APM Server インスタンスを7.0以降にアップグレードしていることも確認してください。",
"xpack.apm.servicesTable.environmentColumnLabel": "環境",
"xpack.apm.servicesTable.healthColumnLabel": "ヘルス",
"xpack.apm.servicesTable.latencyAvgColumnLabel": "レイテンシ(平均)",
"xpack.apm.servicesTable.metricsExplanationLabel": "これらのメトリックは何か。",
"xpack.apm.servicesTable.nameColumnLabel": "名前",
"xpack.apm.servicesTable.noServicesLabel": "APM サービスがインストールされていないようです。追加しましょう!",
"xpack.apm.servicesTable.notFoundLabel": "サービスが見つかりません",
"xpack.apm.servicesTable.throughputColumnLabel": "スループット",
"xpack.apm.servicesTable.tooltip.metricsExplanation": "サービスメトリックは、トランザクションタイプ「要求」、「ページ読み込み」、または上位の使用可能なトランザクションタイプのいずれかで集計されます。",
"xpack.apm.servicesTable.transactionColumnLabel": "トランザクションタイプ",
"xpack.apm.servicesTable.transactionErrorRate": "失敗したトランザクション率",
"xpack.apm.servicesTable.UpgradeAssistantLink": "Kibana アップグレードアシスタントで詳細をご覧ください",
"xpack.apm.settings.agentConfig": "エージェントの編集",
"xpack.apm.settings.agentConfig.createConfigButton.tooltip": "エージェント構成を作成する権限がありません",
"xpack.apm.settings.agentConfig.descriptionText": "APMアプリ内からエージェント構成を微調整してください。変更はAPMエージェントに自動的に伝達されるので、再デプロイする必要はありません。",

View file

@ -6949,21 +6949,17 @@
"xpack.apm.serviceProfiling.valueTypeLabel.samples": "样例",
"xpack.apm.serviceProfiling.valueTypeLabel.unknown": "其他",
"xpack.apm.serviceProfiling.valueTypeLabel.wallTime": "现实时间",
"xpack.apm.servicesTable.7xOldDataMessage": "可能还有需要迁移的旧数据。",
"xpack.apm.servicesTable.7xUpgradeServerMessage": "从 7.x 之前的版本升级?另外,确保您已将\n APM Server 实例升级到至少 7.0。",
"xpack.apm.servicesTable.environmentColumnLabel": "环境",
"xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 个环境} other {# 个环境}}",
"xpack.apm.servicesTable.healthColumnLabel": "运行状况",
"xpack.apm.servicesTable.latencyAvgColumnLabel": "延迟(平均值)",
"xpack.apm.servicesTable.metricsExplanationLabel": "这些指标是什么?",
"xpack.apm.servicesTable.nameColumnLabel": "名称",
"xpack.apm.servicesTable.noServicesLabel": "似乎您没有安装任何 APM 服务。让我们添加一些!",
"xpack.apm.servicesTable.notFoundLabel": "未找到任何服务",
"xpack.apm.servicesTable.throughputColumnLabel": "吞吐量",
"xpack.apm.servicesTable.tooltip.metricsExplanation": "服务指标将基于事务类型“request”、“page-load”或排名靠前的可用事务类型进行聚合。",
"xpack.apm.servicesTable.transactionColumnLabel": "事务类型",
"xpack.apm.servicesTable.transactionErrorRate": "失败事务率",
"xpack.apm.servicesTable.UpgradeAssistantLink": "通过访问 Kibana 升级助手来了解详情",
"xpack.apm.settings.agentConfig": "代理配置",
"xpack.apm.settings.agentConfig.createConfigButton.tooltip": "您无权创建代理配置",
"xpack.apm.settings.agentConfig.descriptionText": "从 APM 应用中微调您的代理配置。更改将自动传播到 APM 代理,因此无需重新部署。",

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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { registry } from '../../common/registry';
export default function ApiTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const archiveName = 'apm_8.0.0';
registry.when(
'Historical data when data is not loaded',
{ config: 'basic', archives: [] },
() => {
it('handles the empty state', async () => {
const response = await supertest.get(`/api/apm/has_data`);
expect(response.status).to.be(200);
expect(response.body.hasData).to.be(false);
});
}
);
registry.when(
'Historical data when data is loaded',
{ config: 'basic', archives: [archiveName] },
() => {
it('returns hasData: true', async () => {
const response = await supertest.get(`/api/apm/has_data`);
expect(response.status).to.be(200);
expect(response.body.hasData).to.be(true);
});
}
);
}

View file

@ -224,6 +224,10 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte
loadTestFile(require.resolve('./csm/web_core_vitals'));
});
describe('historical_data/has_data', function () {
loadTestFile(require.resolve('./historical_data/has_data'));
});
registry.run(providerContext);
});
}

View file

@ -37,7 +37,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
);
expect(response.status).to.be(200);
expect(response.body.hasHistoricalData).to.be(false);
expect(response.body.hasLegacyData).to.be(false);
expect(response.body.items.length).to.be(0);
});
@ -66,10 +65,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(response.status).to.eql(200);
});
it('returns hasHistoricalData: true', () => {
expect(response.body.hasHistoricalData).to.be(true);
});
it('returns hasLegacyData: false', () => {
expect(response.body.hasLegacyData).to.be(false);
});