mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[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:
parent
153b7e135c
commit
809246721d
7 changed files with 126 additions and 47 deletions
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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`;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue