mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
## Summary Fixes two blockers: 1) Anomaly table would spin forever if you zero jobs configured on a fresh install. 2) If you get Network or ML errors this will report the error to the user in the form of the global toaster. Two examples of Error Toaster before the "See the full error(s)" is clicked:  <img width="348" alt="Screen Shot 2019-07-17 at 4 04 30 PM" src="https://user-images.githubusercontent.com/1151048/61414971-ac4bc980-a8ac-11e9-9d15-28ab1229922f.png"> Example Error Toasters from start job expanded and collapsed: <img width="800" alt="Screen Shot 2019-07-17 at 12 15 04 PM" src="https://user-images.githubusercontent.com/1151048/61414610-9e497900-a8ab-11e9-97cd-ec68e77a555c.png"> <img width="808" alt="Screen Shot 2019-07-17 at 12 14 58 PM" src="https://user-images.githubusercontent.com/1151048/61414628-aacdd180-a8ab-11e9-8ad8-edc67deb8874.png"> Example Network Error Toaster: <img width="437" alt="Screen Shot 2019-07-17 at 12 21 10 PM" src="https://user-images.githubusercontent.com/1151048/61414635-b3260c80-a8ab-11e9-8e50-a35a05fc6d2b.png"> Example API Error if you send in something bad such as a bad payload: <img width="442" alt="Screen Shot 2019-07-17 at 12 34 04 PM" src="https://user-images.githubusercontent.com/1151048/61414658-c0db9200-a8ab-11e9-88ed-e6634f17103a.png"> Example Anomalies Table Error if you have a network issue: <img width="464" alt="Screen Shot 2019-07-17 at 12 39 57 PM" src="https://user-images.githubusercontent.com/1151048/61414691-cf29ae00-a8ab-11e9-882c-3cc770776f2f.png"> ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. - [x] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios - [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist) ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)
This commit is contained in:
parent
919c22a5c6
commit
48a2bd2eff
18 changed files with 848 additions and 298 deletions
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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[];
|
||||
|
@ -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[],
|
||||
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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[];
|
||||
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -6,58 +6,240 @@
|
|||
|
||||
// @ts-ignore
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { throwIfNotOk, parseJsonFromBody, MessageBody } from './throw_if_not_ok';
|
||||
import {
|
||||
throwIfNotOk,
|
||||
parseJsonFromBody,
|
||||
MessageBody,
|
||||
tryParseResponse,
|
||||
throwIfErrorAttached,
|
||||
isMlStartJobError,
|
||||
ToasterErrors,
|
||||
} from './throw_if_not_ok';
|
||||
|
||||
describe('throw_if_not_ok', () => {
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
test('does a throw if it is given response that is not ok and the body is not parseable', async () => {
|
||||
fetchMock.mock('http://example.com', 500);
|
||||
const response = await fetch('http://example.com');
|
||||
await expect(throwIfNotOk(response)).rejects.toThrow('Network Error: Internal Server Error');
|
||||
});
|
||||
|
||||
test('does a throw and returns a body if it is parsable', async () => {
|
||||
fetchMock.mock('http://example.com', {
|
||||
status: 500,
|
||||
body: {
|
||||
statusCode: 500,
|
||||
message: 'I am a custom message',
|
||||
},
|
||||
describe('#throwIfNotOk', () => {
|
||||
test('does a throw if it is given response that is not ok and the body is not parsable', async () => {
|
||||
fetchMock.mock('http://example.com', 500);
|
||||
const response = await fetch('http://example.com');
|
||||
await expect(throwIfNotOk(response)).rejects.toThrow('Network Error: Internal Server Error');
|
||||
});
|
||||
|
||||
test('does a throw and returns a body if it is parsable', async () => {
|
||||
fetchMock.mock('http://example.com', {
|
||||
status: 500,
|
||||
body: {
|
||||
statusCode: 500,
|
||||
message: 'I am a custom message',
|
||||
},
|
||||
});
|
||||
const response = await fetch('http://example.com');
|
||||
await expect(throwIfNotOk(response)).rejects.toThrow('I am a custom message');
|
||||
});
|
||||
|
||||
test('does NOT do a throw if it is given response is not ok', async () => {
|
||||
fetchMock.mock('http://example.com', 200);
|
||||
const response = await fetch('http://example.com');
|
||||
await expect(throwIfNotOk(response)).resolves.toEqual(undefined);
|
||||
});
|
||||
const response = await fetch('http://example.com');
|
||||
await expect(throwIfNotOk(response)).rejects.toThrow('I am a custom message');
|
||||
});
|
||||
|
||||
test('does NOT do a throw if it is given response is not ok', async () => {
|
||||
fetchMock.mock('http://example.com', 200);
|
||||
const response = await fetch('http://example.com');
|
||||
await expect(throwIfNotOk(response)).resolves.toEqual(undefined);
|
||||
});
|
||||
|
||||
test('parses a json from the body correctly', async () => {
|
||||
fetchMock.mock('http://example.com', {
|
||||
status: 500,
|
||||
body: {
|
||||
describe('#parseJsonFromBody', () => {
|
||||
test('parses a json from the body correctly', async () => {
|
||||
fetchMock.mock('http://example.com', {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'some error',
|
||||
statusCode: 500,
|
||||
message: 'I am a custom message',
|
||||
},
|
||||
});
|
||||
const response = await fetch('http://example.com');
|
||||
const expected: MessageBody = {
|
||||
error: 'some error',
|
||||
statusCode: 500,
|
||||
message: 'I am a custom message',
|
||||
},
|
||||
};
|
||||
await expect(parseJsonFromBody(response)).resolves.toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns null if the body does not exist', async () => {
|
||||
fetchMock.mock('http://example.com', { status: 500, body: 'some text' });
|
||||
const response = await fetch('http://example.com');
|
||||
await expect(parseJsonFromBody(response)).resolves.toEqual(null);
|
||||
});
|
||||
const response = await fetch('http://example.com');
|
||||
const expected: MessageBody = {
|
||||
error: 'some error',
|
||||
statusCode: 500,
|
||||
message: 'I am a custom message',
|
||||
};
|
||||
await expect(parseJsonFromBody(response)).resolves.toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns null if the body does not exist', async () => {
|
||||
fetchMock.mock('http://example.com', { status: 500, body: 'some text' });
|
||||
const response = await fetch('http://example.com');
|
||||
await expect(parseJsonFromBody(response)).resolves.toEqual(null);
|
||||
describe('#tryParseResponse', () => {
|
||||
test('It formats a JSON object', () => {
|
||||
const parsed = tryParseResponse(JSON.stringify({ hello: 'how are you?' }));
|
||||
expect(parsed).toEqual('{\n "hello": "how are you?"\n}');
|
||||
});
|
||||
|
||||
test('It returns a string as is if that string is not JSON', () => {
|
||||
const parsed = tryParseResponse('some string');
|
||||
expect(parsed).toEqual('some string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isMlErrorMsg', () => {
|
||||
test('It returns true for a ml error msg json', () => {
|
||||
const json: Record<string, Record<string, unknown>> = {
|
||||
error: {
|
||||
msg: 'some message',
|
||||
response: 'some response',
|
||||
statusCode: 400,
|
||||
},
|
||||
};
|
||||
expect(isMlStartJobError(json)).toEqual(true);
|
||||
});
|
||||
|
||||
test('It returns false to a ml error msg if it is missing msg', () => {
|
||||
const json: Record<string, Record<string, unknown>> = {
|
||||
error: {
|
||||
response: 'some response',
|
||||
statusCode: 400,
|
||||
},
|
||||
};
|
||||
expect(isMlStartJobError(json)).toEqual(false);
|
||||
});
|
||||
|
||||
test('It returns false to a ml error msg if it is missing response', () => {
|
||||
const json: Record<string, Record<string, unknown>> = {
|
||||
error: {
|
||||
response: 'some response',
|
||||
statusCode: 400,
|
||||
},
|
||||
};
|
||||
expect(isMlStartJobError(json)).toEqual(false);
|
||||
});
|
||||
|
||||
test('It returns false to a ml error msg if it is missing statusCode', () => {
|
||||
const json: Record<string, Record<string, unknown>> = {
|
||||
error: {
|
||||
msg: 'some message',
|
||||
response: 'some response',
|
||||
},
|
||||
};
|
||||
expect(isMlStartJobError(json)).toEqual(false);
|
||||
});
|
||||
|
||||
test('It returns false to a ml error msg if it is missing error completely', () => {
|
||||
const json: Record<string, Record<string, unknown>> = {};
|
||||
expect(isMlStartJobError(json)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#throwIfErrorAttached', () => {
|
||||
test('It throws if an error is attached', async () => {
|
||||
const json: Record<string, Record<string, unknown>> = {
|
||||
'some-id': {
|
||||
error: {
|
||||
msg: 'some message',
|
||||
response: 'some response',
|
||||
statusCode: 400,
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(() => throwIfErrorAttached(json, ['some-id'])).toThrow(
|
||||
new ToasterErrors(['some message'])
|
||||
);
|
||||
});
|
||||
|
||||
test('It throws if an error is attached and has all the messages expected', async () => {
|
||||
const json: Record<string, Record<string, unknown>> = {
|
||||
'some-id': {
|
||||
error: {
|
||||
msg: 'some message',
|
||||
response: 'some response',
|
||||
statusCode: 400,
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
throwIfErrorAttached(json, ['some-id']);
|
||||
} catch (error) {
|
||||
expect(error.messages).toEqual(['some message', 'some response', 'Status Code: 400']);
|
||||
}
|
||||
});
|
||||
|
||||
test('It throws if an error with the response parsed correctly', async () => {
|
||||
const json: Record<string, Record<string, unknown>> = {
|
||||
'some-id': {
|
||||
error: {
|
||||
msg: 'some message',
|
||||
response: JSON.stringify({ hello: 'how are you?' }),
|
||||
statusCode: 400,
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
throwIfErrorAttached(json, ['some-id']);
|
||||
} catch (error) {
|
||||
expect(error.messages).toEqual([
|
||||
'some message',
|
||||
'{\n "hello": "how are you?"\n}',
|
||||
'Status Code: 400',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test('It throws if an error is attached and has all the messages expected with multiple ids', async () => {
|
||||
const json: Record<string, Record<string, unknown>> = {
|
||||
'some-id-1': {
|
||||
error: {
|
||||
msg: 'some message 1',
|
||||
response: 'some response 1',
|
||||
statusCode: 400,
|
||||
},
|
||||
},
|
||||
'some-id-2': {
|
||||
error: {
|
||||
msg: 'some message 2',
|
||||
response: 'some response 2',
|
||||
statusCode: 500,
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
throwIfErrorAttached(json, ['some-id-1', 'some-id-2']);
|
||||
} catch (error) {
|
||||
expect(error.messages).toEqual([
|
||||
'some message 1',
|
||||
'some response 1',
|
||||
'Status Code: 400',
|
||||
'some message 2',
|
||||
'some response 2',
|
||||
'Status Code: 500',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test('It throws if an error is attached and has all the messages expected with multiple ids but only one valid one is given', async () => {
|
||||
const json: Record<string, Record<string, unknown>> = {
|
||||
'some-id-1': {
|
||||
error: {
|
||||
msg: 'some message 1',
|
||||
response: 'some response 1',
|
||||
statusCode: 400,
|
||||
},
|
||||
},
|
||||
'some-id-2': {
|
||||
error: {
|
||||
msg: 'some message 2',
|
||||
response: 'some response 2',
|
||||
statusCode: 500,
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
throwIfErrorAttached(json, ['some-id-1', 'some-id-not-here']);
|
||||
} catch (error) {
|
||||
expect(error.messages).toEqual(['some message 1', 'some response 1', 'Status Code: 400']);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,19 +4,50 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { has } from 'lodash/fp';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface MessageBody {
|
||||
error?: string;
|
||||
message?: string;
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
export interface MlStartJobError {
|
||||
error: {
|
||||
msg: string;
|
||||
response: string;
|
||||
statusCode: number;
|
||||
};
|
||||
started: boolean;
|
||||
}
|
||||
|
||||
export type ToasterErrorsType = Error & {
|
||||
messages: string[];
|
||||
};
|
||||
|
||||
export class ToasterErrors extends Error implements ToasterErrorsType {
|
||||
public messages: string[];
|
||||
|
||||
constructor(messages: string[]) {
|
||||
super(messages[0]);
|
||||
this.name = 'ToasterErrors';
|
||||
this.messages = messages;
|
||||
}
|
||||
}
|
||||
|
||||
export const throwIfNotOk = async (response: Response): Promise<void> => {
|
||||
if (!response.ok) {
|
||||
const body = await parseJsonFromBody(response);
|
||||
if (body != null && body.message) {
|
||||
throw new Error(body.message);
|
||||
if (body.statusCode != null) {
|
||||
throw new ToasterErrors([body.message, `${i18n.STATUS_CODE} ${body.statusCode}`]);
|
||||
} else {
|
||||
throw new ToasterErrors([body.message]);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Network Error: ${response.statusText}`);
|
||||
throw new ToasterErrors([`${i18n.NETWORK_ERROR} ${response.statusText}`]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -29,3 +60,40 @@ export const parseJsonFromBody = async (response: Response): Promise<MessageBody
|
|||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const tryParseResponse = (response: string): string => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(response), null, 2);
|
||||
} catch (error) {
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
export const throwIfErrorAttached = (
|
||||
json: Record<string, Record<string, unknown>>,
|
||||
dataFeedIds: string[]
|
||||
): void => {
|
||||
const errors = dataFeedIds.reduce<string[]>((accum, dataFeedId) => {
|
||||
const dataFeed = json[dataFeedId];
|
||||
if (isMlStartJobError(dataFeed)) {
|
||||
accum = [
|
||||
...accum,
|
||||
dataFeed.error.msg,
|
||||
tryParseResponse(dataFeed.error.response),
|
||||
`${i18n.STATUS_CODE} ${dataFeed.error.statusCode}`,
|
||||
];
|
||||
return accum;
|
||||
} else {
|
||||
return accum;
|
||||
}
|
||||
}, []);
|
||||
if (errors.length > 0) {
|
||||
throw new ToasterErrors(errors);
|
||||
}
|
||||
};
|
||||
|
||||
// use the "in operator" and regular type guards to do a narrow once this issue is fixed below:
|
||||
// https://github.com/microsoft/TypeScript/issues/21732
|
||||
// Otherwise for now, has will work ok even though it casts 'unknown' to 'any'
|
||||
export const isMlStartJobError = (value: unknown): value is MlStartJobError =>
|
||||
has('error.msg', value) && has('error.response', value) && has('error.statusCode', value);
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 STATUS_CODE = i18n.translate(
|
||||
'xpack.siem.components.ml.api.errors.statusCodeFailureTitle',
|
||||
{
|
||||
defaultMessage: 'Status Code:',
|
||||
}
|
||||
);
|
||||
|
||||
export const NETWORK_ERROR = i18n.translate(
|
||||
'xpack.siem.components.ml.api.errors.networkErrorFailureTitle',
|
||||
{
|
||||
defaultMessage: 'Network Error:',
|
||||
}
|
||||
);
|
|
@ -9,16 +9,29 @@ import { MlCapabilities } from '../types';
|
|||
import { getMlCapabilities } from '../api/get_ml_capabilities';
|
||||
import { KibanaConfigContext } from '../../../lib/adapters/framework/kibana_framework_adapter';
|
||||
import { emptyMlCapabilities } from '../empty_ml_capabilities';
|
||||
import { errorToToaster } from '../api/error_to_toaster';
|
||||
import { useStateToaster } from '../../toasters';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const MlCapabilitiesContext = React.createContext<MlCapabilities>(emptyMlCapabilities);
|
||||
|
||||
export const MlCapabilitiesProvider = React.memo<{ children: JSX.Element }>(({ children }) => {
|
||||
const [capabilities, setCapabilities] = useState(emptyMlCapabilities);
|
||||
const config = useContext(KibanaConfigContext);
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
const fetchFunc = async () => {
|
||||
const mlCapabilities = await getMlCapabilities({ 'kbn-version': config.kbnVersion });
|
||||
setCapabilities(mlCapabilities);
|
||||
try {
|
||||
const mlCapabilities = await getMlCapabilities({ 'kbn-version': config.kbnVersion });
|
||||
setCapabilities(mlCapabilities);
|
||||
} catch (error) {
|
||||
errorToToaster({
|
||||
title: i18n.MACHINE_LEARNING_PERMISSIONS_FAILURE,
|
||||
error,
|
||||
dispatchToaster,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -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 MACHINE_LEARNING_PERMISSIONS_FAILURE = i18n.translate(
|
||||
'xpack.siem.components.ml.permissions.errors.machineLearningPermissionsFailureTitle',
|
||||
{
|
||||
defaultMessage: 'Machine learning permissions failure',
|
||||
}
|
||||
);
|
|
@ -15,17 +15,7 @@ import {
|
|||
StartDatafeedResponse,
|
||||
StopDatafeedResponse,
|
||||
} from './types';
|
||||
import { throwIfNotOk } from '../ml/api/throw_if_not_ok';
|
||||
|
||||
const emptyGroup: Group[] = [];
|
||||
|
||||
const emptyMlResponse: SetupMlResponse = { jobs: [], datafeeds: [], kibana: {} };
|
||||
|
||||
const emptyStartDatafeedResponse: StartDatafeedResponse = {};
|
||||
|
||||
const emptyStopDatafeeds: [StopDatafeedResponse, CloseJobsResponse] = [{}, {}];
|
||||
|
||||
const emptyJob: Job[] = [];
|
||||
import { throwIfNotOk, throwIfErrorAttached } from '../ml/api/throw_if_not_ok';
|
||||
|
||||
const emptyIndexPattern: string = '';
|
||||
|
||||
|
@ -35,23 +25,18 @@ const emptyIndexPattern: string = '';
|
|||
* @param headers
|
||||
*/
|
||||
export const groupsData = async (headers: Record<string, string | undefined>): Promise<Group[]> => {
|
||||
try {
|
||||
const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/groups`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'kbn-system-api': 'true',
|
||||
'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 emptyGroup;
|
||||
}
|
||||
const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/groups`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'kbn-system-api': 'true',
|
||||
'kbn-xsrf': chrome.getXsrfToken(),
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
await throwIfNotOk(response);
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -70,30 +55,25 @@ export const setupMlJob = async ({
|
|||
prefix = '',
|
||||
headers = {},
|
||||
}: MlSetupArgs): Promise<SetupMlResponse> => {
|
||||
try {
|
||||
const response = await fetch(`${chrome.getBasePath()}/api/ml/modules/setup/${configTemplate}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
prefix,
|
||||
groups,
|
||||
indexPatternName,
|
||||
startDatafeed: false,
|
||||
useDedicatedIndex: false,
|
||||
}),
|
||||
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 emptyMlResponse;
|
||||
}
|
||||
const response = await fetch(`${chrome.getBasePath()}/api/ml/modules/setup/${configTemplate}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
prefix,
|
||||
groups,
|
||||
indexPatternName,
|
||||
startDatafeed: false,
|
||||
useDedicatedIndex: false,
|
||||
}),
|
||||
headers: {
|
||||
'kbn-system-api': 'true',
|
||||
'content-type': 'application/json',
|
||||
'kbn-xsrf': chrome.getXsrfToken(),
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
await throwIfNotOk(response);
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -108,27 +88,24 @@ export const startDatafeeds = async (
|
|||
headers: Record<string, string | undefined>,
|
||||
start = 0
|
||||
): Promise<StartDatafeedResponse> => {
|
||||
try {
|
||||
const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/force_start_datafeeds`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
datafeedIds,
|
||||
...(start !== 0 && { start }),
|
||||
}),
|
||||
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 emptyStartDatafeedResponse;
|
||||
}
|
||||
const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/force_start_datafeeds`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
datafeedIds,
|
||||
...(start !== 0 && { start }),
|
||||
}),
|
||||
headers: {
|
||||
'kbn-system-api': 'true',
|
||||
'content-type': 'application/json',
|
||||
'kbn-xsrf': chrome.getXsrfToken(),
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
await throwIfNotOk(response);
|
||||
const json = await response.json();
|
||||
throwIfErrorAttached(json, datafeedIds);
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -141,52 +118,44 @@ export const stopDatafeeds = async (
|
|||
datafeedIds: string[],
|
||||
headers: Record<string, string | undefined>
|
||||
): Promise<[StopDatafeedResponse, CloseJobsResponse]> => {
|
||||
try {
|
||||
const stopDatafeedsResponse = await fetch(
|
||||
`${chrome.getBasePath()}/api/ml/jobs/stop_datafeeds`,
|
||||
{
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
datafeedIds,
|
||||
}),
|
||||
headers: {
|
||||
'kbn-system-api': 'true',
|
||||
'content-type': 'application/json',
|
||||
'kbn-xsrf': chrome.getXsrfToken(),
|
||||
...headers,
|
||||
},
|
||||
}
|
||||
);
|
||||
const stopDatafeedsResponse = await fetch(`${chrome.getBasePath()}/api/ml/jobs/stop_datafeeds`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
datafeedIds,
|
||||
}),
|
||||
headers: {
|
||||
'kbn-system-api': 'true',
|
||||
'content-type': 'application/json',
|
||||
'kbn-xsrf': chrome.getXsrfToken(),
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
|
||||
await throwIfNotOk(stopDatafeedsResponse);
|
||||
const stopDatafeedsResponseJson = await stopDatafeedsResponse.json();
|
||||
await throwIfNotOk(stopDatafeedsResponse);
|
||||
const stopDatafeedsResponseJson = await stopDatafeedsResponse.json();
|
||||
|
||||
const datafeedPrefix = 'datafeed-';
|
||||
const closeJobsResponse = await fetch(`${chrome.getBasePath()}/api/ml/jobs/close_jobs`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
jobIds: datafeedIds.map(dataFeedId =>
|
||||
dataFeedId.startsWith(datafeedPrefix)
|
||||
? dataFeedId.substring(datafeedPrefix.length)
|
||||
: dataFeedId
|
||||
),
|
||||
}),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'kbn-system-api': 'true',
|
||||
'kbn-xsrf': chrome.getXsrfToken(),
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
const datafeedPrefix = 'datafeed-';
|
||||
const closeJobsResponse = await fetch(`${chrome.getBasePath()}/api/ml/jobs/close_jobs`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
jobIds: datafeedIds.map(dataFeedId =>
|
||||
dataFeedId.startsWith(datafeedPrefix)
|
||||
? dataFeedId.substring(datafeedPrefix.length)
|
||||
: dataFeedId
|
||||
),
|
||||
}),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'kbn-system-api': 'true',
|
||||
'kbn-xsrf': chrome.getXsrfToken(),
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
|
||||
await throwIfNotOk(stopDatafeedsResponseJson);
|
||||
return [stopDatafeedsResponseJson, await closeJobsResponse.json()];
|
||||
} catch (error) {
|
||||
// TODO: Toaster error when this happens instead of returning empty data
|
||||
return emptyStopDatafeeds;
|
||||
}
|
||||
await throwIfNotOk(closeJobsResponse);
|
||||
return [stopDatafeedsResponseJson, await closeJobsResponse.json()];
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -199,24 +168,19 @@ export const jobsSummary = async (
|
|||
jobIds: string[],
|
||||
headers: Record<string, string | undefined>
|
||||
): Promise<Job[]> => {
|
||||
try {
|
||||
const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/jobs_summary`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ jobIds }),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'kbn-xsrf': chrome.getXsrfToken(),
|
||||
'kbn-system-api': 'true',
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
await throwIfNotOk(response);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
// TODO: Toaster error when this happens instead of returning empty data
|
||||
return emptyJob;
|
||||
}
|
||||
const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/jobs_summary`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ jobIds }),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'kbn-xsrf': chrome.getXsrfToken(),
|
||||
'kbn-system-api': 'true',
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
await throwIfNotOk(response);
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -227,38 +191,33 @@ export const jobsSummary = async (
|
|||
export const getIndexPatterns = async (
|
||||
headers: Record<string, string | undefined>
|
||||
): Promise<string> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${chrome.getBasePath()}/api/saved_objects/_find?type=index-pattern&fields=title&fields=type&per_page=10000`,
|
||||
{
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'kbn-xsrf': chrome.getXsrfToken(),
|
||||
'kbn-system-api': 'true',
|
||||
...headers,
|
||||
},
|
||||
}
|
||||
);
|
||||
await throwIfNotOk(response);
|
||||
const results: IndexPatternResponse = await response.json();
|
||||
|
||||
if (results.saved_objects && Array.isArray(results.saved_objects)) {
|
||||
return results.saved_objects
|
||||
.reduce(
|
||||
(acc: string[], v) => [
|
||||
...acc,
|
||||
...(v.attributes && v.attributes.title ? [v.attributes.title] : []),
|
||||
],
|
||||
[]
|
||||
)
|
||||
.join(', ');
|
||||
} else {
|
||||
return emptyIndexPattern;
|
||||
const response = await fetch(
|
||||
`${chrome.getBasePath()}/api/saved_objects/_find?type=index-pattern&fields=title&fields=type&per_page=10000`,
|
||||
{
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'kbn-xsrf': chrome.getXsrfToken(),
|
||||
'kbn-system-api': 'true',
|
||||
...headers,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
// TODO: Toaster error when this happens instead of returning empty data
|
||||
);
|
||||
await throwIfNotOk(response);
|
||||
const results: IndexPatternResponse = await response.json();
|
||||
|
||||
if (results.saved_objects && Array.isArray(results.saved_objects)) {
|
||||
return results.saved_objects
|
||||
.reduce(
|
||||
(acc: string[], v) => [
|
||||
...acc,
|
||||
...(v.attributes && v.attributes.title ? [v.attributes.title] : []),
|
||||
],
|
||||
[]
|
||||
)
|
||||
.join(', ');
|
||||
} else {
|
||||
return emptyIndexPattern;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 INDEX_PATTERN_FETCH_FAILURE = i18n.translate(
|
||||
'xpack.siem.components.mlPopup.hooks.errors.indexPatternFetchFailureTitle',
|
||||
{
|
||||
defaultMessage: 'Index pattern fetch failure',
|
||||
}
|
||||
);
|
||||
|
||||
export const JOB_SUMMARY_FETCH_FAILURE = i18n.translate(
|
||||
'xpack.siem.components.mlPopup.hooks.errors.jobSummaryFetchFailureTitle',
|
||||
{
|
||||
defaultMessage: 'Job summary fetch failure',
|
||||
}
|
||||
);
|
||||
|
||||
export const SIEM_JOB_FETCH_FAILURE = i18n.translate(
|
||||
'xpack.siem.components.mlPopup.hooks.errors.siemJobFetchFailureTitle',
|
||||
{
|
||||
defaultMessage: 'SIEM job fetch failure',
|
||||
}
|
||||
);
|
|
@ -7,6 +7,10 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { getIndexPatterns } from '../api';
|
||||
import { KibanaConfigContext } from '../../../lib/adapters/framework/kibana_framework_adapter';
|
||||
import { useStateToaster } from '../../toasters';
|
||||
import { errorToToaster } from '../../ml/api/error_to_toaster';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
type Return = [boolean, string];
|
||||
|
||||
|
@ -14,14 +18,20 @@ export const useIndexPatterns = (refreshToggle = false): Return => {
|
|||
const [indexPattern, setIndexPattern] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const config = useContext(KibanaConfigContext);
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
const fetchFunc = async () => {
|
||||
const data = await getIndexPatterns({
|
||||
'kbn-version': config.kbnVersion,
|
||||
});
|
||||
try {
|
||||
const data = await getIndexPatterns({
|
||||
'kbn-version': config.kbnVersion,
|
||||
});
|
||||
|
||||
setIndexPattern(data);
|
||||
setIsLoading(false);
|
||||
setIndexPattern(data);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
errorToToaster({ title: i18n.INDEX_PATTERN_FETCH_FAILURE, error, dispatchToaster });
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -10,6 +10,10 @@ import { Job } from '../types';
|
|||
import { KibanaConfigContext } from '../../../lib/adapters/framework/kibana_framework_adapter';
|
||||
import { hasMlUserPermissions } from '../../ml/permissions/has_ml_user_permissions';
|
||||
import { MlCapabilitiesContext } from '../../ml/permissions/ml_capabilities_provider';
|
||||
import { useStateToaster } from '../../toasters';
|
||||
import { errorToToaster } from '../../ml/api/error_to_toaster';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
type Return = [boolean, Job[] | null];
|
||||
|
||||
|
@ -24,17 +28,22 @@ export const useJobSummaryData = (jobIds: string[] = [], refreshToggle = false):
|
|||
const config = useContext(KibanaConfigContext);
|
||||
const capabilities = useContext(MlCapabilitiesContext);
|
||||
const userPermissions = hasMlUserPermissions(capabilities);
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
const fetchFunc = async () => {
|
||||
if (userPermissions) {
|
||||
const data: Job[] = await jobsSummary(jobIds, {
|
||||
'kbn-version': config.kbnVersion,
|
||||
});
|
||||
try {
|
||||
const data: Job[] = await jobsSummary(jobIds, {
|
||||
'kbn-version': config.kbnVersion,
|
||||
});
|
||||
|
||||
// TODO: API returns all jobs even though we specified jobIds -- jobsSummary call seems to match request in ML App?
|
||||
const siemJobs = getSiemJobsFromJobsSummary(data);
|
||||
// TODO: API returns all jobs even though we specified jobIds -- jobsSummary call seems to match request in ML App?
|
||||
const siemJobs = getSiemJobsFromJobsSummary(data);
|
||||
|
||||
setJobSummaryData(siemJobs);
|
||||
setJobSummaryData(siemJobs);
|
||||
} catch (error) {
|
||||
errorToToaster({ title: i18n.JOB_SUMMARY_FETCH_FAILURE, error, dispatchToaster });
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
|
|
@ -10,6 +10,10 @@ import { Group } from '.././types';
|
|||
import { KibanaConfigContext } from '../../../lib/adapters/framework/kibana_framework_adapter';
|
||||
import { hasMlUserPermissions } from '../../ml/permissions/has_ml_user_permissions';
|
||||
import { MlCapabilitiesContext } from '../../ml/permissions/ml_capabilities_provider';
|
||||
import { useStateToaster } from '../../toasters';
|
||||
import { errorToToaster } from '../../ml/api/error_to_toaster';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
type Return = [boolean, string[]];
|
||||
|
||||
|
@ -24,16 +28,21 @@ export const useSiemJobs = (refetchData: boolean): Return => {
|
|||
const config = useContext(KibanaConfigContext);
|
||||
const capabilities = useContext(MlCapabilitiesContext);
|
||||
const userPermissions = hasMlUserPermissions(capabilities);
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
const fetchFunc = async () => {
|
||||
if (userPermissions) {
|
||||
const data = await groupsData({
|
||||
'kbn-version': config.kbnVersion,
|
||||
});
|
||||
try {
|
||||
const data = await groupsData({
|
||||
'kbn-version': config.kbnVersion,
|
||||
});
|
||||
|
||||
const siemJobIds = getSiemJobIdsFromGroupsData(data);
|
||||
const siemJobIds = getSiemJobIdsFromGroupsData(data);
|
||||
|
||||
setSiemJobs(siemJobIds);
|
||||
setSiemJobs(siemJobIds);
|
||||
} catch (error) {
|
||||
errorToToaster({ title: i18n.SIEM_JOB_FETCH_FAILURE, error, dispatchToaster });
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
|
|
@ -24,6 +24,8 @@ import { ShowingCount } from './jobs_table/showing_count';
|
|||
import { PopoverDescription } from './popover_description';
|
||||
import { getConfigTemplatesToInstall, getJobsToDisplay, getJobsToInstall } from './helpers';
|
||||
import { configTemplates, siemJobPrefix } from './config_templates';
|
||||
import { useStateToaster } from '../toasters';
|
||||
import { errorToToaster } from '../ml/api/error_to_toaster';
|
||||
|
||||
const PopoverContentsDiv = styled.div`
|
||||
max-width: 550px;
|
||||
|
@ -89,6 +91,7 @@ export const MlPopover = React.memo(() => {
|
|||
const [isLoadingJobSummaryData, jobSummaryData] = useJobSummaryData([], refreshToggle);
|
||||
const [isCreatingJobs, setIsCreatingJobs] = useState(false);
|
||||
const [filterQuery, setFilterQuery] = useState('');
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
const [, configuredIndexPattern] = useIndexPatterns(refreshToggle);
|
||||
const config = useContext(KibanaConfigContext);
|
||||
|
@ -105,9 +108,17 @@ export const MlPopover = React.memo(() => {
|
|||
|
||||
if (enable) {
|
||||
const startTime = Math.max(latestTimestampMs, maxStartTime);
|
||||
await startDatafeeds([`datafeed-${jobName}`], headers, startTime);
|
||||
try {
|
||||
await startDatafeeds([`datafeed-${jobName}`], headers, startTime);
|
||||
} catch (error) {
|
||||
errorToToaster({ title: i18n.START_JOB_FAILURE, error, dispatchToaster });
|
||||
}
|
||||
} else {
|
||||
await stopDatafeeds([`datafeed-${jobName}`], headers);
|
||||
try {
|
||||
await stopDatafeeds([`datafeed-${jobName}`], headers);
|
||||
} catch (error) {
|
||||
errorToToaster({ title: i18n.STOP_JOB_FAILURE, error, dispatchToaster });
|
||||
}
|
||||
}
|
||||
dispatch({ type: 'refresh' });
|
||||
};
|
||||
|
@ -144,19 +155,24 @@ export const MlPopover = React.memo(() => {
|
|||
) {
|
||||
const setupJobs = async () => {
|
||||
setIsCreatingJobs(true);
|
||||
await Promise.all(
|
||||
configTemplatesToInstall.map(configTemplate => {
|
||||
return setupMlJob({
|
||||
configTemplate: configTemplate.name,
|
||||
indexPatternName: configTemplate.defaultIndexPattern,
|
||||
groups: ['siem'],
|
||||
prefix: siemJobPrefix,
|
||||
headers,
|
||||
});
|
||||
})
|
||||
);
|
||||
setIsCreatingJobs(false);
|
||||
dispatch({ type: 'refresh' });
|
||||
try {
|
||||
await Promise.all(
|
||||
configTemplatesToInstall.map(configTemplate => {
|
||||
return setupMlJob({
|
||||
configTemplate: configTemplate.name,
|
||||
indexPatternName: configTemplate.defaultIndexPattern,
|
||||
groups: ['siem'],
|
||||
prefix: siemJobPrefix,
|
||||
headers,
|
||||
});
|
||||
})
|
||||
);
|
||||
setIsCreatingJobs(false);
|
||||
dispatch({ type: 'refresh' });
|
||||
} catch (error) {
|
||||
errorToToaster({ title: i18n.CREATE_JOB_FAILURE, error, dispatchToaster });
|
||||
setIsCreatingJobs(false);
|
||||
}
|
||||
};
|
||||
setupJobs();
|
||||
}
|
||||
|
|
|
@ -78,3 +78,21 @@ export const CREATE_CUSTOM_JOB = i18n.translate(
|
|||
defaultMessage: 'Create custom job',
|
||||
}
|
||||
);
|
||||
|
||||
export const START_JOB_FAILURE = i18n.translate(
|
||||
'xpack.siem.components.mlPopup.errors.startJobFailureTitle',
|
||||
{
|
||||
defaultMessage: 'Start job failure',
|
||||
}
|
||||
);
|
||||
|
||||
export const STOP_JOB_FAILURE = i18n.translate('xpack.siem.containers.errors.stopJobFailureTitle', {
|
||||
defaultMessage: 'Stop job failure',
|
||||
});
|
||||
|
||||
export const CREATE_JOB_FAILURE = i18n.translate(
|
||||
'xpack.siem.components.mlPopup.errors.createJobFailureTitle',
|
||||
{
|
||||
defaultMessage: 'Create job failure',
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue