Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exposed common EuiExpressions to separate components be able to reuse for building new for Alert Types #56466

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8c7ed23
Exposed common Expression to separate components be able to reuse
YulNaumenko Jan 31, 2020
e090eca
Merge remote-tracking branch 'upstream/master' into alerts-create-com…
YulNaumenko Feb 1, 2020
b54cdd6
Expressions with unit tests
YulNaumenko Feb 3, 2020
7c5031c
Fixed type check
YulNaumenko Feb 3, 2020
221194b
Merge remote-tracking branch 'upstream/master' into alerts-create-com…
YulNaumenko Feb 3, 2020
a110f9e
Merge remote-tracking branch 'upstream/master' into alerts-create-com…
YulNaumenko Feb 4, 2020
db7678b
Fixed merge issues
YulNaumenko Feb 4, 2020
293d985
Fixed due to review
YulNaumenko Feb 5, 2020
e4e9f4b
Cleaned up some not used params and added position popover definition
YulNaumenko Feb 5, 2020
f310132
Merge remote-tracking branch 'upstream/master' into alerts-create-com…
YulNaumenko Feb 5, 2020
73bc017
fixed type check
YulNaumenko Feb 6, 2020
1ad46d5
Unbinded alerting reusable components from application context
YulNaumenko Feb 6, 2020
2bd5bef
Added consumer and alertTypeId with enable change alert type button p…
YulNaumenko Feb 6, 2020
b8a18f4
Fixed case for default alert type id was set
YulNaumenko Feb 6, 2020
b9a1a56
Merge remote-tracking branch 'upstream/master' into alerts-create-com…
YulNaumenko Feb 6, 2020
2fcd805
Fixed chart visualization issues
YulNaumenko Feb 6, 2020
e414ef7
Exposed registry in triggers and actions ui
YulNaumenko Feb 7, 2020
f8438c9
Fixed alert_list to enable charts
YulNaumenko Feb 7, 2020
c7485d5
Fixed due to comments and simplified props
YulNaumenko Feb 7, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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