diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts index fff14376667b2..a58927dfbd12f 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { colourPalette } from './data_formatting'; +import { colourPalette, getSeriesAndDomain } from './data_formatting'; +import { NetworkItems } from './types'; describe('Palettes', () => { it('A colour palette comprising timing and mime type colours is correctly generated', () => { @@ -25,3 +26,434 @@ describe('Palettes', () => { }); }); }); + +describe('getSeriesAndDomain', () => { + const networkItems: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', + status: 200, + mimeType: 'text/css', + requestSentTime: 18098833.175, + requestStartTime: 18098835.439, + loadEndTime: 18098957.145, + timings: { + connect: 81.10800000213203, + wait: 34.577999998873565, + receive: 0.5520000013348181, + send: 0.3600000018195715, + total: 123.97000000055414, + proxy: -1, + blocked: 0.8540000017092098, + queueing: 2.263999998831423, + ssl: 55.38700000033714, + dns: 3.559999997378327, + }, + }, + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/director@1.2.8/build/director.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 18098833.537, + requestStartTime: 18098837.233999997, + loadEndTime: 18098977.648000002, + timings: { + blocked: 84.54599999822676, + receive: 3.068000001803739, + queueing: 3.69700000010198, + proxy: -1, + total: 144.1110000014305, + wait: 52.56100000042352, + connect: -1, + send: 0.2390000008745119, + ssl: -1, + dns: -1, + }, + }, + ]; + + const networkItemsWithoutFullTimings: NetworkItems = [ + networkItems[0], + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + timings: { + total: 2.7929999996558763, + blocked: -1, + ssl: -1, + wait: -1, + connect: -1, + dns: -1, + queueing: -1, + send: -1, + proxy: -1, + receive: -1, + }, + }, + ]; + + const networkItemsWithoutAnyTimings: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + timings: { + total: -1, + blocked: -1, + ssl: -1, + wait: -1, + connect: -1, + dns: -1, + queueing: -1, + send: -1, + proxy: -1, + receive: -1, + }, + }, + ]; + + const networkItemsWithoutTimingsObject: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + }, + ]; + + it('formats timings', () => { + const actual = getSeriesAndDomain(networkItems); + expect(actual).toMatchInlineSnapshot(` + Object { + "domain": Object { + "max": 140.7760000010603, + "min": 0, + }, + "series": Array [ + Object { + "config": Object { + "colour": "#b9a888", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#b9a888", + "value": "Queued / Blocked: 0.854ms", + }, + }, + "x": 0, + "y": 0.8540000017092098, + "y0": 0, + }, + Object { + "config": Object { + "colour": "#54b399", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#54b399", + "value": "DNS: 3.560ms", + }, + }, + "x": 0, + "y": 4.413999999087537, + "y0": 0.8540000017092098, + }, + Object { + "config": Object { + "colour": "#da8b45", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#da8b45", + "value": "Connecting: 25.721ms", + }, + }, + "x": 0, + "y": 30.135000000882428, + "y0": 4.413999999087537, + }, + Object { + "config": Object { + "colour": "#edc5a2", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#edc5a2", + "value": "TLS: 55.387ms", + }, + }, + "x": 0, + "y": 85.52200000121957, + "y0": 30.135000000882428, + }, + Object { + "config": Object { + "colour": "#d36086", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#d36086", + "value": "Sending request: 0.360ms", + }, + }, + "x": 0, + "y": 85.88200000303914, + "y0": 85.52200000121957, + }, + Object { + "config": Object { + "colour": "#b0c9e0", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#b0c9e0", + "value": "Waiting (TTFB): 34.578ms", + }, + }, + "x": 0, + "y": 120.4600000019127, + "y0": 85.88200000303914, + }, + Object { + "config": Object { + "colour": "#ca8eae", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#ca8eae", + "value": "Content downloading (CSS): 0.552ms", + }, + }, + "x": 0, + "y": 121.01200000324752, + "y0": 120.4600000019127, + }, + Object { + "config": Object { + "colour": "#b9a888", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#b9a888", + "value": "Queued / Blocked: 84.546ms", + }, + }, + "x": 1, + "y": 84.90799999795854, + "y0": 0.3619999997317791, + }, + Object { + "config": Object { + "colour": "#d36086", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#d36086", + "value": "Sending request: 0.239ms", + }, + }, + "x": 1, + "y": 85.14699999883305, + "y0": 84.90799999795854, + }, + Object { + "config": Object { + "colour": "#b0c9e0", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#b0c9e0", + "value": "Waiting (TTFB): 52.561ms", + }, + }, + "x": 1, + "y": 137.70799999925657, + "y0": 85.14699999883305, + }, + Object { + "config": Object { + "colour": "#9170b8", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#9170b8", + "value": "Content downloading (JS): 3.068ms", + }, + }, + "x": 1, + "y": 140.7760000010603, + "y0": 137.70799999925657, + }, + ], + } + `); + }); + + it('handles formatting when only total timing values are available', () => { + const actual = getSeriesAndDomain(networkItemsWithoutFullTimings); + expect(actual).toMatchInlineSnapshot(` + Object { + "domain": Object { + "max": 121.01200000324752, + "min": 0, + }, + "series": Array [ + Object { + "config": Object { + "colour": "#b9a888", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#b9a888", + "value": "Queued / Blocked: 0.854ms", + }, + }, + "x": 0, + "y": 0.8540000017092098, + "y0": 0, + }, + Object { + "config": Object { + "colour": "#54b399", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#54b399", + "value": "DNS: 3.560ms", + }, + }, + "x": 0, + "y": 4.413999999087537, + "y0": 0.8540000017092098, + }, + Object { + "config": Object { + "colour": "#da8b45", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#da8b45", + "value": "Connecting: 25.721ms", + }, + }, + "x": 0, + "y": 30.135000000882428, + "y0": 4.413999999087537, + }, + Object { + "config": Object { + "colour": "#edc5a2", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#edc5a2", + "value": "TLS: 55.387ms", + }, + }, + "x": 0, + "y": 85.52200000121957, + "y0": 30.135000000882428, + }, + Object { + "config": Object { + "colour": "#d36086", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#d36086", + "value": "Sending request: 0.360ms", + }, + }, + "x": 0, + "y": 85.88200000303914, + "y0": 85.52200000121957, + }, + Object { + "config": Object { + "colour": "#b0c9e0", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#b0c9e0", + "value": "Waiting (TTFB): 34.578ms", + }, + }, + "x": 0, + "y": 120.4600000019127, + "y0": 85.88200000303914, + }, + Object { + "config": Object { + "colour": "#ca8eae", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#ca8eae", + "value": "Content downloading (CSS): 0.552ms", + }, + }, + "x": 0, + "y": 121.01200000324752, + "y0": 120.4600000019127, + }, + Object { + "config": Object { + "colour": "#9170b8", + "showTooltip": true, + "tooltipProps": Object { + "colour": "#9170b8", + "value": "Content downloading (JS): 2.793ms", + }, + }, + "x": 1, + "y": 3.714999998046551, + "y0": 0.9219999983906746, + }, + ], + } + `); + }); + + it('handles formatting when there is no timing information available', () => { + const actual = getSeriesAndDomain(networkItemsWithoutAnyTimings); + expect(actual).toMatchInlineSnapshot(` + Object { + "domain": Object { + "max": 0, + "min": 0, + }, + "series": Array [ + Object { + "config": Object { + "colour": "", + "showTooltip": false, + "tooltipProps": undefined, + }, + "x": 0, + "y": 0, + "y0": 0, + }, + ], + } + `); + }); + + it('handles formatting when the timings object is undefined', () => { + const actual = getSeriesAndDomain(networkItemsWithoutTimingsObject); + expect(actual).toMatchInlineSnapshot(` + Object { + "domain": Object { + "max": 0, + "min": 0, + }, + "series": Array [ + Object { + "config": Object { + "showTooltip": false, + }, + "x": 0, + "y": 0, + "y0": 0, + }, + ], + } + `); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts index 7c6e176315b5b..43fa93fa5f6f2 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts @@ -38,6 +38,23 @@ const getColourForMimeType = (mimeType?: string) => { return colourPalette[key]; }; +const getFriendlyTooltipValue = ({ + value, + timing, + mimeType, +}: { + value: number; + timing: Timings; + mimeType?: string; +}) => { + let label = FriendlyTimingLabels[timing]; + if (timing === Timings.Receive && mimeType) { + const formattedMimeType: MimeType = MimeTypesMap[mimeType]; + label += ` (${FriendlyMimetypeLabels[formattedMimeType]})`; + } + return `${label}: ${formatValueForDisplay(value)}ms`; +}; + export const getSeriesAndDomain = (items: NetworkItems) => { const getValueForOffset = (item: NetworkItem) => { return item.requestSentTime; @@ -61,16 +78,26 @@ export const getSeriesAndDomain = (items: NetworkItems) => { }; const series = items.reduce((acc, item, index) => { - if (!item.timings) return acc; + if (!item.timings) { + acc.push({ + x: index, + y0: 0, + y: 0, + config: { + showTooltip: false, + }, + }); + return acc; + } const offsetValue = getValueForOffset(item); + const mimeTypeColour = getColourForMimeType(item.mimeType); let currentOffset = offsetValue - zeroOffset; TIMING_ORDER.forEach((timing) => { const value = getValue(item.timings, timing); - const colour = - timing === Timings.Receive ? getColourForMimeType(item.mimeType) : colourPalette[timing]; + const colour = timing === Timings.Receive ? mimeTypeColour : colourPalette[timing]; if (value && value >= 0) { const y = currentOffset + value; @@ -80,10 +107,13 @@ export const getSeriesAndDomain = (items: NetworkItems) => { y, config: { colour, + showTooltip: true, tooltipProps: { - value: `${FriendlyTimingLabels[timing]}: ${formatValueForDisplay( - y - currentOffset - )}ms`, + value: getFriendlyTooltipValue({ + value: y - currentOffset, + timing, + mimeType: item.mimeType, + }), colour, }, }, @@ -91,6 +121,33 @@ export const getSeriesAndDomain = (items: NetworkItems) => { currentOffset = y; } }); + + /* if no specific timing values are found, use the total time + * if total time is not available use 0, set showTooltip to false, + * and omit tooltip props */ + if (!acc.find((entry) => entry.x === index)) { + const total = item.timings.total; + const hasTotal = total !== -1; + acc.push({ + x: index, + y0: hasTotal ? currentOffset : 0, + y: hasTotal ? currentOffset + item.timings.total : 0, + config: { + colour: hasTotal ? mimeTypeColour : '', + showTooltip: hasTotal, + tooltipProps: hasTotal + ? { + value: getFriendlyTooltipValue({ + value: total, + timing: Timings.Receive, + mimeType: item.mimeType, + }), + colour: mimeTypeColour, + } + : undefined, + }, + }); + } return acc; }, []); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx index 7aa5a22849321..a84765c4ea154 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx @@ -70,7 +70,7 @@ export const WaterfallChartWrapper: React.FC = ({ data }) => { sidebarItems={sidebarItems} legendItems={legendItems} renderTooltipItem={(tooltipProps) => { - return {tooltipProps.value}; + return {tooltipProps?.value}; }} > { +const Tooltip = (tooltipInfo: TooltipInfo) => { const { data, renderTooltipItem } = useWaterfallContext(); const relevantItems = data.filter((item) => { - return item.x === header?.value; + return ( + item.x === tooltipInfo.header?.value && item.config.showTooltip && item.config.tooltipProps + ); }); - return ( + return relevantItems.length ? ( {relevantItems.map((item, index) => { @@ -52,7 +54,7 @@ const Tooltip = ({ header }: TooltipInfo) => { })} - ); + ) : null; }; export type RenderItem = (item: I, index: number) => JSX.Element; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts index d6901fb482599..fd7f9effcd193 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts @@ -11,7 +11,8 @@ interface PlotProperties { } export interface WaterfallDataSeriesConfigProperties { - tooltipProps: Record; + tooltipProps?: Record; + showTooltip: boolean; } export type WaterfallDataEntry = PlotProperties & {