Skip to content

Commit

Permalink
[SIEM] Adds error toasts to MapEmbeddable component (elastic#45088)
Browse files Browse the repository at this point in the history
Displays error toasts when an error occurs within the MapEmbeddable component. This PR also updates the maps colors as per team discussion, removes filter on source/destination layers (to show points when either src/dest are configured), and also increases test coverage.

fixes elastic/siem-team#449

Toast with map hidden:
![image](https://user-images.githubusercontent.com/2946766/64562698-c03f0500-d30a-11e9-8a8a-d6a352cf0d93.png)

Toast Details:
![image](https://user-images.githubusercontent.com/2946766/64562725-cd5bf400-d30a-11e9-8e49-dbaa425b6f2e.png)

![image](https://user-images.githubusercontent.com/2946766/64561629-73f2c580-d308-11e9-9f28-c76c0bc99d39.png)

![image](https://user-images.githubusercontent.com/2946766/64562517-62122200-d30a-11e9-9e1e-737d7dc785f7.png)

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)~

- [ ] ~This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~
- [ ] ~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)~
  • Loading branch information
spong committed Sep 10, 2019
1 parent 7305d0b commit b143a66
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 143 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const mockSourceLayer = {
style: {
type: 'VECTOR',
properties: {
fillColor: { type: 'STATIC', options: { color: '#3cb44b' } },
fillColor: { type: 'STATIC', options: { color: '#3185FC' } },
lineColor: { type: 'STATIC', options: { color: '#FFFFFF' } },
lineWidth: { type: 'STATIC', options: { size: 1 } },
iconSize: { type: 'STATIC', options: { size: 6 } },
Expand All @@ -41,7 +41,7 @@ export const mockSourceLayer = {
visible: true,
applyGlobalQuery: true,
type: 'VECTOR',
query: { query: 'source.geo.location:* and destination.geo.location:*', language: 'kuery' },
query: { query: '', language: 'kuery' },
joins: [],
};

Expand All @@ -65,7 +65,7 @@ export const mockDestinationLayer = {
style: {
type: 'VECTOR',
properties: {
fillColor: { type: 'STATIC', options: { color: '#e6194b' } },
fillColor: { type: 'STATIC', options: { color: '#DB1374' } },
lineColor: { type: 'STATIC', options: { color: '#FFFFFF' } },
lineWidth: { type: 'STATIC', options: { size: 1 } },
iconSize: { type: 'STATIC', options: { size: 6 } },
Expand All @@ -81,7 +81,7 @@ export const mockDestinationLayer = {
visible: true,
applyGlobalQuery: true,
type: 'VECTOR',
query: { query: 'source.geo.location:* and destination.geo.location:*', language: 'kuery' },
query: { query: '', language: 'kuery' },
};

export const mockLineLayer = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import * as React from 'react';
import { EmbeddedMap } from './embedded_map';
import { inputsModel } from '../../store/inputs';
import { SetQuery } from './types';

jest.mock('ui/new_platform', () => ({
npStart: {
Expand All @@ -29,17 +29,7 @@ jest.mock('ui/new_platform', () => ({

describe('EmbeddedMap', () => {
let applyFilterQueryFromKueryExpression: (expression: string) => void;
let setQuery: ({
id,
inspect,
loading,
refetch,
}: {
id: string;
inspect: inputsModel.InspectQuery | null;
loading: boolean;
refetch: inputsModel.Refetch;
}) => void;
let setQuery: SetQuery;

beforeEach(() => {
applyFilterQueryFromKueryExpression = jest.fn(expression => {});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,21 @@ import * as React from 'react';
import { useEffect, useState } from 'react';
import { npStart } from 'ui/new_platform';
import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
import uuid from 'uuid';

import styled from 'styled-components';
import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy';
import {
APPLY_FILTER_ACTION,
APPLY_FILTER_TRIGGER,
CONTEXT_MENU_TRIGGER,
EmbeddablePanel,
PANEL_BADGE_TRIGGER,
ViewMode,
} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
// @ts-ignore Missing type defs as maps moves to Typescript
import { MAP_SAVED_OBJECT_TYPE } from '../../../../maps/common/constants';
import { EmbeddablePanel } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';

import { Loader } from '../loader';
import {
APPLY_SIEM_FILTER_ACTION_ID,
ApplySiemFilterAction,
} from './actions/apply_siem_filter_action';
import { useIndexPatterns } from '../ml_popover/hooks/use_index_patterns';
import { getLayerList } from './map_config';
import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting';
import { DEFAULT_INDEX_KEY } from '../../../common/constants';
import { getIndexPatternTitleIdMapping } from '../ml_popover/helpers';
import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt';
import {
EmbeddableOutput,
IEmbeddable,
} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/embeddables';
import { IndexPatternMapping, MapEmbeddableInput } from './types';
import { MapEmbeddable, SetQuery } from './types';
import * as i18n from './translations';
import { inputsModel } from '../../store/inputs';

// Used for setQuery to get a hook for when the user requests a refresh. Scope to page type if using map elsewhere
const ID = 'embeddedMap';
import { useStateToaster } from '../toasters';
import { createEmbeddable, displayErrorToast, setupEmbeddablesAPI } from './embedded_map_helpers';

const EmbeddableWrapper = styled(EuiFlexGroup)`
position: relative;
Expand All @@ -60,118 +40,62 @@ export interface EmbeddedMapProps {
queryExpression: string;
startDate: number;
endDate: number;
setQuery: (params: {
id: string;
inspect: inputsModel.InspectQuery | null;
loading: boolean;
refetch: inputsModel.Refetch;
}) => void;
setQuery: SetQuery;
}

export const EmbeddedMap = React.memo<EmbeddedMapProps>(
({ applyFilterQueryFromKueryExpression, queryExpression, startDate, endDate, setQuery }) => {
const [embeddable, setEmbeddable] = React.useState<IEmbeddable<
MapEmbeddableInput,
EmbeddableOutput
> | null>(null);
const [embeddable, setEmbeddable] = React.useState<MapEmbeddable | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
const [isIndexError, setIsIndexError] = useState(false);

const [, dispatchToaster] = useStateToaster();
const [loadingKibanaIndexPatterns, kibanaIndexPatterns] = useIndexPatterns();
const [siemDefaultIndices] = useKibanaUiSetting(DEFAULT_INDEX_KEY);

const loadEmbeddable = async (id: string, indexPatterns: IndexPatternMapping[]) => {
const setupEmbeddable = async () => {
// Configure Embeddables API
try {
const factory = start.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE);

const state = {
layerList: getLayerList(indexPatterns),
title: i18n.MAP_TITLE,
};
const input = {
id,
filters: [],
hidePanelTitles: true,
query: { query: queryExpression, language: 'kuery' },
refreshConfig: { value: 0, pause: true },
timeRange: {
from: new Date(startDate).toISOString(),
to: new Date(endDate).toISOString(),
},
viewMode: ViewMode.VIEW,
isLayerTOCOpen: false,
openTOCDetails: [],
hideFilterActions: false,
mapCenter: { lon: -1.05469, lat: 15.96133, zoom: 1 },
};

// @ts-ignore method added in https://github.com/elastic/kibana/pull/43878
const embeddableObject = await factory.createFromState(state, input);

// Wire up to app refresh action
setQuery({
id: ID,
inspect: null,
loading: false,
refetch: embeddableObject.reload,
});

setEmbeddable(embeddableObject);
setupEmbeddablesAPI(applyFilterQueryFromKueryExpression);
} catch (e) {
// TODO: Throw toast https://github.com/elastic/siem-team/issues/449
displayErrorToast(i18n.ERROR_CONFIGURING_EMBEDDABLES_API, e.message, dispatchToaster);
setIsLoading(false);
setIsError(true);
return false;
}
};

/**
* Temporary Embeddables API configuration override until ability to edit actions is addressed:
* https://github.com/elastic/kibana/issues/43643
*/
const setupEmbeddablesAPI = (): boolean => {
// Ensure at least one `siem:defaultIndex` index pattern exists before trying to import
const matchingIndexPatterns = kibanaIndexPatterns.filter(ip =>
siemDefaultIndices.includes(ip.attributes.title)
);
if (matchingIndexPatterns.length === 0) {
setIsLoading(false);
setIsIndexError(true);
return;
}

// Create & set Embeddable
try {
const actions = start.getTriggerActions(APPLY_FILTER_TRIGGER);
const actionLoaded = actions.some(a => a.id === APPLY_SIEM_FILTER_ACTION_ID);
if (!actionLoaded) {
const siemFilterAction = new ApplySiemFilterAction({
applyFilterQueryFromKueryExpression,
});
start.registerAction(siemFilterAction);
start.attachAction(APPLY_FILTER_TRIGGER, siemFilterAction.id);

start.detachAction(CONTEXT_MENU_TRIGGER, 'CUSTOM_TIME_RANGE');
start.detachAction(PANEL_BADGE_TRIGGER, 'CUSTOM_TIME_RANGE_BADGE');
start.detachAction(APPLY_FILTER_TRIGGER, APPLY_FILTER_ACTION);
}
return true;
const embeddableObject = await createEmbeddable(
getIndexPatternTitleIdMapping(matchingIndexPatterns),
queryExpression,
startDate,
endDate,
setQuery
);
setEmbeddable(embeddableObject);
} catch (e) {
// TODO: Throw toast https://github.com/elastic/siem-team/issues/449
return false;
displayErrorToast(i18n.ERROR_CREATING_EMBEDDABLE, e.message, dispatchToaster);
setIsError(true);
}
setIsLoading(false);
};

// Initial Load useEffect
useEffect(() => {
setIsLoading(true);

const importIfNotExists = async () => {
const matchingIndexPatterns = kibanaIndexPatterns.filter(ip =>
siemDefaultIndices.includes(ip.attributes.title)
);

const setupSuccessfully = setupEmbeddablesAPI();

// Ensure at least one `siem:defaultIndex` index pattern exists before trying to import
if (matchingIndexPatterns.length === 0 || !setupSuccessfully) {
setIsLoading(false);
setIsError(true);
return;
}

await loadEmbeddable(uuid.v4(), getIndexPatternTitleIdMapping(matchingIndexPatterns));
setIsLoading(false);
};

if (!loadingKibanaIndexPatterns && kibanaIndexPatterns.length > 0) {
importIfNotExists();
if (!loadingKibanaIndexPatterns) {
setupEmbeddable();
}
}, [loadingKibanaIndexPatterns, kibanaIndexPatterns]);

Expand All @@ -194,11 +118,12 @@ export const EmbeddedMap = React.memo<EmbeddedMapProps>(
}
}, [startDate, endDate]);

return (
return isError ? null : (
<>
<EmbeddableWrapper>
{embeddable != null ? (
<EmbeddablePanel
data-test-subj="embeddable-panel"
embeddable={embeddable}
getActions={start.getTriggerCompatibleActions}
getEmbeddableFactory={start.getEmbeddableFactory}
Expand All @@ -208,10 +133,10 @@ export const EmbeddedMap = React.memo<EmbeddedMapProps>(
inspector={npStart.plugins.inspector}
SavedObjectFinder={SavedObjectFinder}
/>
) : !isLoading && isError ? (
<IndexPatternsMissingPrompt />
) : !isLoading && isIndexError ? (
<IndexPatternsMissingPrompt data-test-subj="missing-prompt" />
) : (
<Loader data-test-subj="pewpew-loading-panel" overlay size="xl" />
<Loader data-test-subj="loading-panel" overlay size="xl" />
)}
</EmbeddableWrapper>
<EuiSpacer />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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 { createEmbeddable, displayErrorToast, setupEmbeddablesAPI } from './embedded_map_helpers';
import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy';

jest.mock('../../lib/settings/use_kibana_ui_setting');

jest.mock(
'../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy',
() => ({
start: {
getTriggerActions: jest.fn(() => []),
registerAction: jest.fn(),
attachAction: jest.fn(),
detachAction: jest.fn(),
getEmbeddableFactory: () => ({
createFromState: () => ({
reload: jest.fn(),
}),
}),
},
})
);

jest.mock('uuid', () => {
return {
v4: jest.fn(() => '9e1f72a9-7c73-4b7f-a562-09940f7daf4a'),
};
});

describe('embedded_map_helpers', () => {
describe('displayErrorToast', () => {
test('dispatches toast with correct title and message', () => {
const mockToast = {
toast: {
color: 'danger',
errors: ['message'],
iconType: 'alert',
id: '9e1f72a9-7c73-4b7f-a562-09940f7daf4a',
title: 'Title',
},
type: 'addToaster',
};
const dispatchToasterMock = jest.fn();
displayErrorToast('Title', 'message', dispatchToasterMock);
expect(dispatchToasterMock.mock.calls[0][0]).toEqual(mockToast);
});
});

describe('setupEmbeddablesAPI', () => {
test('attaches SIEM_FILTER_ACTION, and detaches extra UI actions', () => {
const applyFilterMock = jest.fn();
setupEmbeddablesAPI(applyFilterMock);
expect(start.registerAction).toHaveBeenCalledTimes(1);
expect(start.detachAction).toHaveBeenCalledTimes(3);
});
});

describe('createEmbeddable', () => {
test('attaches refresh action', async () => {
const setQueryMock = jest.fn();
await createEmbeddable([], '', 0, 0, setQueryMock);
expect(setQueryMock).toHaveBeenCalledTimes(1);
});
});
});
Loading

0 comments on commit b143a66

Please sign in to comment.