[ResponseOps] Change the duration/percentile display format to mm:ss (#124647)

* Change the duration/percentile display format to mm:ss

* Addressed comments

* Add time format to tooltip

* Addressed comments, percentiles can show N/A

* Fix flaky test

* remove only

* address comments, now tests for N/A

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jiawei Wu 2022-02-09 10:28:39 -07:00 committed by GitHub
parent 153b7e135c
commit 809246721d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 126 additions and 47 deletions

View file

@ -4,7 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getFormattedSuccessRatio, getFormattedRuleExecutionPercentile } from './monitoring_utils';
import {
getFormattedSuccessRatio,
getFormattedDuration,
getFormattedMilliseconds,
} from './monitoring_utils';
describe('monitoring_utils', () => {
it('should return a decimal as a percent', () => {
@ -12,9 +16,18 @@ describe('monitoring_utils', () => {
expect(getFormattedSuccessRatio(0.75345345345345)).toEqual('75%');
});
it('should return percentiles as an integer', () => {
expect(getFormattedRuleExecutionPercentile(0)).toEqual('0ms');
expect(getFormattedRuleExecutionPercentile(100.5555)).toEqual('101ms');
expect(getFormattedRuleExecutionPercentile(99.1111)).toEqual('99ms');
it('should return a formatted duration', () => {
expect(getFormattedDuration(0)).toEqual('00:00');
expect(getFormattedDuration(100.111)).toEqual('00:00');
expect(getFormattedDuration(50000)).toEqual('00:50');
expect(getFormattedDuration(500000)).toEqual('08:20');
expect(getFormattedDuration(5000000)).toEqual('83:20');
expect(getFormattedDuration(50000000)).toEqual('833:20');
});
it('should format a duration as an integer', () => {
expect(getFormattedMilliseconds(0)).toEqual('0 ms');
expect(getFormattedMilliseconds(100.5555)).toEqual('101 ms');
expect(getFormattedMilliseconds(99.1111)).toEqual('99 ms');
});
});

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import moment from 'moment';
import numeral from '@elastic/numeral';
export function getFormattedSuccessRatio(successRatio: number) {
@ -11,7 +12,17 @@ export function getFormattedSuccessRatio(successRatio: number) {
return `${formatted}%`;
}
export function getFormattedRuleExecutionPercentile(percentile: number) {
const formatted = numeral(percentile).format('0,0');
return `${formatted}ms`;
export function getFormattedDuration(value: number) {
if (!value) {
return '00:00';
}
const duration = moment.duration(value);
const minutes = Math.floor(duration.asMinutes()).toString().padStart(2, '0');
const seconds = duration.seconds().toString().padStart(2, '0');
return `${minutes}:${seconds}`;
}
export function getFormattedMilliseconds(value: number) {
const formatted = numeral(value).format('0,0');
return `${formatted} ms`;
}

View file

@ -18,6 +18,8 @@ import {
ALERTS_FEATURE_ID,
parseDuration,
} from '../../../../../../alerting/common';
import { getFormattedDuration, getFormattedMilliseconds } from '../../../lib/monitoring_utils';
import { useKibana } from '../../../../common/lib/kibana';
jest.mock('../../../../common/lib/kibana');
@ -180,22 +182,22 @@ describe('alerts_list component with items', () => {
history: [
{
success: true,
duration: 100,
duration: 1000000,
},
{
success: true,
duration: 200,
duration: 200000,
},
{
success: false,
duration: 300,
duration: 300000,
},
],
calculated_metrics: {
success_ratio: 0.66,
p50: 200,
p95: 300,
p99: 300,
p50: 200000,
p95: 300000,
p99: 300000,
},
},
},
@ -227,18 +229,18 @@ describe('alerts_list component with items', () => {
history: [
{
success: true,
duration: 100,
duration: 100000,
},
{
success: true,
duration: 500,
duration: 500000,
},
],
calculated_metrics: {
success_ratio: 1,
p50: 0,
p95: 100,
p99: 500,
p95: 100000,
p99: 500000,
},
},
},
@ -458,7 +460,7 @@ describe('alerts_list component with items', () => {
wrapper.update();
expect(wrapper.find('.euiToolTipPopover').text()).toBe(
'The length of time it took for the rule to run.'
'The length of time it took for the rule to run (mm:ss).'
);
// Status column
@ -508,14 +510,24 @@ describe('alerts_list component with items', () => {
).toBeTruthy();
let percentiles = wrapper.find(
`EuiTableRowCell[data-test-subj="alertsTableCell-ruleExecutionPercentile"] span[data-test-subj="${Percentiles.P50}Percentile"]`
`EuiTableRowCell[data-test-subj="alertsTableCell-ruleExecutionPercentile"] span[data-test-subj="rule-duration-format-value"]`
);
mockedAlertsData.forEach((rule, index) => {
if (typeof rule.monitoring?.execution.calculated_metrics.p50 === 'number') {
// Ensure the table cells are getting the correct values
expect(percentiles.at(index).text()).toEqual(
`${rule.monitoring.execution.calculated_metrics.p50}ms`
getFormattedDuration(rule.monitoring.execution.calculated_metrics.p50)
);
// Ensure the tooltip is showing the correct content
expect(
wrapper
.find(
'EuiTableRowCell[data-test-subj="alertsTableCell-ruleExecutionPercentile"] [data-test-subj="rule-duration-format-tooltip"]'
)
.at(index)
.props().content
).toEqual(getFormattedMilliseconds(rule.monitoring.execution.calculated_metrics.p50));
} else {
expect(percentiles.at(index).text()).toEqual('N/A');
}
@ -581,13 +593,13 @@ describe('alerts_list component with items', () => {
).toBeTruthy();
percentiles = wrapper.find(
`EuiTableRowCell[data-test-subj="alertsTableCell-ruleExecutionPercentile"] span[data-test-subj="${Percentiles.P95}Percentile"]`
`EuiTableRowCell[data-test-subj="alertsTableCell-ruleExecutionPercentile"] span[data-test-subj="rule-duration-format-value"]`
);
mockedAlertsData.forEach((rule, index) => {
if (typeof rule.monitoring?.execution.calculated_metrics.p95 === 'number') {
expect(percentiles.at(index).text()).toEqual(
`${rule.monitoring.execution.calculated_metrics.p95}ms`
getFormattedDuration(rule.monitoring.execution.calculated_metrics.p95)
);
} else {
expect(percentiles.at(index).text()).toEqual('N/A');

View file

@ -86,14 +86,9 @@ import { ManageLicenseModal } from './manage_license_modal';
import { checkAlertTypeEnabled } from '../../../lib/check_alert_type_enabled';
import { RuleEnabledSwitch } from './rule_enabled_switch';
import { PercentileSelectablePopover } from './percentile_selectable_popover';
import {
formatMillisForDisplay,
shouldShowDurationWarning,
} from '../../../lib/execution_duration_utils';
import {
getFormattedSuccessRatio,
getFormattedRuleExecutionPercentile,
} from '../../../lib/monitoring_utils';
import { RuleDurationFormat } from './rule_duration_format';
import { shouldShowDurationWarning } from '../../../lib/execution_duration_utils';
import { getFormattedSuccessRatio } from '../../../lib/monitoring_utils';
const ENTER_KEY = 13;
@ -396,7 +391,7 @@ export const AlertsList: React.FunctionComponent = () => {
content={i18n.translate(
'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.ruleExecutionPercentileTooltip',
{
defaultMessage: `{percentileOrdinal} percentile of this rule's past {sampleLimit} execution durations`,
defaultMessage: `{percentileOrdinal} percentile of this rule's past {sampleLimit} execution durations (mm:ss).`,
values: {
percentileOrdinal: percentileOrdinals[selectedPercentile!],
sampleLimit: MONITORING_HISTORY_LIMIT,
@ -420,7 +415,7 @@ export const AlertsList: React.FunctionComponent = () => {
const renderPercentileCellValue = (value: number) => {
return (
<span data-test-subj={`${selectedPercentile}Percentile`}>
{typeof value === 'number' ? getFormattedRuleExecutionPercentile(value) : 'N/A'}
<RuleDurationFormat allowZero={false} duration={value} />
</span>
);
};
@ -630,7 +625,7 @@ export const AlertsList: React.FunctionComponent = () => {
content={i18n.translate(
'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.durationTitle',
{
defaultMessage: 'The length of time it took for the rule to run.',
defaultMessage: 'The length of time it took for the rule to run (mm:ss).',
}
)}
>
@ -651,7 +646,7 @@ export const AlertsList: React.FunctionComponent = () => {
return (
<>
{`${formatMillisForDisplay(value)}`}
{<RuleDurationFormat duration={value} />}
{showDurationWarning && (
<EuiIconTip
data-test-subj="ruleDurationWarning"
@ -671,6 +666,7 @@ export const AlertsList: React.FunctionComponent = () => {
);
},
},
getPercentileColumn(),
{
field: 'monitoring.execution.calculated_metrics.success_ratio',
width: '12%',
@ -680,7 +676,7 @@ export const AlertsList: React.FunctionComponent = () => {
content={i18n.translate(
'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.successRatioTitle',
{
defaultMessage: 'How often this rule executes successfully',
defaultMessage: 'How often this rule executes successfully.',
}
)}
>
@ -701,7 +697,6 @@ export const AlertsList: React.FunctionComponent = () => {
);
},
},
getPercentileColumn(),
{
field: 'executionStatus.status',
name: i18n.translate(

View file

@ -0,0 +1,39 @@
/*
* 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, { memo, useMemo } from 'react';
import { EuiToolTip } from '@elastic/eui';
import { getFormattedDuration, getFormattedMilliseconds } from '../../../lib/monitoring_utils';
interface Props {
duration: number;
allowZero?: boolean;
}
export const RuleDurationFormat = memo((props: Props) => {
const { duration, allowZero = true } = props;
const formattedDuration = useMemo(() => {
if (allowZero || typeof duration === 'number') {
return getFormattedDuration(duration);
}
return 'N/A';
}, [duration, allowZero]);
const formattedTooltip = useMemo(() => {
if (allowZero || typeof duration === 'number') {
return getFormattedMilliseconds(duration);
}
return 'N/A';
}, [duration, allowZero]);
return (
<EuiToolTip data-test-subj="rule-duration-format-tooltip" content={formattedTooltip}>
<span data-test-subj="rule-duration-format-value">{formattedDuration}</span>
</EuiToolTip>
);
});

View file

@ -168,7 +168,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
tags: '',
interval: '1 min',
});
expect(searchResultAfterSave.duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/);
expect(searchResultAfterSave.duration).to.match(/\d{2,}:\d{2}/);
// clean up created alert
const alertsToDelete = await getAlertsByName(alertName);

View file

@ -81,7 +81,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
expect(searchResults[0].name).to.equal(`${createdAlert.name}Test: Noop`);
expect(searchResults[0].interval).to.equal('1 min');
expect(searchResults[0].tags).to.equal('2');
expect(searchResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/);
expect(searchResults[0].duration).to.match(/\d{2,}:\d{2}/);
});
it('should update alert list on the search clear button click', async () => {
@ -103,7 +103,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
expect(searchResults[0].name).to.equal('bTest: Noop');
expect(searchResults[0].interval).to.equal('1 min');
expect(searchResults[0].tags).to.equal('2');
expect(searchResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/);
expect(searchResults[0].duration).to.match(/\d{2,}:\d{2}/);
const searchClearButton = await find.byCssSelector('.euiFormControlLayoutClearButton');
await searchClearButton.click();
@ -115,11 +115,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
expect(searchResultsAfterClear[0].name).to.equal('bTest: Noop');
expect(searchResultsAfterClear[0].interval).to.equal('1 min');
expect(searchResultsAfterClear[0].tags).to.equal('2');
expect(searchResultsAfterClear[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/);
expect(searchResultsAfterClear[0].duration).to.match(/\d{2,}:\d{2}/);
expect(searchResultsAfterClear[1].name).to.equal('cTest: Noop');
expect(searchResultsAfterClear[1].interval).to.equal('1 min');
expect(searchResultsAfterClear[1].tags).to.equal('');
expect(searchResultsAfterClear[1].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/);
expect(searchResultsAfterClear[1].duration).to.match(/\d{2,}:\d{2}/);
});
it('should search for tags', async () => {
@ -136,7 +136,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
expect(searchResults[0].name).to.equal(`${createdAlert.name}Test: Noop`);
expect(searchResults[0].interval).to.equal('1 min');
expect(searchResults[0].tags).to.equal('3');
expect(searchResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/);
expect(searchResults[0].duration).to.match(/\d{2,}:\d{2}/);
});
it('should display an empty list when search did not return any alerts', async () => {
@ -369,11 +369,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.existOrFail('P50Percentile');
await retry.try(async () => {
const percentileCell = await find.byCssSelector(
'[data-test-subj="P50Percentile"]:nth-of-type(1)'
);
const percentileCellText = await percentileCell.getVisibleText();
expect(percentileCellText).to.match(/^N\/A|\d{2,}:\d{2}$/);
await testSubjects.click('percentileSelectablePopover-iconButton');
await testSubjects.existOrFail('percentileSelectablePopover-selectable');
const searchClearButton = await find.byCssSelector(
'[data-test-subj="percentileSelectablePopover-selectable"] li:nth-child(2)'
);
const alertResults = await pageObjects.triggersActionsUI.getAlertsList();
expect(alertResults[0].duration).to.match(/^N\/A|\d{2,}:\d{2}$/);
await searchClearButton.click();
await testSubjects.missingOrFail('percentileSelectablePopover-selectable');
await testSubjects.existOrFail('alertsTable-P95ColumnName');
@ -427,7 +436,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
expect(filterErrorOnlyResults[0].name).to.equal(`${failingAlert.name}Test: Failing`);
expect(filterErrorOnlyResults[0].interval).to.equal('30 sec');
expect(filterErrorOnlyResults[0].status).to.equal('Error');
expect(filterErrorOnlyResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/);
expect(filterErrorOnlyResults[0].duration).to.match(/\d{2,}:\d{2}/);
});
});
@ -440,7 +449,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
expect(refreshResults[0].name).to.equal(`${createdAlert.name}Test: Noop`);
expect(refreshResults[0].interval).to.equal('1 min');
expect(refreshResults[0].status).to.equal('Ok');
expect(refreshResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/);
expect(refreshResults[0].duration).to.match(/\d{2,}:\d{2}/);
});
const alertsErrorBannerWhenNoErrors = await find.allByCssSelector(
@ -484,7 +493,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
expect(filterFailingAlertOnlyResults.length).to.equal(1);
expect(filterFailingAlertOnlyResults[0].name).to.equal(`${failingAlert.name}Test: Failing`);
expect(filterFailingAlertOnlyResults[0].interval).to.equal('30 sec');
expect(filterFailingAlertOnlyResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/);
expect(filterFailingAlertOnlyResults[0].duration).to.match(/\d{2,}:\d{2}/);
});
});
@ -518,7 +527,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
`${noopAlertWithAction.name}Test: Noop`
);
expect(filterWithSlackOnlyResults[0].interval).to.equal('1 min');
expect(filterWithSlackOnlyResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/);
expect(filterWithSlackOnlyResults[0].duration).to.match(/\d{2,}:\d{2}/);
});
await testSubjects.click('alertTypeFilterButton');