Skip to content

Commit

Permalink
[SIEM] Implement NP Plugin Setup (#54030)
Browse files Browse the repository at this point in the history
* Set up our react app in the NP way

* Defines the setup() method for our UI plugin
* Renders the app in the NP way within our setup() method
* Defines a legacy file that invokes the plugin manually

Things seem to be mostly working; the app mounts with no immediate
errors, at least.

* Move files into NP structure

Our plugin function and class are both direct children of siem/public.
The app folder contains both our React app and the function to render
it.

* Register SIEM in the feature catalogue via NP format

Unfortunately, this can't live in the plugin for now because it doesn't
get invoked when we need it. For now, it's going to live in the same
spot, and once we're a real NP plugin we can move it.

* Eliminate usage of timezoneBrowser UI setting

This seems to be redundant with dateFormat:tz except that it always
returns a real timezone, not just a preference. By wrapping that logic
in our own hook, useTimeZone, we can remove this weird usage and stick
to the standard dateFormat and dateFormat:tz.

* Clean up tests for FormattedDate components

Mocks our simpler wrapping hooks rather than the entire UI Settings
module.

* Remove remaining uses of UI Settings mocks

These remaining tests can mock settings directly, or otherwise were
misusing the settings mocks to retrieve assertion values.

* Remove unnecessary intermediate `describe` blocks

They were not adding any information to the tests.

* Remove use of kibana version in client requests

We were previously passing this version all over the place for the sake
of our framework-specific request header. The sole advantage of supplying
such a header is that the client will receive an informative error modal
in the case of a version mismatch between the client and server.

We can successfully perform these requests with the `kbn-xsrf` header
instead. Long-term, we can use core.http.fetch to perform the requests
and auto-populate the version header, but it would be nicer to abstract
those requests to the framework level rather than threading the HTTP
client throughout the application.

* Remove newly added uses of kbnVersion

These happened on master in the meantime.

* Use helper to generate test assertion

Allows us to change the implementation of the empty string without
breaking the test.

* Remove guard from date formatting component

We're always going to get back usable values from these hooks; while the
user can unset the dateFormat in their settings, we'll still get an
empty string which is effectively the same as no formatting (as
evidenced in the tests).

* Remove default from byte formatting component

If the user has deleted this default, they presumably meant to do so and
we shouldn't supersede it.

* Refactor bytes formatting to allow use in our charts

We need a formatting function to use with our charts, so this splits out
a hook from the original react component, allowing our charts to be
formatted as specified in the user's UI settings.

* Refer to our constant for APP_ID

* Explicit return values for some UI Settings hooks

This forces accidental changes to the return value to be explicit.

* Remove use of ui/chrome in request header

This is an unnecessary use: kibana works the same no matter what
contents the `kbn-xsrf` header contains (as long as it's there).

* Mock UI Settings values in our TestProvider

When using our TestProvider components, we were previously relying on
platform's UISettings mocks instead of our own, more comprehensive ones.
This worked for the most part, and when we needed real settings we would
mock the UI Settings client manually.

When we removed some app code that defaulted UI Settings values when the
client did not return a value, tests that used TestProviders but also
relied on those defaults broke. This adds that behavior back,
and obviates the need for manual calls to jest.mock except when we're a)
not using TestProviders but b) overriding the platform mocks.

Also removes some of those unneeded uses.

* Remove unused import

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
rylnd and elasticmachine authored Jan 7, 2020
1 parent ae1fac4 commit b7a534b
Show file tree
Hide file tree
Showing 74 changed files with 431 additions and 743 deletions.
2 changes: 0 additions & 2 deletions x-pack/legacy/plugins/siem/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ export const DEFAULT_SEARCH_AFTER_PAGE_SIZE = 100;
export const DEFAULT_ANOMALY_SCORE = 'siem:defaultAnomalyScore';
export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000;
export const DEFAULT_SCALE_DATE_FORMAT = 'dateFormat:scaled';
export const DEFAULT_KBN_VERSION = 'kbnVersion';
export const DEFAULT_TIMEZONE_BROWSER = 'timezoneBrowser';
export const DEFAULT_FROM = 'now-24h';
export const DEFAULT_TO = 'now';
export const DEFAULT_INTERVAL_PAUSE = true;
Expand Down
4 changes: 2 additions & 2 deletions x-pack/legacy/plugins/siem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { resolve } from 'path';
import { Server } from 'hapi';
import { Root } from 'joi';

import { PluginInitializerContext } from 'src/core/server';
import { PluginInitializerContext } from '../../../../src/core/server';
import { plugin } from './server';
import { savedObjectMappings } from './server/saved_objects';

Expand Down Expand Up @@ -43,7 +43,7 @@ export const siem = (kibana: any) => {
description: i18n.translate('xpack.siem.securityDescription', {
defaultMessage: 'Explore your SIEM App',
}),
main: 'plugins/siem/app',
main: 'plugins/siem/legacy',
euiIconType: 'securityAnalyticsApp',
title: APP_NAME,
listed: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { DEFAULT_DARK_MODE } from '../../common/constants';
import { ErrorToastDispatcher } from '../components/error_toast_dispatcher';
import { compose } from '../lib/compose/kibana_compose';
import { AppFrontendLibs, AppApolloClient } from '../lib/lib';
import { StartCore, StartPlugins } from './plugin';
import { CoreStart, StartPlugins } from '../plugin';
import { PageRouter } from '../routes';
import { createStore } from '../store';
import { GlobalToaster, ManageGlobalToaster } from '../components/toasters';
Expand Down Expand Up @@ -89,10 +89,8 @@ const StartAppComponent: FC<AppFrontendLibs> = libs => {

const StartApp = memo(StartAppComponent);

export const ROOT_ELEMENT_ID = 'react-siem-root';

interface SiemAppComponentProps {
core: StartCore;
core: CoreStart;
plugins: StartPlugins;
}

Expand Down
20 changes: 20 additions & 0 deletions x-pack/legacy/plugins/siem/public/app/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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 React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';

import { CoreStart, StartPlugins, AppMountParameters } from '../plugin';
import { SiemApp } from './app';

export const renderApp = (
core: CoreStart,
plugins: StartPlugins,
{ element }: AppMountParameters
) => {
render(<SiemApp core={core} plugins={plugins} />, element);
return () => unmountComponentAtNode(element);
};
18 changes: 0 additions & 18 deletions x-pack/legacy/plugins/siem/public/apps/index.ts

This file was deleted.

59 changes: 0 additions & 59 deletions x-pack/legacy/plugins/siem/public/apps/plugin.tsx

This file was deleted.

1 change: 0 additions & 1 deletion x-pack/legacy/plugins/siem/public/apps/template.html

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { getOr, get, isNull, isNumber } from 'lodash/fp';
import { AutoSizer } from '../auto_sizer';
import { ChartPlaceHolder } from './chart_place_holder';
import { useTimeZone } from '../../hooks';
import {
chartDefaultSettings,
ChartSeriesConfigs,
Expand All @@ -26,7 +27,6 @@ import {
getChartWidth,
WrappedByAutoSizer,
useTheme,
useBrowserTimeZone,
} from './common';

// custom series styles: https://ela.st/areachart-styling
Expand Down Expand Up @@ -71,7 +71,7 @@ export const AreaChartBaseComponent = ({
configs?: ChartSeriesConfigs | undefined;
}) => {
const theme = useTheme();
const timeZone = useBrowserTimeZone();
const timeZone = useTimeZone();
const xTickFormatter = get('configs.axis.xTickFormatter', chartConfigs);
const yTickFormatter = get('configs.axis.yTickFormatter', chartConfigs);
const xAxisId = `group-${data[0].key}-x`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import React from 'react';
import { Chart, BarSeries, Axis, Position, ScaleType, Settings } from '@elastic/charts';
import { getOr, get, isNumber } from 'lodash/fp';
import { useTimeZone } from '../../hooks';
import { AutoSizer } from '../auto_sizer';
import { ChartPlaceHolder } from './chart_place_holder';
import {
Expand All @@ -17,7 +18,6 @@ import {
getChartHeight,
getChartWidth,
WrappedByAutoSizer,
useBrowserTimeZone,
useTheme,
} from './common';

Expand All @@ -44,7 +44,7 @@ export const BarChartBaseComponent = ({
configs?: ChartSeriesConfigs | undefined;
}) => {
const theme = useTheme();
const timeZone = useBrowserTimeZone();
const timeZone = useTimeZone();
const xTickFormatter = get('configs.axis.xTickFormatter', chartConfigs);
const yTickFormatter = get('configs.axis.yTickFormatter', chartConfigs);
const tickSize = getOr(0, 'configs.axis.tickSize', chartConfigs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ import {
SettingsSpecProps,
TickFormatter,
} from '@elastic/charts';
import moment from 'moment-timezone';
import styled from 'styled-components';
import { useUiSetting } from '../../lib/kibana';
import { DEFAULT_DATE_FORMAT_TZ, DEFAULT_DARK_MODE } from '../../../common/constants';
import { DEFAULT_DARK_MODE } from '../../../common/constants';

export const defaultChartHeight = '100%';
export const defaultChartWidth = '100%';
Expand Down Expand Up @@ -108,11 +107,6 @@ export const chartDefaultSettings = {
debug: false,
};

export const useBrowserTimeZone = () => {
const kibanaTimezone = useUiSetting<string>(DEFAULT_DATE_FORMAT_TZ);
return kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone;
};

export const getChartHeight = (customHeight?: number, autoSizerHeight?: number): string => {
const height = customHeight || autoSizerHeight;
return height ? `${height}px` : defaultChartHeight;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import { mockBrowserFields } from '../../containers/source/mock';
import { defaultHeaders } from '../../mock/header';
import { useMountAppended } from '../../utils/use_mount_appended';

jest.mock('../../lib/kibana');

describe('EventFieldsBrowser', () => {
const mount = useMountAppended();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import { mockBrowserFields } from '../../containers/source/mock';
import { eventsDefaultModel } from './default_model';
import { useMountAppended } from '../../utils/use_mount_appended';

jest.mock('../../lib/kibana');

const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock;
jest.mock('../../containers/detection_engine/rules/fetch_index_patterns');
mockUseFetchIndexPatterns.mockImplementation(() => [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/f
import { mockBrowserFields } from '../../containers/source/mock';
import { eventsDefaultModel } from './default_model';

jest.mock('../../lib/kibana');

const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock;
jest.mock('../../containers/detection_engine/rules/fetch_index_patterns');
mockUseFetchIndexPatterns.mockImplementation(() => [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ const testFlyoutHeight = 980;
const testWidth = 640;
const usersViewing = ['elastic'];

jest.mock('../../../lib/kibana');

describe('Pane', () => {
test('renders correctly against snapshot', () => {
const EmptyComponent = shallow(
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,58 +8,49 @@ import { mount, shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import React from 'react';

import { mockFrameworks, getMockKibanaUiSetting } from '../../mock';
import { useUiSetting$ } from '../../lib/kibana';

import { PreferenceFormattedBytesComponent } from '.';

jest.mock('../../lib/kibana');
const mockUseUiSetting$ = useUiSetting$ as jest.Mock;

describe('formatted_bytes', () => {
describe('PreferenceFormattedBytes', () => {
describe('rendering', () => {
beforeEach(() => {
mockUseUiSetting$.mockClear();
});
const DEFAULT_BYTES_FORMAT_VALUE = '0,0.[0]b'; // kibana's default for this setting
const bytes = '2806422';

const bytes = '2806422';
describe('PreferenceFormattedBytes', () => {
test('renders correctly against snapshot', () => {
mockUseUiSetting$.mockImplementation(() => [DEFAULT_BYTES_FORMAT_VALUE]);
const wrapper = shallow(<PreferenceFormattedBytesComponent value={bytes} />);

test('renders correctly against snapshot', () => {
mockUseUiSetting$.mockImplementation(
getMockKibanaUiSetting(mockFrameworks.default_browser)
);
const wrapper = shallow(<PreferenceFormattedBytesComponent value={bytes} />);
expect(toJson(wrapper)).toMatchSnapshot();
});
expect(toJson(wrapper)).toMatchSnapshot();
});

test('it renders bytes to Numeral formatting when no format setting exists', () => {
mockUseUiSetting$.mockImplementation(() => [null]);
const wrapper = mount(<PreferenceFormattedBytesComponent value={bytes} />);

expect(wrapper.text()).toEqual('2,806,422');
});

test('it renders bytes to hardcoded format when no configuration exists', () => {
mockUseUiSetting$.mockImplementation(() => [null]);
const wrapper = mount(<PreferenceFormattedBytesComponent value={bytes} />);
expect(wrapper.text()).toEqual('2.7MB');
});
test('it renders bytes according to the default format', () => {
mockUseUiSetting$.mockImplementation(() => [DEFAULT_BYTES_FORMAT_VALUE]);
const wrapper = mount(<PreferenceFormattedBytesComponent value={bytes} />);

test('it renders bytes according to the default format', () => {
mockUseUiSetting$.mockImplementation(
getMockKibanaUiSetting(mockFrameworks.default_browser)
);
const wrapper = mount(<PreferenceFormattedBytesComponent value={bytes} />);
expect(wrapper.text()).toEqual('2.7MB');
});
expect(wrapper.text()).toEqual('2.7MB');
});

test('it renders bytes supplied as a number according to the default format', () => {
mockUseUiSetting$.mockImplementation(() => [DEFAULT_BYTES_FORMAT_VALUE]);
const wrapper = mount(<PreferenceFormattedBytesComponent value={+bytes} />);

expect(wrapper.text()).toEqual('2.7MB');
});

test('it renders bytes supplied as a number according to the default format', () => {
mockUseUiSetting$.mockImplementation(
getMockKibanaUiSetting(mockFrameworks.default_browser)
);
const wrapper = mount(<PreferenceFormattedBytesComponent value={+bytes} />);
expect(wrapper.text()).toEqual('2.7MB');
});
test('it renders bytes according to new format', () => {
mockUseUiSetting$.mockImplementation(() => ['0b']);
const wrapper = mount(<PreferenceFormattedBytesComponent value={bytes} />);

test('it renders bytes according to new format', () => {
mockUseUiSetting$.mockImplementation(getMockKibanaUiSetting(mockFrameworks.bytes_short));
const wrapper = mount(<PreferenceFormattedBytesComponent value={bytes} />);
expect(wrapper.text()).toEqual('3MB');
});
});
expect(wrapper.text()).toEqual('3MB');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,22 @@ import numeral from '@elastic/numeral';
import { DEFAULT_BYTES_FORMAT } from '../../../common/constants';
import { useUiSetting$ } from '../../lib/kibana';

export const PreferenceFormattedBytesComponent = ({ value }: { value: string | number }) => {
type Bytes = string | number;

export const formatBytes = (value: Bytes, format: string) => {
return numeral(value).format(format);
};

export const useFormatBytes = () => {
const [bytesFormat] = useUiSetting$<string>(DEFAULT_BYTES_FORMAT);
return (
<>{bytesFormat ? numeral(value).format(bytesFormat) : numeral(value).format('0,0.[0]b')}</>
);

return (value: Bytes) => formatBytes(value, bytesFormat);
};

export const PreferenceFormattedBytesComponent = ({ value }: { value: Bytes }) => (
<>{useFormatBytes()(value)}</>
);

PreferenceFormattedBytesComponent.displayName = 'PreferenceFormattedBytesComponent';

export const PreferenceFormattedBytes = React.memo(PreferenceFormattedBytesComponent);
Expand Down

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

Loading

0 comments on commit b7a534b

Please sign in to comment.