Skip to content

Commit 374fea7

Browse files
committed
[UX] Add empty states (#80904)
* Add empty state for user experience metrics. * Add empty state for page load duration metrics. * Add empty state for core web vitals. * Fix bug injected by these changes. * Add a test.
1 parent 6b29f9f commit 374fea7

8 files changed

Lines changed: 96 additions & 23 deletions

File tree

x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,24 @@ const ClFlexGroup = styled(EuiFlexGroup)`
2222
}
2323
`;
2424

25+
function formatTitle(unit: string, value?: number) {
26+
if (typeof value === 'undefined') return I18LABELS.dataMissing;
27+
return formatToSec(value, unit);
28+
}
29+
30+
function PageViewsTotalTitle({ pageViews }: { pageViews?: number }) {
31+
if (typeof pageViews === 'undefined') {
32+
return <>{I18LABELS.dataMissing}</>;
33+
}
34+
return pageViews < 10000 ? (
35+
<>{numeral(pageViews).format('0,0')}</>
36+
) : (
37+
<EuiToolTip content={numeral(pageViews).format('0,0')}>
38+
<>{numeral(pageViews).format('0 a')}</>
39+
</EuiToolTip>
40+
);
41+
}
42+
2543
export function ClientMetrics() {
2644
const uxQuery = useUxQuery();
2745

@@ -50,38 +68,28 @@ export function ClientMetrics() {
5068

5169
const STAT_STYLE = { width: '240px' };
5270

53-
const pageViewsTotal = data?.pageViews?.value ?? 0;
54-
5571
return (
5672
<ClFlexGroup responsive={false}>
5773
<EuiFlexItem grow={false} style={STAT_STYLE}>
5874
<EuiStat
5975
titleSize="l"
60-
title={formatToSec(data?.backEnd?.value ?? 0, 'ms')}
76+
title={formatTitle('ms', data?.backEnd?.value)}
6177
description={I18LABELS.backEnd}
6278
isLoading={status !== 'success'}
6379
/>
6480
</EuiFlexItem>
6581
<EuiFlexItem grow={false} style={STAT_STYLE}>
6682
<EuiStat
6783
titleSize="l"
68-
title={formatToSec(data?.frontEnd?.value ?? 0, 'ms')}
84+
title={formatTitle('ms', data?.frontEnd?.value)}
6985
description={I18LABELS.frontEnd}
7086
isLoading={status !== 'success'}
7187
/>
7288
</EuiFlexItem>
7389
<EuiFlexItem grow={false} style={STAT_STYLE}>
7490
<EuiStat
7591
titleSize="l"
76-
title={
77-
pageViewsTotal < 10000 ? (
78-
numeral(pageViewsTotal).format('0,0')
79-
) : (
80-
<EuiToolTip content={numeral(pageViewsTotal).format('0,0')}>
81-
<>{numeral(pageViewsTotal).format('0 a')}</>
82-
</EuiToolTip>
83-
)
84-
}
92+
title={<PageViewsTotalTitle pageViews={data?.pageViews?.value} />}
8593
description={I18LABELS.pageViews}
8694
isLoading={status !== 'success'}
8795
/>

x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import React from 'react';
88
import { EuiFlexItem, EuiStat, EuiFlexGroup } from '@elastic/eui';
99
import numeral from '@elastic/numeral';
1010
import {
11+
DATA_UNDEFINED_LABEL,
1112
FCP_LABEL,
1213
LONGEST_LONG_TASK,
1314
NO_OF_LONG_TASK,
@@ -36,6 +37,11 @@ interface Props {
3637
loading: boolean;
3738
}
3839

40+
function formatTitle(unit: string, value?: number) {
41+
if (typeof value === 'undefined') return DATA_UNDEFINED_LABEL;
42+
return formatToSec(value, unit);
43+
}
44+
3945
export function KeyUXMetrics({ data, loading }: Props) {
4046
const uxQuery = useUxQuery();
4147

@@ -62,39 +68,43 @@ export function KeyUXMetrics({ data, loading }: Props) {
6268
<EuiFlexItem grow={false} style={STAT_STYLE}>
6369
<EuiStat
6470
titleSize="s"
65-
title={formatToSec(data?.fcp, 'ms')}
71+
title={formatTitle('ms', data?.fcp)}
6672
description={FCP_LABEL}
6773
isLoading={loading}
6874
/>
6975
</EuiFlexItem>
7076
<EuiFlexItem grow={false} style={STAT_STYLE}>
7177
<EuiStat
7278
titleSize="s"
73-
title={formatToSec(data?.tbt, 'ms')}
79+
title={formatTitle('ms', data?.tbt)}
7480
description={TBT_LABEL}
7581
isLoading={loading}
7682
/>
7783
</EuiFlexItem>
7884
<EuiFlexItem grow={false} style={STAT_STYLE}>
7985
<EuiStat
8086
titleSize="s"
81-
title={numeral(longTaskData?.noOfLongTasks ?? 0).format('0,0')}
87+
title={
88+
longTaskData?.noOfLongTasks
89+
? numeral(longTaskData.noOfLongTasks).format('0,0')
90+
: DATA_UNDEFINED_LABEL
91+
}
8292
description={NO_OF_LONG_TASK}
8393
isLoading={status !== 'success'}
8494
/>
8595
</EuiFlexItem>
8696
<EuiFlexItem grow={false} style={STAT_STYLE}>
8797
<EuiStat
8898
titleSize="s"
89-
title={formatToSec(longTaskData?.longestLongTask, 'ms')}
99+
title={formatTitle('ms', longTaskData?.longestLongTask)}
90100
description={LONGEST_LONG_TASK}
91101
isLoading={status !== 'success'}
92102
/>
93103
</EuiFlexItem>
94104
<EuiFlexItem grow={false} style={STAT_STYLE}>
95105
<EuiStat
96106
titleSize="s"
97-
title={formatToSec(longTaskData?.sumOfLongTasks, 'ms')}
107+
title={formatTitle('ms', longTaskData?.sumOfLongTasks)}
98108
description={SUM_LONG_TASKS}
99109
isLoading={status !== 'success'}
100110
/>

x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/translations.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66

77
import { i18n } from '@kbn/i18n';
88

9+
export const DATA_UNDEFINED_LABEL = i18n.translate(
10+
'xpack.apm.rum.coreVitals.dataUndefined',
11+
{
12+
defaultMessage: 'N/A',
13+
}
14+
);
15+
916
export const FCP_LABEL = i18n.translate('xpack.apm.rum.coreVitals.fcp', {
1017
defaultMessage: 'First contentful paint',
1118
});

x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import { i18n } from '@kbn/i18n';
88

99
export const I18LABELS = {
10+
dataMissing: i18n.translate('xpack.apm.rum.dashboard.dataMissing', {
11+
defaultMessage: 'N/A',
12+
}),
1013
backEnd: i18n.translate('xpack.apm.rum.dashboard.backend', {
1114
defaultMessage: 'Backend',
1215
}),

x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,27 @@ describe('UXSection', () => {
7676
expect(queryAllByText('View in app')).toEqual([]);
7777
expect(getByText('elastic-co-frontend')).toBeInTheDocument();
7878
});
79+
it('shows empty state', () => {
80+
jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
81+
data: undefined,
82+
status: fetcherHook.FETCH_STATUS.SUCCESS,
83+
refetch: jest.fn(),
84+
});
85+
const { getByText, queryAllByText, getAllByText } = render(
86+
<UXSection
87+
absoluteTime={{
88+
start: moment('2020-06-29T11:38:23.747Z').valueOf(),
89+
end: moment('2020-06-29T12:08:23.748Z').valueOf(),
90+
}}
91+
relativeTime={{ start: 'now-15m', end: 'now' }}
92+
bucketSize="60s"
93+
serviceName="elastic-co-frontend"
94+
/>
95+
);
96+
97+
expect(getByText('User Experience')).toBeInTheDocument();
98+
expect(getAllByText('No data is available.')).toHaveLength(3);
99+
expect(queryAllByText('View in app')).toEqual([]);
100+
expect(getByText('elastic-co-frontend')).toBeInTheDocument();
101+
});
79102
});

x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { EuiFlexGroup, EuiIconTip, euiPaletteForStatus, EuiSpacer, EuiStat } from '@elastic/eui';
7+
import {
8+
EuiCard,
9+
EuiFlexGroup,
10+
EuiIconTip,
11+
euiPaletteForStatus,
12+
EuiSpacer,
13+
EuiStat,
14+
} from '@elastic/eui';
815
import React, { useState } from 'react';
916
import { i18n } from '@kbn/i18n';
1017
import { PaletteLegends } from './palette_legends';
@@ -14,6 +21,7 @@ import {
1421
CV_GOOD_LABEL,
1522
LESS_LABEL,
1623
MORE_LABEL,
24+
NO_DATA,
1725
CV_POOR_LABEL,
1826
IS_LABEL,
1927
TAKES_LABEL,
@@ -26,7 +34,7 @@ export interface Thresholds {
2634

2735
interface Props {
2836
title: string;
29-
value: string;
37+
value?: string;
3038
ranks?: number[];
3139
loading: boolean;
3240
thresholds: Thresholds;
@@ -80,6 +88,9 @@ export function CoreVitalItem({
8088

8189
const biggestValIndex = ranks.indexOf(Math.max(...ranks));
8290

91+
if (value === undefined && ranks[0] === 100 && !loading) {
92+
return <EuiCard title={title} isDisabled={true} description={NO_DATA} />;
93+
}
8394
return (
8495
<>
8596
<EuiStat

x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ interface Props {
5050
serviceName?: string;
5151
}
5252

53+
function formatValue(value?: number) {
54+
if (typeof value === 'undefined') {
55+
return undefined;
56+
}
57+
return formatToSec(value, 'ms');
58+
}
59+
5360
export function CoreVitals({ data, loading, displayServiceName, serviceName }: Props) {
5461
const { lcp, lcpRanks, fid, fidRanks, cls, clsRanks } = data || {};
5562

@@ -63,7 +70,7 @@ export function CoreVitals({ data, loading, displayServiceName, serviceName }: P
6370
<EuiFlexItem style={{ flexBasis: 380 }}>
6471
<CoreVitalItem
6572
title={LCP_LABEL}
66-
value={formatToSec(lcp, 'ms')}
73+
value={formatValue(lcp)}
6774
ranks={lcpRanks}
6875
loading={loading}
6976
thresholds={CoreVitalsThresholds.LCP}
@@ -73,7 +80,7 @@ export function CoreVitals({ data, loading, displayServiceName, serviceName }: P
7380
<EuiFlexItem style={{ flexBasis: 380 }}>
7481
<CoreVitalItem
7582
title={FID_LABEL}
76-
value={formatToSec(fid, 'ms')}
83+
value={formatValue(fid)}
7784
ranks={fidRanks}
7885
loading={loading}
7986
thresholds={CoreVitalsThresholds.FID}
@@ -83,7 +90,7 @@ export function CoreVitals({ data, loading, displayServiceName, serviceName }: P
8390
<EuiFlexItem style={{ flexBasis: 380 }}>
8491
<CoreVitalItem
8592
title={CLS_LABEL}
86-
value={cls ?? '0'}
93+
value={cls}
8794
ranks={clsRanks}
8895
loading={loading}
8996
thresholds={CoreVitalsThresholds.CLS}

x-pack/plugins/observability/public/components/shared/core_web_vitals/translations.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
import { i18n } from '@kbn/i18n';
88

9+
export const NO_DATA = i18n.translate('xpack.observability.ux.coreVitals.noData', {
10+
defaultMessage: 'No data is available.',
11+
});
12+
913
export const LCP_LABEL = i18n.translate('xpack.observability.ux.coreVitals.lcp', {
1014
defaultMessage: 'Largest contentful paint',
1115
});

0 commit comments

Comments
 (0)