Skip to content

Commit

Permalink
[7.x] [Metrics UI] Prefill alerts from the global dropdown (elastic#6…
Browse files Browse the repository at this point in the history
…8967) (elastic#69981)

* [Metrics UI] Prefill alerts from the global dropdown (elastic#68967)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

* Fix uncaught typecheck merge conflict

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
Zacqary and elasticmachine committed Jun 25, 2020
1 parent b408a9f commit 35fde53
Show file tree
Hide file tree
Showing 18 changed files with 539 additions and 135 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import React, { useState, useCallback, useMemo } from 'react';
import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill';
import { AlertFlyout } from './alert_flyout';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';

Expand All @@ -15,6 +16,9 @@ export const InventoryAlertDropdown = () => {
const [flyoutVisible, setFlyoutVisible] = useState(false);
const kibana = useKibana();

const { inventoryPrefill } = useAlertPrefillContext();
const { nodeType, metric, filterQuery } = inventoryPrefill;

const closePopover = useCallback(() => {
setPopoverOpen(false);
}, [setPopoverOpen]);
Expand Down Expand Up @@ -57,7 +61,13 @@ export const InventoryAlertDropdown = () => {
>
<EuiContextMenuPanel items={menuItems} />
</EuiPopover>
<AlertFlyout setVisible={setFlyoutVisible} visible={flyoutVisible} />
<AlertFlyout
setVisible={setFlyoutVisible}
visible={flyoutVisible}
nodeType={nodeType}
options={{ metric }}
filter={filterQuery}
/>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { useState } from 'react';
import { SnapshotMetricInput } from '../../../../common/http_api/snapshot_api';
import { InventoryItemType } from '../../../../common/inventory_models/types';

export const useInventoryAlertPrefill = () => {
const [nodeType, setNodeType] = useState<InventoryItemType>('host');
const [filterQuery, setFilterQuery] = useState<string | undefined>();
const [metric, setMetric] = useState<SnapshotMetricInput>({ type: 'cpu' });

return {
nodeType,
filterQuery,
metric,
setNodeType,
setFilterQuery,
setMetric,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@
import React, { useState, useCallback, useMemo } from 'react';
import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { AlertFlyout } from './alert_flyout';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { useAlertPrefillContext } from '../../use_alert_prefill';
import { AlertFlyout } from './alert_flyout';

export const MetricsAlertDropdown = () => {
const [popoverOpen, setPopoverOpen] = useState(false);
const [flyoutVisible, setFlyoutVisible] = useState(false);
const kibana = useKibana();

const { metricThresholdPrefill } = useAlertPrefillContext();
const { groupBy, filterQuery, metrics } = metricThresholdPrefill;

const closePopover = useCallback(() => {
setPopoverOpen(false);
}, [setPopoverOpen]);
Expand Down Expand Up @@ -57,7 +61,11 @@ export const MetricsAlertDropdown = () => {
>
<EuiContextMenuPanel items={menuItems} />
</EuiPopover>
<AlertFlyout setVisible={setFlyoutVisible} visible={flyoutVisible} />
<AlertFlyout
setVisible={setFlyoutVisible}
visible={flyoutVisible}
options={{ groupBy, filterQuery, metrics }}
/>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
import { actionTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/action_type_registry.mock';
import { alertTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/alert_type_registry.mock';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context';
import { AlertContextMeta } from '../types';
import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer';
import React from 'react';
import { Expressions } from './expression';
import { act } from 'react-dom/test-utils';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { Comparator } from '../../../../server/lib/alerting/metric_threshold/types';

jest.mock('../../../containers/source/use_source_via_http', () => ({
useSourceViaHttp: () => ({
source: { id: 'default' },
createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }),
}),
}));

describe('Expression', () => {
async function setup(currentOptions: {
metrics?: MetricsExplorerMetric[];
filterQuery?: string;
groupBy?: string;
}) {
const alertParams = {
criteria: [],
groupBy: undefined,
filterQueryText: '',
};

const mocks = coreMock.createSetup();
const startMocks = coreMock.createStart();
const [
{
application: { capabilities },
},
] = await mocks.getStartServices();

const context: AlertsContextValue<AlertContextMeta> = {
http: mocks.http,
toastNotifications: mocks.notifications.toasts,
actionTypeRegistry: actionTypeRegistryMock.create() as any,
alertTypeRegistry: alertTypeRegistryMock.create() as any,
docLinks: startMocks.docLinks,
capabilities: {
...capabilities,
actions: {
delete: true,
save: true,
show: true,
},
},
metadata: {
currentOptions,
},
};

const wrapper = mountWithIntl(
<Expressions
alertsContext={context}
alertInterval="1m"
alertParams={alertParams}
errors={[]}
setAlertParams={(key, value) => Reflect.set(alertParams, key, value)}
setAlertProperty={() => {}}
/>
);

const update = async () =>
await act(async () => {
await nextTick();
wrapper.update();
});

await update();

return { wrapper, update, alertParams };
}

it('should prefill the alert using the context metadata', async () => {
const currentOptions = {
groupBy: 'host.hostname',
filterQuery: 'foo',
metrics: [
{ aggregation: 'avg', field: 'system.load.1' },
{ aggregation: 'cardinality', field: 'system.cpu.user.pct' },
] as MetricsExplorerMetric[],
};
const { alertParams } = await setup(currentOptions);
expect(alertParams.groupBy).toBe('host.hostname');
expect(alertParams.filterQueryText).toBe('foo');
expect(alertParams.criteria).toEqual([
{
metric: 'system.load.1',
comparator: Comparator.GT,
threshold: [],
timeSize: 1,
timeUnit: 'm',
aggType: 'avg',
},
{
metric: 'system.cpu.user.pct',
comparator: Comparator.GT,
threshold: [],
timeSize: 1,
timeUnit: 'm',
aggType: 'cardinality',
},
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { debounce, pick } from 'lodash';
import { debounce, pick, omit } from 'lodash';
import { Unit } from '@elastic/datemath';
import * as rt from 'io-ts';
import React, { ChangeEvent, useCallback, useMemo, useEffect, useState } from 'react';
Expand Down Expand Up @@ -52,22 +52,15 @@ import { useSourceViaHttp } from '../../../containers/source/use_source_via_http
import { convertKueryToElasticSearchQuery } from '../../../utils/kuery';

import { ExpressionRow } from './expression_row';
import { AlertContextMeta, TimeUnit, MetricExpression } from '../types';
import { AlertContextMeta, TimeUnit, MetricExpression, AlertParams } from '../types';
import { ExpressionChart } from './expression_chart';
import { validateMetricThreshold } from './validation';

const FILTER_TYPING_DEBOUNCE_MS = 500;

interface Props {
errors: IErrorObject[];
alertParams: {
criteria: MetricExpression[];
groupBy?: string;
filterQuery?: string;
sourceId?: string;
filterQueryText?: string;
alertOnNoData?: boolean;
};
alertParams: AlertParams;
alertsContext: AlertsContextValue<AlertContextMeta>;
alertInterval: string;
setAlertParams(key: string, value: any): void;
Expand All @@ -81,6 +74,7 @@ const defaultExpression = {
timeSize: 1,
timeUnit: 'm',
} as MetricExpression;
export { defaultExpression };

export const Expressions: React.FC<Props> = (props) => {
const { setAlertParams, alertParams, errors, alertsContext, alertInterval } = props;
Expand Down Expand Up @@ -247,6 +241,13 @@ export const Expressions: React.FC<Props> = (props) => {
}
}, [alertsContext.metadata, derivedIndexPattern, setAlertParams]);

const preFillAlertGroupBy = useCallback(() => {
const md = alertsContext.metadata;
if (md && md.currentOptions?.groupBy && !md.series) {
setAlertParams('groupBy', md.currentOptions.groupBy);
}
}, [alertsContext.metadata, setAlertParams]);

const onSelectPreviewLookbackInterval = useCallback((e) => {
setPreviewLookbackInterval(e.target.value);
setPreviewResult(null);
Expand Down Expand Up @@ -286,6 +287,10 @@ export const Expressions: React.FC<Props> = (props) => {
preFillAlertFilter();
}

if (!alertParams.groupBy) {
preFillAlertGroupBy();
}

if (!alertParams.sourceId) {
setAlertParams('sourceId', source?.id || 'default');
}
Expand Down Expand Up @@ -465,7 +470,7 @@ export const Expressions: React.FC<Props> = (props) => {
id="selectPreviewLookbackInterval"
value={previewLookbackInterval}
onChange={onSelectPreviewLookbackInterval}
options={previewOptions}
options={previewDOMOptions}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
Expand Down Expand Up @@ -588,6 +593,10 @@ export const Expressions: React.FC<Props> = (props) => {
);
};

const previewDOMOptions: Array<{ text: string; value: string }> = previewOptions.map((o) =>
omit(o, 'shortText')
);

// required for dynamic import
// eslint-disable-next-line import/no-default-export
export default Expressions;
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function validateMetricThreshold({
if (!c.aggType) {
errors[id].aggField.push(
i18n.translate('xpack.infra.metrics.alertFlyout.error.aggregationRequired', {
defaultMessage: 'Aggreation is required.',
defaultMessage: 'Aggregation is required.',
})
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { isEqual } from 'lodash';
import { useState } from 'react';
import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer';

interface MetricThresholdPrefillOptions {
groupBy: string | string[] | undefined;
filterQuery: string | undefined;
metrics: MetricsExplorerMetric[];
}

export const useMetricThresholdAlertPrefill = () => {
const [prefillOptionsState, setPrefillOptionsState] = useState<MetricThresholdPrefillOptions>({
groupBy: undefined,
filterQuery: undefined,
metrics: [],
});

const { groupBy, filterQuery, metrics } = prefillOptionsState;

return {
groupBy,
filterQuery,
metrics,
setPrefillOptions(newState: MetricThresholdPrefillOptions) {
if (!isEqual(newState, prefillOptionsState)) setPrefillOptionsState(newState);
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,12 @@ export interface ExpressionChartData {
id: string;
series: ExpressionChartSeries;
}

export interface AlertParams {
criteria: MetricExpression[];
groupBy?: string;
filterQuery?: string;
sourceId?: string;
filterQueryText?: string;
alertOnNoData?: boolean;
}
18 changes: 18 additions & 0 deletions x-pack/plugins/infra/public/alerting/use_alert_prefill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import createContainer from 'constate';
import { useMetricThresholdAlertPrefill } from './metric_threshold/hooks/use_metric_threshold_alert_prefill';
import { useInventoryAlertPrefill } from './inventory/hooks/use_inventory_alert_prefill';

const useAlertPrefill = () => {
const metricThresholdPrefill = useMetricThresholdAlertPrefill();
const inventoryPrefill = useInventoryAlertPrefill();

return { metricThresholdPrefill, inventoryPrefill };
};

export const [AlertPrefillProvider, useAlertPrefillContext] = createContainer(useAlertPrefill);
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class WithKueryAutocompletionComponent extends React.Component<
) => {
const { indexPattern } = this.props;
const language = 'kuery';
const hasQuerySuggestions = this.props.kibana.services.data.autocomplete.hasQuerySuggestions(
const hasQuerySuggestions = this.props.kibana.services.data?.autocomplete.hasQuerySuggestions(
language
);

Expand Down
Loading

0 comments on commit 35fde53

Please sign in to comment.