From bcbc4ae96f60fbb196bdfb4e7c6e73e68acaf3c5 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Tue, 29 Sep 2020 16:10:01 -0500 Subject: [PATCH] Empty prompt and loading spinner for service map (#78382) (#78853) * Empty prompt for service map To be shown when fetching returns no elements. Match the style of the license prompt. * Add loading spinner --- .../app/ServiceMap/empty_prompt.tsx | 35 +++++++++ .../components/app/ServiceMap/index.test.tsx | 76 +++++++++++++++---- .../components/app/ServiceMap/index.tsx | 72 +++++++++++++----- 3 files changed, 148 insertions(+), 35 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/ServiceMap/empty_prompt.tsx diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/empty_prompt.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/empty_prompt.tsx new file mode 100644 index 00000000000000..128b5696b23b88 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/empty_prompt.tsx @@ -0,0 +1,35 @@ +/* + * 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 { EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink'; + +export function EmptyPrompt() { + return ( + + {i18n.translate('xpack.apm.serviceMap.noServicesPromptTitle', { + defaultMessage: 'No services available', + })} + + } + body={ +

+ {i18n.translate('xpack.apm.serviceMap.noServicesPromptDescription', { + defaultMessage: + 'We can’t find any services to map within the currently selected time range and environment. Please try another range or check the environment selected. If you don’t have any services, please use our setup instructions to get started.', + })} +

+ } + actions={} + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx index f504ac1f68ce19..2a5b4ce44ff467 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx @@ -9,16 +9,30 @@ import { CoreStart } from 'kibana/public'; import React, { ReactNode } from 'react'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; import { License } from '../../../../../licensing/common/license'; +import { EuiThemeProvider } from '../../../../../observability/public'; +import { FETCH_STATUS } from '../../../../../observability/public/hooks/use_fetcher'; import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; import { LicenseContext } from '../../../context/LicenseContext'; +import * as useFetcherModule from '../../../hooks/useFetcher'; import { ServiceMap } from './'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiStats: () => {} }, } as Partial); +const activeLicense = new License({ + signature: 'active test signature', + license: { + expiryDateInMillis: 0, + mode: 'platinum', + status: 'active', + type: 'platinum', + uid: '1', + }, +}); + const expiredLicense = new License({ - signature: 'test signature', + signature: 'expired test signature', license: { expiryDateInMillis: 0, mode: 'platinum', @@ -28,26 +42,58 @@ const expiredLicense = new License({ }, }); -function Wrapper({ children }: { children?: ReactNode }) { - return ( - - - {children} - - - ); +function createWrapper(license: License | null) { + return ({ children }: { children?: ReactNode }) => { + return ( + + + + + {children} + + + + + ); + }; } describe('ServiceMap', () => { - describe('with an inactive license', () => { + describe('with no license', () => { + it('renders null', async () => { + expect( + await render(, { + wrapper: createWrapper(null), + }).queryByTestId('ServiceMap') + ).not.toBeInTheDocument(); + }); + }); + + describe('with an expired license', () => { it('renders the license banner', async () => { expect( - ( + await render(, { + wrapper: createWrapper(expiredLicense), + }).findAllByText(/Platinum/) + ).toHaveLength(1); + }); + }); + + describe('with an active license', () => { + describe('with an empty response', () => { + it('renders the empty banner', async () => { + jest.spyOn(useFetcherModule, 'useFetcher').mockReturnValueOnce({ + data: { elements: [] }, + refetch: () => {}, + status: FETCH_STATUS.SUCCESS, + }); + + expect( await render(, { - wrapper: Wrapper, - }).findAllByText(/Platinum/) - ).length - ).toBeGreaterThan(0); + wrapper: createWrapper(activeLicense), + }).findAllByText(/No services available/) + ).toHaveLength(1); + }); }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index bb450131bdfb88..1d2e4ada43add8 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import React, { ReactNode } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { invalidLicenseMessage, isActivePlatinumLicense, } from '../../../../common/service_map'; -import { useFetcher } from '../../../hooks/useFetcher'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; import { useTheme } from '../../../hooks/useTheme'; import { useUrlParams } from '../../../hooks/useUrlParams'; @@ -21,6 +21,7 @@ import { Controls } from './Controls'; import { Cytoscape } from './Cytoscape'; import { getCytoscapeDivStyle } from './cytoscapeOptions'; import { EmptyBanner } from './EmptyBanner'; +import { EmptyPrompt } from './empty_prompt'; import { Popover } from './Popover'; import { useRefDimensions } from './useRefDimensions'; @@ -28,12 +29,39 @@ interface ServiceMapProps { serviceName?: string; } +function PromptContainer({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} + +function LoadingSpinner() { + return ( + + ); +} + export function ServiceMap({ serviceName }: ServiceMapProps) { const theme = useTheme(); const license = useLicense(); const { urlParams } = useUrlParams(); - const { data = { elements: [] } } = useFetcher(() => { + const { data = { elements: [] }, status } = useFetcher(() => { // When we don't have a license or a valid license, don't make the request. if (!license || !isActivePlatinumLicense(license)) { return; @@ -65,37 +93,41 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { return null; } - return isActivePlatinumLicense(license) ? ( + if (!isActivePlatinumLicense(license)) { + return ( + + + + ); + } + + if (status === FETCH_STATUS.SUCCESS && data.elements.length === 0) { + return ( + + + + ); + } + + return (
{serviceName && } + {status === FETCH_STATUS.LOADING && }
- ) : ( - - - - - ); }