Skip to content

Commit

Permalink
Exposed common EuiExpressions to separate components be able to reuse…
Browse files Browse the repository at this point in the history
… for building new for Alert Types (elastic#56466)

* Exposed common Expression to separate components be able to reuse

* Expressions with unit tests

* Fixed type check

* Fixed merge issues

* Fixed due to review

* Cleaned up some not used params and added position popover definition

* fixed type check

* Unbinded alerting reusable components from application context

* Added consumer and alertTypeId with enable change alert type button props

* Fixed case for default alert type id was set

* Fixed chart visualization issues

* Exposed registry in triggers and actions ui

* Fixed alert_list to enable charts

* Fixed due to comments and simplified props
  • Loading branch information
YulNaumenko authored and Maja Grubic committed Feb 10, 2020
1 parent 57efc47 commit 4084600
Show file tree
Hide file tree
Showing 43 changed files with 1,949 additions and 792 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { Alert, AlertTypeModel, ValidationResult } from '../../../../types';
import { IndexThresholdAlertTypeExpression, aggregationTypes, groupByTypes } from './expression';
import { AlertTypeModel, ValidationResult } from '../../../../types';
import { IndexThresholdAlertTypeExpression } from './expression';
import { IndexThresholdAlertParams } from './types';
import { builtInGroupByTypes, builtInAggregationTypes } from '../../../../common/constants';

export function getAlertType(): AlertTypeModel {
return {
id: 'threshold',
name: 'Index Threshold',
iconClass: 'alert',
alertParamsExpression: IndexThresholdAlertTypeExpression,
validate: (alert: Alert): ValidationResult => {
validate: (alertParams: IndexThresholdAlertParams): ValidationResult => {
const {
index,
timeField,
Expand All @@ -24,7 +26,7 @@ export function getAlertType(): AlertTypeModel {
termField,
threshold,
timeWindowSize,
} = alert.params;
} = alertParams;
const validationResult = { errors: {} };
const errors = {
aggField: new Array<string>(),
Expand All @@ -51,7 +53,7 @@ export function getAlertType(): AlertTypeModel {
})
);
}
if (aggType && aggregationTypes[aggType].fieldRequired && !aggField) {
if (aggType && builtInAggregationTypes[aggType].fieldRequired && !aggField) {
errors.aggField.push(
i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredAggFieldText', {
defaultMessage: 'Aggregation field is required.',
Expand All @@ -65,7 +67,7 @@ export function getAlertType(): AlertTypeModel {
})
);
}
if (groupBy && groupByTypes[groupBy].sizeRequired && !termField) {
if (!termField && groupBy && builtInGroupByTypes[groupBy].sizeRequired) {
errors.termField.push(
i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredtTermFieldText', {
defaultMessage: 'Term field is required.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ function isValidMoment(m) {
* @param {state} object - one of ""
* @param {[type]} display [description]
*/
function TimeBuckets(uiSettings, dataPlugin) {
function TimeBuckets(uiSettings, dataFieldsFormats) {
this.uiSettings = uiSettings;
this.dataPlugin = dataPlugin;
this.dataFieldsFormats = dataFieldsFormats;
return TimeBuckets.__cached__(this);
}

Expand Down Expand Up @@ -220,14 +220,14 @@ TimeBuckets.prototype.getInterval = function(useNormalizedEsInterval = true) {
function readInterval() {
const interval = self._i;
if (moment.isDuration(interval)) return interval;
return calcAutoIntervalNear(this.uiSettings.get('histogram:barTarget'), Number(duration));
return calcAutoIntervalNear(self.uiSettings.get('histogram:barTarget'), Number(duration));
}

// check to see if the interval should be scaled, and scale it if so
function maybeScaleInterval(interval) {
if (!self.hasBounds()) return interval;

const maxLength = this.uiSettings.get('histogram:maxBars');
const maxLength = self.uiSettings.get('histogram:maxBars');
const approxLen = duration / interval;
let scaled;

Expand Down Expand Up @@ -294,7 +294,7 @@ TimeBuckets.prototype.getScaledDateFormat = function() {
};

TimeBuckets.prototype.getScaledDateFormatter = function() {
const fieldFormatsService = this.dataPlugin.fieldFormats;
const fieldFormatsService = this.dataFieldsFormats;
const DateFieldFormat = fieldFormatsService.getType(fieldFormats.FIELD_FORMAT_IDS.DATE);

return new DateFieldFormat(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,17 @@ export interface GroupByType {
value: string;
validNormalizedTypes: string[];
}

export interface IndexThresholdAlertParams {
index: string[];
timeField?: string;
aggType: string;
aggField?: string;
groupBy?: string;
termSize?: number;
termField?: string;
thresholdComparator?: string;
threshold: number[];
timeWindowSize: number;
timeWindowUnit: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@ import dateMath from '@elastic/datemath';
import moment from 'moment-timezone';
import { EuiCallOut, EuiLoadingChart, EuiSpacer, EuiEmptyPrompt, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { getThresholdAlertVisualizationData } from './lib/api';
import { AggregationType, Comparator } from '../../../../common/types';
/* TODO: This file was copied from ui/time_buckets for NP migration. We should clean this up and add TS support */
import { TimeBuckets } from './lib/time_buckets';
import { getThresholdAlertVisualizationData } from './lib/api';
import { comparators, aggregationTypes } from './expression';
import { useAppDependencies } from '../../../app_context';
import { Alert } from '../../../../types';
import { DataPublicPluginStart } from '../../../../../../../../src/plugins/data/public';
import { AlertsContextValue } from '../../../context/alerts_context';
import { IndexThresholdAlertParams } from './types';

const customTheme = () => {
return {
Expand Down Expand Up @@ -77,40 +76,35 @@ const getDomain = (alertParams: any) => {
};
};

const getThreshold = (alertParams: any) => {
return alertParams.threshold.slice(
0,
comparators[alertParams.thresholdComparator].requiredValues
);
};

const getTimeBuckets = (
uiSettings: IUiSettingsClient,
dataPlugin: DataPublicPluginStart,
dataFieldsFormats: any,
alertParams: any
) => {
const domain = getDomain(alertParams);
const timeBuckets = new TimeBuckets(uiSettings, dataPlugin);
const timeBuckets = new TimeBuckets(uiSettings, dataFieldsFormats);
timeBuckets.setBounds(domain);
return timeBuckets;
};

interface Props {
alert: Alert;
alertParams: IndexThresholdAlertParams;
aggregationTypes: { [key: string]: AggregationType };
comparators: {
[key: string]: Comparator;
};
alertsContext: AlertsContextValue;
}

export const ThresholdVisualization: React.FunctionComponent<Props> = ({ alert }) => {
const { http, uiSettings, toastNotifications, charts, dataPlugin } = useAppDependencies();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<undefined | any>(undefined);
const [visualizationData, setVisualizationData] = useState<Record<string, any>>([]);

const chartsTheme = charts.theme.useChartsTheme();
export const ThresholdVisualization: React.FunctionComponent<Props> = ({
alertParams,
aggregationTypes,
comparators,
alertsContext,
}) => {
const {
index,
timeField,
triggerIntervalSize,
triggerIntervalUnit,
aggType,
aggField,
termSize,
Expand All @@ -120,21 +114,12 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({ alert }
timeWindowUnit,
groupBy,
threshold,
} = alert.params;
} = alertParams;
const { http, toastNotifications, charts, uiSettings, dataFieldsFormats } = alertsContext;

const domain = getDomain(alert.params);
const timeBuckets = new TimeBuckets(uiSettings, dataPlugin);
timeBuckets.setBounds(domain);
const interval = timeBuckets.getInterval().expression;
const visualizeOptions = {
rangeFrom: domain.min,
rangeTo: domain.max,
interval,
timezone: getTimezone(uiSettings),
};

// Fetching visualization data is independent of alert actions
const alertWithoutActions = { ...alert.params, actions: [], type: 'threshold' };
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<undefined | any>(undefined);
const [visualizationData, setVisualizationData] = useState<Record<string, any>>([]);

useEffect(() => {
(async () => {
Expand All @@ -148,12 +133,14 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({ alert }
})
);
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate(
'xpack.triggersActionsUI.sections.alertAdd.unableToLoadVisualizationMessage',
{ defaultMessage: 'Unable to load visualization' }
),
});
if (toastNotifications) {
toastNotifications.addDanger({
title: i18n.translate(
'xpack.triggersActionsUI.sections.alertAdd.unableToLoadVisualizationMessage',
{ defaultMessage: 'Unable to load visualization' }
),
});
}
setError(e);
} finally {
setIsLoading(false);
Expand All @@ -163,8 +150,6 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({ alert }
}, [
index,
timeField,
triggerIntervalSize,
triggerIntervalUnit,
aggType,
aggField,
termSize,
Expand All @@ -177,6 +162,25 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({ alert }
]);
/* eslint-enable react-hooks/exhaustive-deps */

if (!charts || !uiSettings || !dataFieldsFormats) {
return null;
}
const chartsTheme = charts.theme.useChartsTheme();

const domain = getDomain(alertParams);
const timeBuckets = new TimeBuckets(uiSettings, dataFieldsFormats);
timeBuckets.setBounds(domain);
const interval = timeBuckets.getInterval().expression;
const visualizeOptions = {
rangeFrom: domain.min,
rangeTo: domain.max,
interval,
timezone: getTimezone(uiSettings),
};

// Fetching visualization data is independent of alert actions
const alertWithoutActions = { ...alertParams, actions: [], type: 'threshold' };

if (isLoading) {
return (
<EuiEmptyPrompt
Expand Down Expand Up @@ -215,11 +219,17 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({ alert }
);
}

const getThreshold = () => {
return thresholdComparator
? threshold.slice(0, comparators[thresholdComparator].requiredValues)
: [];
};

if (visualizationData) {
const alertVisualizationDataKeys = Object.keys(visualizationData);
const timezone = getTimezone(uiSettings);
const actualThreshold = getThreshold(alert.params);
let maxY = actualThreshold[actualThreshold.length - 1];
const actualThreshold = getThreshold();
let maxY = actualThreshold[actualThreshold.length - 1] as any;

(Object.values(visualizationData) as number[][][]).forEach(data => {
data.forEach(([, y]) => {
Expand All @@ -231,7 +241,7 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({ alert }
const dateFormatter = (d: number) => {
return moment(d)
.tz(timezone)
.format(getTimeBuckets(uiSettings, dataPlugin, alert.params).getScaledDateFormat());
.format(getTimeBuckets(uiSettings, dataFieldsFormats, alertParams).getScaledDateFormat());
};
const aggLabel = aggregationTypes[aggType].text;
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,26 @@
*/

import React, { useContext, createContext } from 'react';
import { HttpSetup, IUiSettingsClient, ToastsApi } from 'kibana/public';
import { ChartsPluginSetup } from 'src/plugins/charts/public';
import { FieldFormatsRegistry } from 'src/plugins/data/common/field_formats/static';
import { TypeRegistry } from '../type_registry';
import { AlertTypeModel, ActionTypeModel } from '../../types';

export interface AlertsContextValue {
addFlyoutVisible: boolean;
setAddFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
reloadAlerts: () => Promise<void>;
reloadAlerts?: () => Promise<void>;
http: HttpSetup;
alertTypeRegistry: TypeRegistry<AlertTypeModel>;
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
uiSettings?: IUiSettingsClient;
toastNotifications?: Pick<
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
charts?: ChartsPluginSetup;
dataFieldsFormats?: Pick<FieldFormatsRegistry, 'register'>;
}

const AlertsContext = createContext<AlertsContextValue>(null as any);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,26 @@ import { coreMock } from '../../../../../../../src/core/public/mocks';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult, ActionConnector } from '../../../types';
import { ActionConnectorForm } from './action_connector_form';
import { AppContextProvider } from '../../app_context';
import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks';
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';

const actionTypeRegistry = actionTypeRegistryMock.create();

describe('action_connector_form', () => {
let deps: any;
beforeAll(async () => {
const mockes = coreMock.createSetup();
const mocks = coreMock.createSetup();
const [
{
chrome,
docLinks,
application: { capabilities },
},
] = await mockes.getStartServices();
] = await mocks.getStartServices();
deps = {
chrome,
docLinks,
dataPlugin: dataPluginMock.createStartContract(),
charts: chartPluginMock.createStartContract(),
toastNotifications: mockes.notifications.toasts,
injectedMetadata: mockes.injectedMetadata,
http: mockes.http,
uiSettings: mockes.uiSettings,
toastNotifications: mocks.notifications.toasts,
injectedMetadata: mocks.injectedMetadata,
http: mocks.http,
uiSettings: mocks.uiSettings,
capabilities: {
...capabilities,
actions: {
Expand All @@ -43,7 +37,9 @@ describe('action_connector_form', () => {
show: true,
},
},
setBreadcrumbs: jest.fn(),
legacy: {
MANAGEMENT_BREADCRUMB: { set: () => {} } as any,
},
actionTypeRegistry: actionTypeRegistry as any,
alertTypeRegistry: {} as any,
};
Expand Down Expand Up @@ -72,19 +68,21 @@ describe('action_connector_form', () => {
config: {},
secrets: {},
} as ActionConnector;
const wrapper = mountWithIntl(
<AppContextProvider appDeps={deps}>
let wrapper;
if (deps) {
wrapper = mountWithIntl(
<ActionConnectorForm
actionTypeName={'my-action-type-name'}
connector={initialConnector}
dispatch={() => {}}
serverError={null}
errors={{ name: [] }}
actionTypeRegistry={deps.actionTypeRegistry}
/>
</AppContextProvider>
);
const connectorNameField = wrapper.find('[data-test-subj="nameInput"]');
expect(connectorNameField.exists()).toBeTruthy();
expect(connectorNameField.first().prop('value')).toBe('');
);
}
const connectorNameField = wrapper?.find('[data-test-subj="nameInput"]');
expect(connectorNameField?.exists()).toBeTruthy();
expect(connectorNameField?.first().prop('value')).toBe('');
});
});
Loading

0 comments on commit 4084600

Please sign in to comment.