[Synthetics UI] Monitor history tab (#143516)

## Summary

Closes https://github.com/elastic/kibana/issues/142997

Contents for the monitor history tab, minus the status widget.

Co-authored-by: shahzad31 <shahzad.muhammad@elastic.co>
This commit is contained in:
Alejandro Fernández Gómez 2022-11-10 14:01:49 +01:00 committed by GitHub
parent cbc7fe10f6
commit a1128cf3cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 345 additions and 19 deletions

View file

@ -105,7 +105,19 @@ export function getSyntheticsKPIConfig({ dataView }: ConfigProps): SeriesConfig
columnFilters: [
{
language: 'kuery',
query: `state.id: * and state.up: 0`,
query: `summary: * and summary.down > 0 and and monitor.status: "down"`,
},
],
},
{
label: 'Monitor Complete',
id: 'state.up',
field: 'state.up',
columnType: OPERATION_COLUMN,
columnFilters: [
{
language: 'kuery',
query: `summary: * and summary.down: 0 and monitor.status: "up"`,
},
],
},

View file

@ -93,6 +93,30 @@ export function getSyntheticsSingleMetricConfig({ dataView }: ConfigProps): Seri
titlePosition: 'bottom',
},
},
{
id: 'monitor_total_runs',
label: i18n.translate('xpack.observability.expView.totalRuns', {
defaultMessage: 'Total Runs',
}),
metricStateOptions: {
titlePosition: 'bottom',
},
columnType: FORMULA_COLUMN,
formula: 'unique_count(monitor.check_group)',
format: 'number',
},
{
id: 'monitor_complete',
label: i18n.translate('xpack.observability.expView.complete', {
defaultMessage: 'Complete',
}),
metricStateOptions: {
titlePosition: 'bottom',
},
columnType: FORMULA_COLUMN,
formula: 'unique_count(monitor.check_group, kql=\'monitor.status: "up"\')',
format: 'number',
},
{
id: 'monitor_errors',
label: i18n.translate('xpack.observability.expView.errors', {
@ -104,7 +128,7 @@ export function getSyntheticsSingleMetricConfig({ dataView }: ConfigProps): Seri
palette: getColorPalette('danger'),
},
columnType: FORMULA_COLUMN,
formula: 'unique_count(state.id, kql=\'monitor.status: "down"\')',
formula: 'unique_count(monitor.check_group, kql=\'monitor.status: "down"\')',
format: 'number',
},
{

View file

@ -50,6 +50,7 @@ export interface ExploratoryEmbeddableProps {
showCalculationMethod?: boolean;
title?: string | JSX.Element;
withActions?: boolean | ActionTypes[];
align?: 'left' | 'right' | 'center';
}
export interface ExploratoryEmbeddableComponentProps extends ExploratoryEmbeddableProps {
@ -80,6 +81,7 @@ export default function Embeddable({
withActions = true,
lensFormulaHelper,
hideTicks,
align,
}: ExploratoryEmbeddableComponentProps) {
const LensComponent = lens?.EmbeddableComponent;
const LensSaveModalComponent = lens?.SaveModalComponent;
@ -168,7 +170,7 @@ export default function Embeddable({
}
return (
<Wrapper $customHeight={customHeight}>
<Wrapper $customHeight={customHeight} align={align}>
{(title || showCalculationMethod || appendTitle) && (
<EuiFlexGroup alignItems="center" gutterSize="none">
{title && (
@ -226,6 +228,7 @@ export default function Embeddable({
const Wrapper = styled.div<{
$customHeight?: string | number;
align?: 'left' | 'right' | 'center';
}>`
height: ${(props) => (props.$customHeight ? `${props.$customHeight};` : `100%;`)};
position: relative;
@ -239,6 +242,14 @@ const Wrapper = styled.div<{
}
.legacyMtrVis {
> :first-child {
justify-content: ${(props) =>
props.align === 'left'
? `flex-start;`
: props.align === 'right'
? `flex-end;`
: 'center;'};
}
justify-content: flex-end;
.legacyMtrVis__container {
padding: 0;

View file

@ -4,17 +4,34 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useCallback } from 'react';
import { EuiSpacer } from '@elastic/eui';
import { useUrlParams } from '../../../hooks';
import { useDimensions } from '../../../hooks';
import { SyntheticsDatePicker } from '../../common/date_picker/synthetics_date_picker';
import { AvailabilityPanel } from '../monitor_summary/availability_panel';
import { DurationPanel } from '../monitor_summary/duration_panel';
import { MonitorDurationTrend } from '../monitor_summary/duration_trend';
import { TestRunsTable } from '../monitor_summary/test_runs_table';
import { MonitorErrorsCount } from '../monitor_summary/monitor_errors_count';
import { MonitorCompleteCount } from '../monitor_summary/monitor_complete_count';
import { MonitorTotalRunsCount } from '../monitor_summary/monitor_total_runs_count';
import { MonitorErrorSparklines } from '../monitor_summary/monitor_error_sparklines';
import { AvailabilitySparklines } from '../monitor_summary/availability_sparklines';
import { DurationSparklines } from '../monitor_summary/duration_sparklines';
import { MonitorCompleteSparklines } from '../monitor_summary/monitor_complete_sparklines';
import { MonitorStatusPanel } from '../monitor_status/monitor_status_panel';
const STATS_WIDTH_SINGLE_COLUMN_THRESHOLD = 360; // ✨ determined by trial and error
export const MonitorHistory = () => {
const [useGetUrlParams, updateUrlParams] = useUrlParams();
const { dateRangeStart, dateRangeEnd } = useGetUrlParams();
const { elementRef: statsRef, width: statsWidth } = useDimensions<HTMLDivElement>();
const statsColumns = statsWidth && statsWidth < STATS_WIDTH_SINGLE_COLUMN_THRESHOLD ? 1 : 2;
const handleStatusChartBrushed = useCallback(
({ fromUtc, toUtc }) => {
updateUrlParams({ dateRangeStart: fromUtc, dateRangeEnd: toUtc });
@ -23,18 +40,96 @@ export const MonitorHistory = () => {
);
return (
<>
<SyntheticsDatePicker fullWidth={true} />
<EuiSpacer size="m" />
<MonitorStatusPanel
from={dateRangeStart}
to={dateRangeEnd}
showViewHistoryButton={false}
periodCaption={''}
brushable={true}
onBrushed={handleStatusChartBrushed}
/>
<EuiSpacer size="m" />
</>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<SyntheticsDatePicker fullWidth={true} />
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="m">
<EuiFlexItem grow={1}>
{/* @ts-expect-error Current @elastic/eui has the wrong types for the ref */}
<EuiPanel hasShadow={false} hasBorder={true} panelRef={statsRef}>
<EuiTitle size="xs">
<h3>{STATS_LABEL}</h3>
</EuiTitle>
<EuiFlexGrid columns={statsColumns} gutterSize="s" responsive={false}>
<EuiFlexItem>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>
<MonitorCompleteCount from={dateRangeStart} to={dateRangeEnd} />
</EuiFlexItem>
<EuiFlexItem>
<MonitorCompleteSparklines from={dateRangeStart} to={dateRangeEnd} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>
<AvailabilityPanel from={dateRangeStart} to={dateRangeEnd} />
</EuiFlexItem>
<EuiFlexItem>
<AvailabilitySparklines from={dateRangeStart} to={dateRangeEnd} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>
<MonitorErrorsCount from={dateRangeStart} to={dateRangeEnd} />
</EuiFlexItem>
<EuiFlexItem>
<MonitorErrorSparklines from={dateRangeStart} to={dateRangeEnd} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>
<DurationPanel from={dateRangeStart} to={dateRangeEnd} />
</EuiFlexItem>
<EuiFlexItem>
<DurationSparklines from={dateRangeStart} to={dateRangeEnd} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<MonitorTotalRunsCount from={dateRangeStart} to={dateRangeEnd} />
</EuiFlexItem>
</EuiFlexGrid>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={2}>
<EuiPanel hasShadow={false} hasBorder={true}>
<EuiTitle size="xs">
<h3>{DURATION_TREND_LABEL}</h3>
</EuiTitle>
<MonitorDurationTrend from={dateRangeStart} to={dateRangeEnd} />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<MonitorStatusPanel
from={dateRangeStart}
to={dateRangeEnd}
showViewHistoryButton={false}
periodCaption={''}
brushable={true}
onBrushed={handleStatusChartBrushed}
/>
</EuiFlexItem>
<EuiFlexItem>
<TestRunsTable from={dateRangeStart} to={dateRangeEnd} />
</EuiFlexItem>
</EuiFlexGroup>
);
};
const STATS_LABEL = i18n.translate('xpack.synthetics.historyPanel.stats', {
defaultMessage: 'Stats',
});
const DURATION_TREND_LABEL = i18n.translate('xpack.synthetics.historyPanel.durationTrends', {
defaultMessage: 'Duration trends',
});

View file

@ -33,6 +33,7 @@ export const AvailabilityPanel = (props: AvailabilityPanelprops) => {
return (
<ExploratoryViewEmbeddable
align="left"
customHeight="70px"
reportType={ReportTypes.SINGLE_METRIC}
attributes={[

View file

@ -33,6 +33,7 @@ export const DurationPanel = (props: DurationPanelProps) => {
return (
<ExploratoryViewEmbeddable
align="left"
customHeight="70px"
reportType={ReportTypes.SINGLE_METRIC}
attributes={[

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 { useKibana } from '@kbn/kibana-react-plugin/public';
import React from 'react';
import { ReportTypes } from '@kbn/observability-plugin/public';
import { ClientPluginsStart } from '../../../../../plugin';
import { useMonitorQueryId } from '../hooks/use_monitor_query_id';
interface MonitorCompleteCountProps {
from: string;
to: string;
}
export const MonitorCompleteCount = (props: MonitorCompleteCountProps) => {
const { observability } = useKibana<ClientPluginsStart>().services;
const { ExploratoryViewEmbeddable } = observability;
const monitorId = useMonitorQueryId();
return (
<ExploratoryViewEmbeddable
align="left"
reportType={ReportTypes.SINGLE_METRIC}
attributes={[
{
time: props,
reportDefinitions: { config_id: [monitorId] },
dataType: 'synthetics',
selectedMetricField: 'monitor_complete',
name: 'synthetics-series-1',
},
]}
/>
);
};

View file

@ -0,0 +1,47 @@
/*
* 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 { useKibana } from '@kbn/kibana-react-plugin/public';
import React from 'react';
import { useEuiTheme } from '@elastic/eui';
import { ClientPluginsStart } from '../../../../../plugin';
import { useMonitorQueryId } from '../hooks/use_monitor_query_id';
interface Props {
from: string;
to: string;
}
export const MonitorCompleteSparklines = (props: Props) => {
const { observability } = useKibana<ClientPluginsStart>().services;
const { ExploratoryViewEmbeddable } = observability;
const monitorId = useMonitorQueryId();
const { euiTheme } = useEuiTheme();
return (
<ExploratoryViewEmbeddable
reportType="kpi-over-time"
axisTitlesVisibility={{ x: false, yRight: false, yLeft: false }}
legendIsVisible={false}
hideTicks={true}
attributes={[
{
seriesType: 'area',
time: props,
reportDefinitions: { 'monitor.id': [monitorId] },
dataType: 'synthetics',
selectedMetricField: 'state.id',
name: 'Monitor complete',
color: euiTheme.colors.success,
operationType: 'unique_count',
},
]}
/>
);
};

View file

@ -46,7 +46,7 @@ export const MonitorErrorSparklines = (props: Props) => {
'observer.geo.name': [selectedLocation?.label],
},
dataType: 'synthetics',
selectedMetricField: 'state.id',
selectedMetricField: 'state.up',
name: 'Monitor errors',
color: euiTheme.colors.danger,
operationType: 'unique_count',

View file

@ -32,6 +32,7 @@ export const MonitorErrorsCount = (props: MonitorErrorsCountProps) => {
return (
<ExploratoryViewEmbeddable
align="left"
customHeight="70px"
reportType={ReportTypes.SINGLE_METRIC}
attributes={[

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 { useKibana } from '@kbn/kibana-react-plugin/public';
import React from 'react';
import { ReportTypes } from '@kbn/observability-plugin/public';
import { ClientPluginsStart } from '../../../../../plugin';
import { useMonitorQueryId } from '../hooks/use_monitor_query_id';
interface MonitorTotalRunsCountProps {
from: string;
to: string;
}
export const MonitorTotalRunsCount = (props: MonitorTotalRunsCountProps) => {
const { observability } = useKibana<ClientPluginsStart>().services;
const { ExploratoryViewEmbeddable } = observability;
const monitorId = useMonitorQueryId();
return (
<ExploratoryViewEmbeddable
align="left"
reportType={ReportTypes.SINGLE_METRIC}
attributes={[
{
time: props,
reportDefinitions: { config_id: [monitorId] },
dataType: 'synthetics',
selectedMetricField: 'monitor_total_runs',
name: 'synthetics-series-1',
},
]}
/>
);
};

View file

@ -15,3 +15,4 @@ export * from './use_last_50_duration_chart';
export * from './use_location_name';
export * from './use_status_by_location';
export * from './use_composite_image';
export * from './use_dimensions';

View file

@ -0,0 +1,51 @@
/*
* 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 { useEffect, useRef, useState } from 'react';
/** Returns a `ref` to attach to a DOM element, and its dimensions. */
export function useDimensions<T extends Element>() {
const [dimensions, setDimensions] = useState<{ width: number; height: number } | undefined>();
const elementRef = useRef<T>();
const resizeObserverInstance = useRef<ResizeObserver>(
new ResizeObserver((entries) => {
if (entries && entries[0]) {
setDimensions({
width: entries[0].contentRect.width,
height: entries[0].contentRect.height,
});
}
})
);
useEffect(() => {
// This makes ESLint happy. The cleanup function cannot point to the
// `.current` property of a ref.
const ref = elementRef.current;
const resizeObserver = resizeObserverInstance.current;
if (ref) {
resizeObserver.observe(ref);
}
return () => {
if (ref) {
resizeObserver.unobserve(ref);
}
};
// ESlint complains about this dependencies not triggering `useEffect`
// because they are mutable. This is not a problem for our case. We don't
// care if the attached DOM node mutates. We only want to know when the node
// gets attached to the ref.
//
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [elementRef.current, resizeObserverInstance.current]);
return { elementRef, ...dimensions };
}