Skip to content

Commit

Permalink
[Uptime] Added trends chart and deep link to exp view on waterfall st…
Browse files Browse the repository at this point in the history
…ep metric markers (#114068)

* added trend chart and link to exp view

* Added step level filtering/breakdowns

* fix type

* make step and monitor name single select

* fix test

* added step filters

* pr feedback

* hide step filter for non step metrics

* pr feedback

* remove step def when it's not applicable

* ^^also for step name breakdown

* Update x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx

Co-authored-by: Dominique Clarke <[email protected]>

* use side effct

* update monitor filter

* update all_value usage

* refactor

* fix type

* use last value operation

* use last 48 intervals

Co-authored-by: Dominique Clarke <[email protected]>
shahzad31 and dominiqueclarke authored Oct 19, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent ea00e60 commit 5477f60
Showing 13 changed files with 293 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ import {
AvgIndexPatternColumn,
MedianIndexPatternColumn,
PercentileIndexPatternColumn,
LastValueIndexPatternColumn,
OperationType,
PersistedIndexPatternLayer,
RangeIndexPatternColumn,
@@ -219,13 +220,52 @@ export class LensAttributes {
columnFilter,
});
}
if (operationType === 'last_value') {
return this.getLastValueOperationColumn({
sourceField,
operationType,
label,
seriesConfig,
columnFilter,
});
}
if (operationType?.includes('th')) {
return this.getPercentileNumberColumn(sourceField, operationType, seriesConfig!);
}
}
return this.getNumberRangeColumn(sourceField, seriesConfig!, label);
}

getLastValueOperationColumn({
sourceField,
label,
seriesConfig,
operationType,
columnFilter,
}: {
sourceField: string;
operationType: 'last_value';
label?: string;
seriesConfig: SeriesConfig;
columnFilter?: ColumnFilter;
}): LastValueIndexPatternColumn {
return {
...buildNumberColumn(sourceField),
operationType,
label: i18n.translate('xpack.observability.expView.columns.operation.label', {
defaultMessage: '{operationType} of {sourceField}',
values: {
sourceField: label || seriesConfig.labels[sourceField],
operationType: capitalize(operationType),
},
}),
filter: columnFilter,
params: {
sortField: '@timestamp',
},
};
}

