Skip to content

Commit 257adde

Browse files
Merge branch 'master' into mocks_cleanup
2 parents 058b291 + 5308cc7 commit 257adde

29 files changed

+882
-71
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { i18n } from '@kbn/i18n';
8-
import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api';
8+
import { SnapshotCustomMetricInput } from '../http_api/snapshot_api';
99

1010
export const getCustomMetricLabel = (metric: SnapshotCustomMetricInput) => {
1111
const METRIC_LABELS = {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { decimalToPct, pctToDecimal } from './corrected_percent_convert';
8+
9+
describe('decimalToPct', () => {
10+
test('should retain correct floating point precision up to 10 decimal places', () => {
11+
// Most of these cases would still work fine just doing x * 100 instead of passing it through
12+
// decimalToPct, but the function still needs to work regardless
13+
expect(decimalToPct(0)).toBe(0);
14+
expect(decimalToPct(0.1)).toBe(10);
15+
expect(decimalToPct(0.01)).toBe(1);
16+
expect(decimalToPct(0.014)).toBe(1.4);
17+
expect(decimalToPct(0.0141)).toBe(1.41);
18+
expect(decimalToPct(0.01414)).toBe(1.414);
19+
// This case is known to fail without decimalToPct; vanilla JS 0.014141 * 100 === 1.4141000000000001
20+
expect(decimalToPct(0.014141)).toBe(1.4141);
21+
expect(decimalToPct(0.0141414)).toBe(1.41414);
22+
expect(decimalToPct(0.01414141)).toBe(1.414141);
23+
expect(decimalToPct(0.014141414)).toBe(1.4141414);
24+
});
25+
test('should also work with values greater than 1', () => {
26+
expect(decimalToPct(2)).toBe(200);
27+
expect(decimalToPct(2.1)).toBe(210);
28+
expect(decimalToPct(2.14)).toBe(214);
29+
expect(decimalToPct(2.14141414)).toBe(214.141414);
30+
});
31+
});
32+
33+
describe('pctToDecimal', () => {
34+
test('should retain correct floating point precision up to 10 decimal places', () => {
35+
expect(pctToDecimal(0)).toBe(0);
36+
expect(pctToDecimal(10)).toBe(0.1);
37+
expect(pctToDecimal(1)).toBe(0.01);
38+
expect(pctToDecimal(1.4)).toBe(0.014);
39+
expect(pctToDecimal(1.41)).toBe(0.0141);
40+
expect(pctToDecimal(1.414)).toBe(0.01414);
41+
expect(pctToDecimal(1.4141)).toBe(0.014141);
42+
expect(pctToDecimal(1.41414)).toBe(0.0141414);
43+
expect(pctToDecimal(1.414141)).toBe(0.01414141);
44+
expect(pctToDecimal(1.4141414)).toBe(0.014141414);
45+
});
46+
test('should also work with values greater than 100%', () => {
47+
expect(pctToDecimal(200)).toBe(2);
48+
expect(pctToDecimal(210)).toBe(2.1);
49+
expect(pctToDecimal(214)).toBe(2.14);
50+
expect(pctToDecimal(214.141414)).toBe(2.14141414);
51+
});
52+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
const correctedPctConvert = (v: number, decimalToPct: boolean) => {
8+
// Correct floating point precision
9+
const replacementPattern = decimalToPct ? new RegExp(/0?\./) : '.';
10+
const numberOfDigits = String(v).replace(replacementPattern, '').length;
11+
const multipliedValue = decimalToPct ? v * 100 : v / 100;
12+
return parseFloat(multipliedValue.toPrecision(numberOfDigits));
13+
};
14+
15+
export const decimalToPct = (v: number) => correctedPctConvert(v, true);
16+
export const pctToDecimal = (v: number) => correctedPctConvert(v, false);

x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'
1212
import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types';
1313
import { InfraWaffleMapOptions } from '../../../lib/lib';
1414
import { InventoryItemType } from '../../../../common/inventory_models/types';
15+
import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill';
1516

1617
interface Props {
1718
visible?: boolean;
@@ -21,16 +22,24 @@ interface Props {
2122
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
2223
}
2324

24-
export const AlertFlyout = (props: Props) => {
25+
export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: Props) => {
2526
const { triggersActionsUI } = useContext(TriggerActionsContext);
2627
const { services } = useKibana();
2728

29+
const { inventoryPrefill } = useAlertPrefillContext();
30+
const { customMetrics } = inventoryPrefill;
31+
2832
return (
2933
<>
3034
{triggersActionsUI && (
3135
<AlertsContextProvider
3236
value={{
33-
metadata: { options: props.options, nodeType: props.nodeType, filter: props.filter },
37+
metadata: {
38+
options,
39+
nodeType,
40+
filter,
41+
customMetrics,
42+
},
3443
toastNotifications: services.notifications?.toasts,
3544
http: services.http,
3645
docLinks: services.docLinks,
@@ -40,8 +49,8 @@ export const AlertFlyout = (props: Props) => {
4049
}}
4150
>
4251
<AlertAdd
43-
addFlyoutVisible={props.visible!}
44-
setAddFlyoutVisibility={props.setVisible}
52+
addFlyoutVisible={visible!}
53+
setAddFlyoutVisibility={setVisible}
4554
alertTypeId={METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID}
4655
canChangeTrigger={false}
4756
consumer={'infrastructure'}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
8+
import { actionTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/action_type_registry.mock';
9+
import { alertTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/alert_type_registry.mock';
10+
import { coreMock } from '../../../../../../../src/core/public/mocks';
11+
import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context';
12+
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
13+
import { InventoryMetricConditions } from '../../../../server/lib/alerting/inventory_metric_threshold/types';
14+
import React from 'react';
15+
import { Expressions, AlertContextMeta, ExpressionRow } from './expression';
16+
import { act } from 'react-dom/test-utils';
17+
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
18+
import { Comparator } from '../../../../server/lib/alerting/metric_threshold/types';
19+
import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api';
20+
21+
jest.mock('../../../containers/source/use_source_via_http', () => ({
22+
useSourceViaHttp: () => ({
23+
source: { id: 'default' },
24+
createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }),
25+
}),
26+
}));
27+
28+
const exampleCustomMetric = {
29+
id: 'this-is-an-id',
30+
field: 'some.system.field',
31+
aggregation: 'rate',
32+
type: 'custom',
33+
} as SnapshotCustomMetricInput;
34+
35+
describe('Expression', () => {
36+
async function setup(currentOptions: AlertContextMeta) {
37+
const alertParams = {
38+
criteria: [],
39+
nodeType: undefined,
40+
filterQueryText: '',
41+
};
42+
43+
const mocks = coreMock.createSetup();
44+
const startMocks = coreMock.createStart();
45+
const [
46+
{
47+
application: { capabilities },
48+
},
49+
] = await mocks.getStartServices();
50+
51+
const context: AlertsContextValue<AlertContextMeta> = {
52+
http: mocks.http,
53+
toastNotifications: mocks.notifications.toasts,
54+
actionTypeRegistry: actionTypeRegistryMock.create() as any,
55+
alertTypeRegistry: alertTypeRegistryMock.create() as any,
56+
docLinks: startMocks.docLinks,
57+
capabilities: {
58+
...capabilities,
59+
actions: {
60+
delete: true,
61+
save: true,
62+
show: true,
63+
},
64+
},
65+
metadata: currentOptions,
66+
};
67+
68+
const wrapper = mountWithIntl(
69+
<Expressions
70+
alertsContext={context}
71+
alertInterval="1m"
72+
alertParams={alertParams as any}
73+
errors={[]}
74+
setAlertParams={(key, value) => Reflect.set(alertParams, key, value)}
75+
setAlertProperty={() => {}}
76+
/>
77+
);
78+
79+
const update = async () =>
80+
await act(async () => {
81+
await nextTick();
82+
wrapper.update();
83+
});
84+
85+
await update();
86+
87+
return { wrapper, update, alertParams };
88+
}
89+
90+
it('should prefill the alert using the context metadata', async () => {
91+
const currentOptions = {
92+
filter: 'foo',
93+
nodeType: 'pod',
94+
customMetrics: [],
95+
options: { metric: { type: 'memory' } },
96+
};
97+
const { alertParams } = await setup(currentOptions as AlertContextMeta);
98+
expect(alertParams.nodeType).toBe('pod');
99+
expect(alertParams.filterQueryText).toBe('foo');
100+
expect(alertParams.criteria).toEqual([
101+
{
102+
metric: 'memory',
103+
comparator: Comparator.GT,
104+
threshold: [],
105+
timeSize: 1,
106+
timeUnit: 'm',
107+
},
108+
]);
109+
});
110+
describe('using custom metrics', () => {
111+
it('should prefill the alert using the context metadata', async () => {
112+
const currentOptions = {
113+
filter: '',
114+
nodeType: 'tx',
115+
customMetrics: [exampleCustomMetric],
116+
options: { metric: exampleCustomMetric },
117+
};
118+
const { alertParams, update } = await setup(currentOptions as AlertContextMeta);
119+
await update();
120+
expect(alertParams.nodeType).toBe('tx');
121+
expect(alertParams.filterQueryText).toBe('');
122+
expect(alertParams.criteria).toEqual([
123+
{
124+
metric: 'custom',
125+
comparator: Comparator.GT,
126+
threshold: [],
127+
timeSize: 1,
128+
timeUnit: 'm',
129+
customMetric: exampleCustomMetric,
130+
},
131+
]);
132+
});
133+
});
134+
});
135+
136+
describe('ExpressionRow', () => {
137+
async function setup(expression: InventoryMetricConditions) {
138+
const wrapper = mountWithIntl(
139+
<ExpressionRow
140+
nodeType="host"
141+
canDelete={false}
142+
remove={() => {}}
143+
addExpression={() => {}}
144+
key={1}
145+
expressionId={1}
146+
setAlertParams={() => {}}
147+
errors={{
148+
aggField: [],
149+
timeSizeUnit: [],
150+
timeWindowSize: [],
151+
metric: [],
152+
}}
153+
expression={expression}
154+
alertsContextMetadata={{
155+
customMetrics: [],
156+
}}
157+
/>
158+
);
159+
160+
const update = async () =>
161+
await act(async () => {
162+
await nextTick();
163+
wrapper.update();
164+
});
165+
166+
await update();
167+
168+
return { wrapper, update };
169+
}
170+
const expression = {
171+
metric: 'custom',
172+
comparator: Comparator.GT,
173+
threshold: [],
174+
timeSize: 1,
175+
timeUnit: 'm',
176+
customMetric: exampleCustomMetric,
177+
};
178+
179+
it('loads custom metrics passed in through the expression, even with an empty context', async () => {
180+
const { wrapper } = await setup(expression as InventoryMetricConditions);
181+
const [valueMatch] =
182+
wrapper.html().match('<span class="euiExpression__value">Rate of some.system.field</span>') ??
183+
[];
184+
expect(valueMatch).toBeTruthy();
185+
});
186+
});

0 commit comments

Comments
 (0)