Skip to content

Commit

Permalink
[SIEM] Add toaster logic for Machine Learning (#41401)
Browse files Browse the repository at this point in the history
* Initial toaster logic

* Updated toaster error handling

* Cleanups of code not used

* Added i18n support

* Added missing i18n keys and fixed the name spaces of all of them

* Added unit test

* Added the rest of the unit tests

* Updated from feedback from the PR review

* Fix capitalization issue
  • Loading branch information
FrankHassanabad authored Jul 17, 2019
1 parent dcd2f57 commit 1f8c215
Show file tree
Hide file tree
Showing 18 changed files with 851 additions and 301 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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 { i18n } from '@kbn/i18n';

export const SIEM_TABLE_FETCH_FAILURE = i18n.translate(
'xpack.siem.components.ml.anomaly.errors.anomaliesTableFetchFailureTitle',
{
defaultMessage: 'Anomalies table fetch failure',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import {
import { hasMlUserPermissions } from '../permissions/has_ml_user_permissions';
import { MlCapabilitiesContext } from '../permissions/ml_capabilities_provider';
import { useSiemJobs } from '../../ml_popover/hooks/use_siem_jobs';
import { useStateToaster } from '../../toasters';
import { errorToToaster } from '../api/error_to_toaster';

import * as i18n from './translations';

interface Args {
influencers?: InfluencerInput[];
Expand Down Expand Up @@ -75,6 +79,7 @@ export const useAnomaliesTableData = ({
const config = useContext(KibanaConfigContext);
const capabilities = useContext(MlCapabilitiesContext);
const userPermissions = hasMlUserPermissions(capabilities);
const [, dispatchToaster] = useStateToaster();

const fetchFunc = async (
influencersInput: InfluencerInput[],
Expand All @@ -83,27 +88,34 @@ export const useAnomaliesTableData = ({
latestMs: number
) => {
if (userPermissions && !skip && siemJobs.length > 0) {
const data = await anomaliesTableData(
{
jobIds: siemJobs,
criteriaFields: criteriaFieldsInput,
aggregationInterval: 'auto',
threshold: getThreshold(config, threshold),
earliestMs,
latestMs,
influencers: influencersInput,
dateFormatTz: getTimeZone(config),
maxRecords: 500,
maxExamples: 10,
},
{
'kbn-version': config.kbnVersion,
}
);
setTableData(data);
setLoading(false);
try {
const data = await anomaliesTableData(
{
jobIds: siemJobs,
criteriaFields: criteriaFieldsInput,
aggregationInterval: 'auto',
threshold: getThreshold(config, threshold),
earliestMs,
latestMs,
influencers: influencersInput,
dateFormatTz: getTimeZone(config),
maxRecords: 500,
maxExamples: 10,
},
{
'kbn-version': config.kbnVersion,
}
);
setTableData(data);
setLoading(false);
} catch (error) {
errorToToaster({ title: i18n.SIEM_TABLE_FETCH_FAILURE, error, dispatchToaster });
setLoading(false);
}
} else if (!userPermissions) {
setLoading(false);
} else if (siemJobs.length === 0) {
setLoading(false);
} else {
setTableData(null);
setLoading(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,21 @@ export interface Body {
maxExamples: number;
}

const empty: Anomalies = {
anomalies: [],
interval: 'second',
};

export const anomaliesTableData = async (
body: Body,
headers: Record<string, string | undefined>
): Promise<Anomalies> => {
try {
const response = await fetch(`${chrome.getBasePath()}/api/ml/results/anomalies_table_data`, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify(body),
headers: {
'kbn-system-api': 'true',
'content-Type': 'application/json',
'kbn-xsrf': chrome.getXsrfToken(),
...headers,
},
});
await throwIfNotOk(response);
return await response.json();
} catch (error) {
// TODO: Toaster error when this happens instead of returning empty data
return empty;
}
const response = await fetch(`${chrome.getBasePath()}/api/ml/results/anomalies_table_data`, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify(body),
headers: {
'kbn-system-api': 'true',
'content-Type': 'application/json',
'kbn-xsrf': chrome.getXsrfToken(),
...headers,
},
});
await throwIfNotOk(response);
return await response.json();
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* 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 { isAnError, isToasterError, errorToToaster } from './error_to_toaster';
import { ToasterErrors } from './throw_if_not_ok';

describe('error_to_toaster', () => {
let dispatchToaster = jest.fn();

beforeEach(() => {
dispatchToaster = jest.fn();
});

describe('#errorToToaster', () => {
test('adds a ToastError given multiple toaster errors', () => {
const error = new ToasterErrors(['some error 1', 'some error 2']);
errorToToaster({ id: 'some-made-up-id', title: 'some title', error, dispatchToaster });
expect(dispatchToaster.mock.calls[0]).toEqual([
{
toast: {
color: 'danger',
errors: ['some error 1', 'some error 2'],
iconType: 'alert',
id: 'some-made-up-id',
title: 'some title',
},
type: 'addToaster',
},
]);
});

test('adds a ToastError given a single toaster errors', () => {
const error = new ToasterErrors(['some error 1']);
errorToToaster({ id: 'some-made-up-id', title: 'some title', error, dispatchToaster });
expect(dispatchToaster.mock.calls[0]).toEqual([
{
toast: {
color: 'danger',
errors: ['some error 1'],
iconType: 'alert',
id: 'some-made-up-id',
title: 'some title',
},
type: 'addToaster',
},
]);
});

test('adds a regular Error given a single error', () => {
const error = new Error('some error 1');
errorToToaster({ id: 'some-made-up-id', title: 'some title', error, dispatchToaster });
expect(dispatchToaster.mock.calls[0]).toEqual([
{
toast: {
color: 'danger',
errors: ['some error 1'],
iconType: 'alert',
id: 'some-made-up-id',
title: 'some title',
},
type: 'addToaster',
},
]);
});

test('adds a generic Network Error given a non Error object such as a string', () => {
const error = 'terrible string';
errorToToaster({ id: 'some-made-up-id', title: 'some title', error, dispatchToaster });
expect(dispatchToaster.mock.calls[0]).toEqual([
{
toast: {
color: 'danger',
errors: ['Network Error'],
iconType: 'alert',
id: 'some-made-up-id',
title: 'some title',
},
type: 'addToaster',
},
]);
});
});

describe('#isAnError', () => {
test('returns true if given an error object', () => {
const error = new Error('some error');
expect(isAnError(error)).toEqual(true);
});

test('returns false if given a regular object', () => {
expect(isAnError({})).toEqual(false);
});

test('returns false if given a string', () => {
expect(isAnError('som string')).toEqual(false);
});

test('returns true if given a toaster error', () => {
const error = new ToasterErrors(['some error']);
expect(isAnError(error)).toEqual(true);
});
});

describe('#isToasterError', () => {
test('returns true if given a ToasterErrors object', () => {
const error = new ToasterErrors(['some error']);
expect(isToasterError(error)).toEqual(true);
});

test('returns false if given a regular object', () => {
expect(isToasterError({})).toEqual(false);
});

test('returns false if given a string', () => {
expect(isToasterError('som string')).toEqual(false);
});

test('returns false if given a regular error', () => {
const error = new Error('some error');
expect(isToasterError(error)).toEqual(false);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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 { isError } from 'lodash/fp';
import uuid from 'uuid';
import { ActionToaster, AppToast } from '../../toasters';
import { ToasterErrorsType, ToasterErrors } from './throw_if_not_ok';

export type ErrorToToasterArgs = Partial<AppToast> & {
error: unknown;
dispatchToaster: React.Dispatch<ActionToaster>;
};

export const errorToToaster = ({
id = uuid.v4(),
title,
error,
color = 'danger',
iconType = 'alert',
dispatchToaster,
}: ErrorToToasterArgs) => {
if (isToasterError(error)) {
const toast: AppToast = {
id,
title,
color,
iconType,
errors: error.messages,
};
dispatchToaster({
type: 'addToaster',
toast,
});
} else if (isAnError(error)) {
const toast: AppToast = {
id,
title,
color,
iconType,
errors: [error.message],
};
dispatchToaster({
type: 'addToaster',
toast,
});
} else {
const toast: AppToast = {
id,
title,
color,
iconType,
errors: ['Network Error'],
};
dispatchToaster({
type: 'addToaster',
toast,
});
}
};

export const isAnError = (error: unknown): error is Error => isError(error);

export const isToasterError = (error: unknown): error is ToasterErrorsType =>
error instanceof ToasterErrors;
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import chrome from 'ui/chrome';
import { InfluencerInput, MlCapabilities } from '../types';
import { throwIfNotOk } from './throw_if_not_ok';
import { emptyMlCapabilities } from '../empty_ml_capabilities';

export interface Body {
jobIds: string[];
Expand All @@ -25,21 +24,16 @@ export interface Body {
export const getMlCapabilities = async (
headers: Record<string, string | undefined>
): Promise<MlCapabilities> => {
try {
const response = await fetch(`${chrome.getBasePath()}/api/ml/ml_capabilities`, {
method: 'GET',
credentials: 'same-origin',
headers: {
'kbn-system-api': 'true',
'content-Type': 'application/json',
'kbn-xsrf': chrome.getXsrfToken(),
...headers,
},
});
await throwIfNotOk(response);
return await response.json();
} catch (error) {
// TODO: Toaster error when this happens instead of returning empty data
return emptyMlCapabilities;
}
const response = await fetch(`${chrome.getBasePath()}/api/ml/ml_capabilities`, {
method: 'GET',
credentials: 'same-origin',
headers: {
'kbn-system-api': 'true',
'content-Type': 'application/json',
'kbn-xsrf': chrome.getXsrfToken(),
...headers,
},
});
await throwIfNotOk(response);
return await response.json();
};
Loading

0 comments on commit 1f8c215

Please sign in to comment.