getNumberOperationColumn({
sourceField,
label,
@@ -622,6 +662,12 @@ export class LensAttributes {

const label = timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label;

let filterQuery = columnFilter || mainYAxis.filter?.query;

if (columnFilter && mainYAxis.filter?.query) {
filterQuery = `${columnFilter} and ${mainYAxis.filter.query}`;
}

layers[layerId] = {
columnOrder: [
`x-axis-column-${layerId}`,
@@ -637,9 +683,7 @@ export class LensAttributes {
...mainYAxis,
label,
filter: {
query: mainYAxis.filter
? `${columnFilter} and ${mainYAxis.filter.query}`
: columnFilter,
query: filterQuery ?? '',
language: 'kuery',
},
...(timeShift ? { timeShift } : {}),
Original file line number Diff line number Diff line change
@@ -111,44 +111,48 @@ export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesCon
field: SYNTHETICS_LCP,
id: SYNTHETICS_LCP,
columnType: OPERATION_COLUMN,
columnFilters: [STEP_METRIC_FILTER],
columnFilters: getStepMetricColumnFilter(SYNTHETICS_LCP),
},
{
label: FCP_LABEL,
field: SYNTHETICS_FCP,
id: SYNTHETICS_FCP,
columnType: OPERATION_COLUMN,
columnFilters: [STEP_METRIC_FILTER],
columnFilters: getStepMetricColumnFilter(SYNTHETICS_FCP),
},
{
label: DCL_LABEL,
field: SYNTHETICS_DCL,
id: SYNTHETICS_DCL,
columnType: OPERATION_COLUMN,
columnFilters: [STEP_METRIC_FILTER],
columnFilters: getStepMetricColumnFilter(SYNTHETICS_DCL),
},
{
label: DOCUMENT_ONLOAD_LABEL,
field: SYNTHETICS_DOCUMENT_ONLOAD,
id: SYNTHETICS_DOCUMENT_ONLOAD,
columnType: OPERATION_COLUMN,
columnFilters: [STEP_METRIC_FILTER],
columnFilters: getStepMetricColumnFilter(SYNTHETICS_DOCUMENT_ONLOAD),
},
{
label: CLS_LABEL,
field: SYNTHETICS_CLS,
id: SYNTHETICS_CLS,
columnType: OPERATION_COLUMN,
columnFilters: [STEP_METRIC_FILTER],
columnFilters: getStepMetricColumnFilter(SYNTHETICS_CLS),
},
],
labels: { ...FieldLabels, [SUMMARY_UP]: UP_LABEL, [SUMMARY_DOWN]: DOWN_LABEL },
};
}

const STEP_METRIC_FILTER: ColumnFilter = {
language: 'kuery',
query: `synthetics.type: step/metrics`,
const getStepMetricColumnFilter = (field: string): ColumnFilter[] => {
return [
{
language: 'kuery',
query: `synthetics.type: step/metrics and ${field}: *`,
},
];
};

const STEP_END_FILTER: ColumnFilter = {
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import { AllSeries, useTheme } from '../../../..';
import { LayerConfig, LensAttributes } from '../configurations/lens_attributes';
import { ReportViewType } from '../types';
import { getLayerConfigs } from '../hooks/use_lens_attributes';
import { LensPublicStart } from '../../../../../../lens/public';
import { LensPublicStart, XYState } from '../../../../../../lens/public';
import { OperationTypeComponent } from '../series_editor/columns/operation_type_select';
import { IndexPatternState } from '../hooks/use_app_index_pattern';

@@ -22,6 +22,8 @@ export interface ExploratoryEmbeddableProps {
appendTitle?: JSX.Element;
title: string | JSX.Element;
showCalculationMethod?: boolean;
axisTitlesVisibility?: XYState['axisTitlesVisibilitySettings'];
legendIsVisible?: boolean;
}

export interface ExploratoryEmbeddableComponentProps extends ExploratoryEmbeddableProps {
@@ -37,6 +39,8 @@ export default function Embeddable({
appendTitle,
indexPatterns,
lens,
axisTitlesVisibility,
legendIsVisible,
showCalculationMethod = false,
}: ExploratoryEmbeddableComponentProps) {
const LensComponent = lens?.EmbeddableComponent;
@@ -57,11 +61,20 @@ export default function Embeddable({
return <EuiText>No lens component</EuiText>;
}

const attributesJSON = lensAttributes.getJSON();

(attributesJSON.state.visualization as XYState).axisTitlesVisibilitySettings =
axisTitlesVisibility;

if (typeof legendIsVisible !== 'undefined') {
(attributesJSON.state.visualization as XYState).legend.isVisible = legendIsVisible;
}

return (
<Wrapper>
<EuiFlexGroup>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiTitle size="s">
<EuiTitle size="xs">
<h3>{title}</h3>
</EuiTitle>
</EuiFlexItem>
@@ -81,7 +94,7 @@ export default function Embeddable({
id="exploratoryView"
style={{ height: '100%' }}
timeRange={series?.time}
attributes={lensAttributes.getJSON()}
attributes={attributesJSON}
onBrushEnd={({ range }) => {}}
/>
</Wrapper>
@@ -92,7 +105,7 @@ const Wrapper = styled.div`
height: 100%;
&&& {
> :nth-child(2) {
height: calc(100% - 56px);
height: calc(100% - 32px);
}
}
`;
Original file line number Diff line number Diff line change
@@ -73,6 +73,12 @@ export function OperationTypeComponent({
defaultMessage: 'Sum',
}),
},
{
value: 'last_value' as OperationType,
inputDisplay: i18n.translate('xpack.observability.expView.operationType.lastValue', {
defaultMessage: 'Last value',
}),
},
{
value: '75th' as OperationType,
inputDisplay: i18n.translate('xpack.observability.expView.operationType.75thPercentile', {
4 changes: 4 additions & 0 deletions x-pack/plugins/uptime/common/runtime_types/ping/synthetics.ts
Original file line number Diff line number Diff line change
@@ -22,6 +22,10 @@ export const JourneyStepType = t.intersection([
name: t.string,
status: t.string,
type: t.string,
timespan: t.type({
gte: t.string,
lt: t.string,
}),
}),
synthetics: t.partial({
error: t.partial({
Original file line number Diff line number Diff line change
@@ -45,7 +45,11 @@ export const StepDetailContainer: React.FC<Props> = ({ checkGroup, stepIndex })
</EuiFlexGroup>
)}
{journey && activeStep && !journey.loading && (
<WaterfallChartContainer checkGroup={checkGroup} stepIndex={stepIndex} />
<WaterfallChartContainer
checkGroup={checkGroup}
stepIndex={stepIndex}
activeStep={activeStep}
/>
)}
</>
);
Original file line number Diff line number Diff line change
@@ -15,17 +15,19 @@ import { networkEventsSelector } from '../../../../../state/selectors';
import { WaterfallChartWrapper } from './waterfall_chart_wrapper';
import { extractItems } from './data_formatting';
import { useStepWaterfallMetrics } from '../use_step_waterfall_metrics';
import { JourneyStep } from '../../../../../../common/runtime_types';

export const NO_DATA_TEXT = i18n.translate('xpack.uptime.synthetics.stepDetail.waterfallNoData', {
defaultMessage: 'No waterfall data could be found for this step',
});

interface Props {
checkGroup: string;
activeStep?: JourneyStep;
stepIndex: number;
}

export const WaterfallChartContainer: React.FC<Props> = ({ checkGroup, stepIndex }) => {
export const WaterfallChartContainer: React.FC<Props> = ({ checkGroup, stepIndex, activeStep }) => {
const dispatch = useDispatch();

useEffect(() => {
@@ -79,6 +81,7 @@ export const WaterfallChartContainer: React.FC<Props> = ({ checkGroup, stepIndex
data={extractItems(networkEvents.events)}
markerItems={metrics}
total={networkEvents.total}
activeStep={activeStep}
/>
)}
{waterfallLoaded && hasEvents && !isWaterfallSupported && (
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ import { WaterfallFilter } from './waterfall_filter';
import { WaterfallFlyout } from './waterfall_flyout';
import { WaterfallSidebarItem } from './waterfall_sidebar_item';
import { MarkerItems } from '../../waterfall/context/waterfall_chart';
import { JourneyStep } from '../../../../../../common/runtime_types';

export const renderLegendItem: RenderItem<LegendItem> = (item) => {
return (
@@ -26,11 +27,17 @@ export const renderLegendItem: RenderItem<LegendItem> = (item) => {

interface Props {
total: number;
activeStep?: JourneyStep;
data: NetworkItems;
markerItems?: MarkerItems;
}

export const WaterfallChartWrapper: React.FC<Props> = ({ data, total, markerItems }) => {
export const WaterfallChartWrapper: React.FC<Props> = ({
data,
total,
markerItems,
activeStep,
}) => {
const [query, setQuery] = useState<string>('');
const [activeFilters, setActiveFilters] = useState<string[]>([]);
const [onlyHighlighted, setOnlyHighlighted] = useState(false);
@@ -109,6 +116,7 @@ export const WaterfallChartWrapper: React.FC<Props> = ({ data, total, markerItem

return (
<WaterfallProvider
activeStep={activeStep}
markerItems={markerItems}
totalNetworkRequests={total}
fetchedNetworkRequests={networkData.length}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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, { useState } from 'react';
import { EuiButtonIcon, EuiIcon, EuiPopover } from '@elastic/eui';
import { WaterfallMarkerTrend } from './waterfall_marker_trend';

export function WaterfallMarkerIcon({ field, label }: { field: string; label: string }) {
const [isOpen, setIsOpen] = useState(false);

if (!field) {
return <EuiIcon type="dot" size="l" />;
}

return (
<EuiPopover
isOpen={isOpen}
closePopover={() => setIsOpen(false)}
anchorPosition="downLeft"
panelStyle={{ paddingBottom: 0, paddingLeft: 4 }}
zIndex={100}
button={
<EuiButtonIcon
iconType="dot"
iconSize="l"
color="text"
onClick={() => setIsOpen((prevState) => !prevState)}
/>
}
>
<WaterfallMarkerTrend title={label} field={field} />
</EuiPopover>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* 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 { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { useUptimeStartPlugins } from '../../../../../contexts/uptime_startup_plugins_context';
import { useUptimeSettingsContext } from '../../../../../contexts/uptime_settings_context';
import { AllSeries, createExploratoryViewUrl } from '../../../../../../../observability/public';
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
import { useWaterfallContext } from '../context/waterfall_chart';
import { JourneyStep } from '../../../../../../common/runtime_types';

const getLast48Intervals = (activeStep: JourneyStep) => {
const { lt, gte } = activeStep.monitor.timespan!;
const inDays = moment(lt).diff(moment(gte), 'days');
if (inDays > 0) {
return { to: 'now', from: `now-${inDays * 48}d` };
}

const inHours = moment(lt).diff(moment(gte), 'hours');
if (inHours > 0) {
return { to: 'now', from: `now-${inHours * 48}h` };
}

const inMinutes = moment(lt).diff(moment(gte), 'minutes');
if (inMinutes > 0) {
return { to: 'now', from: `now-${inMinutes * 48}m` };
}

const inSeconds = moment(lt).diff(moment(gte), 'seconds');
return { to: 'now', from: `now-${inSeconds * 48}s` };
};

export function WaterfallMarkerTrend({ title, field }: { title: string; field: string }) {
const { observability } = useUptimeStartPlugins();

const EmbeddableExpVIew = observability!.ExploratoryViewEmbeddable;

const { basePath } = useUptimeSettingsContext();

const { activeStep } = useWaterfallContext();

if (!activeStep) {
return null;
}

const allSeries: AllSeries = [
{
name: `${title}(${activeStep.synthetics.step?.name!})`,
selectedMetricField: field,
time: getLast48Intervals(activeStep),
seriesType: 'area',
dataType: 'synthetics',
reportDefinitions: {
'monitor.name': [activeStep.monitor.name!],
'synthetics.step.name.keyword': [activeStep.synthetics.step?.name!],
},
operationType: 'last_value',
},
];

const href = createExploratoryViewUrl(
{
reportType: 'kpi-over-time',
allSeries,
},
basePath
);

return (
<Wrapper>
<EmbeddableExpVIew
title={title}
appendTitle={
<EuiButton iconType={'visArea'} href={href} target="_blank" size="s">
{EXPLORE_LABEL}
</EuiButton>
}
reportType={'kpi-over-time'}
attributes={allSeries}
axisTitlesVisibility={{ x: false, yLeft: false, yRight: false }}
legendIsVisible={false}
/>
</Wrapper>
);
}

export const EXPLORE_LABEL = i18n.translate('xpack.uptime.synthetics.markers.explore', {
defaultMessage: 'Explore',
});

const Wrapper = euiStyled.div`
height: 200px;
width: 400px;
&&& {
.expExpressionRenderer__expression {
padding-bottom: 0 !important;
}
}
`;
Original file line number Diff line number Diff line change
@@ -7,11 +7,11 @@

import React from 'react';
import { AnnotationDomainType, LineAnnotation } from '@elastic/charts';
import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useWaterfallContext } from '..';
import { useTheme } from '../../../../../../../observability/public';
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
import { WaterfallMarkerIcon } from './waterfall_marker_icon';

export const FCP_LABEL = i18n.translate('xpack.uptime.synthetics.waterfall.fcpLabel', {
defaultMessage: 'First contentful paint',
@@ -39,6 +39,12 @@ export const DOCUMENT_CONTENT_LOADED_LABEL = i18n.translate(
}
);

export const SYNTHETICS_CLS = 'browser.experience.cls';
export const SYNTHETICS_LCP = 'browser.experience.lcp.us';
export const SYNTHETICS_FCP = 'browser.experience.fcp.us';
export const SYNTHETICS_DOCUMENT_ONLOAD = 'browser.experience.load.us';
export const SYNTHETICS_DCL = 'browser.experience.dcl.us';

export function WaterfallChartMarkers() {
const { markerItems } = useWaterfallContext();

@@ -48,12 +54,32 @@ export function WaterfallChartMarkers() {
return null;
}

const markersInfo: Record<string, { label: string; color: string }> = {
domContentLoaded: { label: DOCUMENT_CONTENT_LOADED_LABEL, color: theme.eui.euiColorVis0 },
firstContentfulPaint: { label: FCP_LABEL, color: theme.eui.euiColorVis1 },
largestContentfulPaint: { label: LCP_LABEL, color: theme.eui.euiColorVis2 },
layoutShift: { label: LAYOUT_SHIFT_LABEL, color: theme.eui.euiColorVis3 },
loadEvent: { label: LOAD_EVENT_LABEL, color: theme.eui.euiColorVis9 },
const markersInfo: Record<string, { label: string; color: string; field: string }> = {
domContentLoaded: {
label: DOCUMENT_CONTENT_LOADED_LABEL,
color: theme.eui.euiColorVis0,
field: SYNTHETICS_DCL,
},
firstContentfulPaint: {
label: FCP_LABEL,
color: theme.eui.euiColorVis1,
field: SYNTHETICS_FCP,
},
largestContentfulPaint: {
label: LCP_LABEL,
color: theme.eui.euiColorVis2,
field: SYNTHETICS_LCP,
},
layoutShift: {
label: LAYOUT_SHIFT_LABEL,
color: theme.eui.euiColorVis3,
field: SYNTHETICS_CLS,
},
loadEvent: {
label: LOAD_EVENT_LABEL,
color: theme.eui.euiColorVis9,
field: SYNTHETICS_DOCUMENT_ONLOAD,
},
};

return (
@@ -73,7 +99,9 @@ export function WaterfallChartMarkers() {
}),
},
]}
marker={<EuiIcon type="dot" size="l" />}
marker={
<WaterfallMarkerIcon field={markersInfo[id]?.field} label={markersInfo[id]?.label} />
}
style={{
line: {
strokeWidth: 2,
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import React, { createContext, useContext, Context } from 'react';
import { WaterfallData, WaterfallDataEntry, WaterfallMetadata } from '../types';
import { OnSidebarClick, OnElementClick, OnProjectionClick } from '../components/use_flyout';
import { SidebarItems } from '../../step_detail/waterfall/types';
import { JourneyStep } from '../../../../../../common/runtime_types';

export type MarkerItems = Array<{
id:
@@ -38,6 +39,7 @@ export interface IWaterfallContext {
index?: number
) => JSX.Element;
markerItems?: MarkerItems;
activeStep?: JourneyStep;
}

export const WaterfallContext = createContext<Partial<IWaterfallContext>>({});
@@ -56,6 +58,7 @@ interface ProviderProps {
metadata: IWaterfallContext['metadata'];
renderTooltipItem: IWaterfallContext['renderTooltipItem'];
markerItems?: MarkerItems;
activeStep?: JourneyStep;
}

export const WaterfallProvider: React.FC<ProviderProps> = ({
@@ -73,11 +76,13 @@ export const WaterfallProvider: React.FC<ProviderProps> = ({
totalNetworkRequests,
highlightedNetworkRequests,
fetchedNetworkRequests,
activeStep,
}) => {
return (
<WaterfallContext.Provider
value={{
data,
activeStep,
markerItems,
showOnlyHighlightedNetworkRequests,
sidebarItems,
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
* 2.0.
*/

import React, { createContext } from 'react';
import React, { createContext, useContext } from 'react';
import { ClientPluginsStart } from '../apps/plugin';

export const UptimeStartupPluginsContext = createContext<Partial<ClientPluginsStart>>({});
@@ -14,3 +14,5 @@ export const UptimeStartupPluginsContextProvider: React.FC<Partial<ClientPlugins
children,
...props
}) => <UptimeStartupPluginsContext.Provider value={{ ...props }} children={children} />;

export const useUptimeStartPlugins = () => useContext(UptimeStartupPluginsContext);

0 comments on commit 5477f60

Please sign in to comment.