From 37d18a7d58a96bc033e6177dde9c510d55c028b5 Mon Sep 17 00:00:00 2001 From: DziyanaDzeraviankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Mon, 2 Mar 2020 12:22:39 +0300 Subject: [PATCH 01/26] [data] Clean up QueryStringInput unit tests (#58704) Co-authored-by: Elastic Machine --- .../language_switcher.test.tsx.snap | 593 --- .../query_string_input.test.tsx.snap | 3919 ----------------- .../language_switcher.test.tsx | 11 +- .../query_bar_top_row.test.tsx | 2 +- .../query_string_input/query_bar_top_row.tsx | 15 +- .../query_string_input.test.tsx | 10 +- .../query_string_input/query_string_input.tsx | 8 +- 7 files changed, 21 insertions(+), 4537 deletions(-) delete mode 100644 src/plugins/data/public/ui/query_string_input/__snapshots__/language_switcher.test.tsx.snap delete mode 100644 src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/language_switcher.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/language_switcher.test.tsx.snap deleted file mode 100644 index 6432f8049641a..0000000000000 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/language_switcher.test.tsx.snap +++ /dev/null @@ -1,593 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LanguageSwitcher should toggle off if language is lucene 1`] = ` - - - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - withTitle={true} - > - -
-
- - - -
-
-
-
-
-
-`; - -exports[`LanguageSwitcher should toggle on if language is kuery 1`] = ` - - - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - withTitle={true} - > - -
-
- - - -
-
-
-
-
-
-`; diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap deleted file mode 100644 index 58f00ff9ed657..0000000000000 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ /dev/null @@ -1,3919 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true 1`] = ` - - - - - - - - -
-
-
- - } - aria-autocomplete="list" - aria-label="Start typing to search and filter the test page" - autoComplete="off" - autoFocus={false} - data-test-subj="queryInput" - fullWidth={true} - inputRef={[Function]} - onChange={[Function]} - onClick={[Function]} - onKeyDown={[Function]} - onKeyUp={[Function]} - placeholder="Search" - role="textbox" - spellCheck={false} - type="text" - value="response:200" - > - - } - fullWidth={true} - > -
-
- - - - -
- - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - withTitle={true} - > - -
-
- - - -
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-`; - -exports[`QueryStringInput Should pass the query language to the language switcher 1`] = ` - - - - - - - - -
-
-
- - } - aria-autocomplete="list" - aria-label="Start typing to search and filter the test page" - autoComplete="off" - autoFocus={true} - data-test-subj="queryInput" - fullWidth={true} - inputRef={[Function]} - onChange={[Function]} - onClick={[Function]} - onKeyDown={[Function]} - onKeyUp={[Function]} - placeholder="Search" - role="textbox" - spellCheck={false} - type="text" - value="response:200" - > - - } - fullWidth={true} - > -
-
- - - - -
- - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - withTitle={true} - > - -
-
- - - -
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-`; - -exports[`QueryStringInput Should render the given query 1`] = ` - - - - - - - - -
-
-
- - } - aria-autocomplete="list" - aria-label="Start typing to search and filter the test page" - autoComplete="off" - autoFocus={true} - data-test-subj="queryInput" - fullWidth={true} - inputRef={[Function]} - onChange={[Function]} - onClick={[Function]} - onKeyDown={[Function]} - onKeyUp={[Function]} - placeholder="Search" - role="textbox" - spellCheck={false} - type="text" - value="response:200" - > - - } - fullWidth={true} - > -
-
- - - - -
- - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - withTitle={true} - > - -
-
- - - -
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-`; diff --git a/src/plugins/data/public/ui/query_string_input/language_switcher.test.tsx b/src/plugins/data/public/ui/query_string_input/language_switcher.test.tsx index e3ec5212abfd2..f8f576c4b6e97 100644 --- a/src/plugins/data/public/ui/query_string_input/language_switcher.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/language_switcher.test.tsx @@ -22,6 +22,7 @@ import { QueryLanguageSwitcher } from './language_switcher'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; import { coreMock } from '../../../../../core/public/mocks'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { EuiButtonEmpty, EuiPopover } from '@elastic/eui'; const startMock = coreMock.createStart(); describe('LanguageSwitcher', () => { @@ -47,8 +48,9 @@ describe('LanguageSwitcher', () => { }, }) ); - - expect(component).toMatchSnapshot(); + component.find(EuiButtonEmpty).simulate('click'); + expect(component.find(EuiPopover).prop('isOpen')).toBe(true); + expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeFalsy(); }); it('should toggle on if language is kuery', () => { @@ -60,7 +62,8 @@ describe('LanguageSwitcher', () => { }, }) ); - - expect(component).toMatchSnapshot(); + component.find(EuiButtonEmpty).simulate('click'); + expect(component.find(EuiPopover).prop('isOpen')).toBe(true); + expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeTruthy(); }); }); diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx index 70d0c96b4733f..f579adbc0c7e2 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx @@ -104,7 +104,7 @@ function wrapQueryBarTopRowInContext(testProps: any) { return ( - + ); diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index ad9c8401389fa..433cb652ee5ce 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -20,6 +20,8 @@ import dateMath from '@elastic/datemath'; import classNames from 'classnames'; import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; + import { EuiButton, EuiFlexGroup, @@ -31,7 +33,7 @@ import { } from '@elastic/eui'; // @ts-ignore import { EuiSuperUpdateButton, OnRefreshProps } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { Toast } from 'src/core/public'; import { IDataPluginServices, IIndexPattern, TimeRange, TimeHistoryContract, Query } from '../..'; import { useKibana, toMountPoint } from '../../../../kibana_react/public'; @@ -48,7 +50,6 @@ interface Props { disableAutoFocus?: boolean; screenTitle?: string; indexPatterns?: Array; - intl: InjectedIntl; isLoading?: boolean; prepend?: React.ComponentProps['prepend']; showQueryInput?: boolean; @@ -64,7 +65,7 @@ interface Props { timeHistory?: TimeHistoryContract; } -function QueryBarTopRowUI(props: Props) { +export function QueryBarTopRow(props: Props) { const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); const kibana = useKibana(); @@ -285,7 +286,6 @@ function QueryBarTopRowUI(props: Props) { function handleLuceneSyntaxWarning() { if (!props.query) return; - const { intl } = props; const { query, language } = props.query; if ( language === 'kuery' && @@ -294,8 +294,7 @@ function QueryBarTopRowUI(props: Props) { doesKueryExpressionHaveLuceneSyntaxError(query) ) { const toast = notifications!.toasts.addWarning({ - title: intl.formatMessage({ - id: 'data.query.queryBar.luceneSyntaxWarningTitle', + title: i18n.translate('data.query.queryBar.luceneSyntaxWarningTitle', { defaultMessage: 'Lucene syntax warning', }), text: toMountPoint( @@ -357,10 +356,8 @@ function QueryBarTopRowUI(props: Props) { ); } -QueryBarTopRowUI.defaultProps = { +QueryBarTopRow.defaultProps = { showQueryInput: true, showDatePicker: true, showAutoRefreshOnly: false, }; - -export const QueryBarTopRow = injectI18n(QueryBarTopRowUI); diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx index 4435bd87cd2d7..738c9cfb39398 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx @@ -102,8 +102,8 @@ describe('QueryStringInput', () => { indexPatterns: [stubIndexPatternWithFields], }) ); - - expect(component).toMatchSnapshot(); + expect(component.find(EuiFieldText).props().value).toBe(kqlQuery.query); + expect(component.find(QueryLanguageSwitcher).prop('language')).toBe(kqlQuery.language); }); it('Should pass the query language to the language switcher', () => { @@ -114,8 +114,7 @@ describe('QueryStringInput', () => { indexPatterns: [stubIndexPatternWithFields], }) ); - - expect(component).toMatchSnapshot(); + expect(component.find(QueryLanguageSwitcher).prop('language')).toBe(luceneQuery.language); }); it('Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true', () => { @@ -127,8 +126,7 @@ describe('QueryStringInput', () => { disableAutoFocus: true, }) ); - - expect(component).toMatchSnapshot(); + expect(component.find(EuiFieldText).prop('autoFocus')).toBeFalsy(); }); it('Should create a unique PersistedLog based on the appName and query language', () => { diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index f1f055160a3ca..018c2927031d0 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -31,7 +31,7 @@ import { EuiLink, } from '@elastic/eui'; -import { InjectedIntl, injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { debounce, compact, isEqual } from 'lodash'; import { Toast } from 'src/core/public'; import { IDataPluginServices, IIndexPattern, SuggestionsComponent, Query } from '../..'; @@ -44,7 +44,6 @@ import { PersistedLog, getQueryLog, matchPairs, toUser, fromUser } from '../../q interface Props { kibana: KibanaReactContextValue; - intl: InjectedIntl; indexPatterns: Array; query: Query; disableAutoFocus?: boolean; @@ -356,8 +355,7 @@ export class QueryStringInputUI extends Component { if (notifications && docLinks) { const toast = notifications.toasts.add({ - title: this.props.intl.formatMessage({ - id: 'data.query.queryBar.KQLNestedQuerySyntaxInfoTitle', + title: i18n.translate('data.query.queryBar.KQLNestedQuerySyntaxInfoTitle', { defaultMessage: 'KQL nested query syntax', }), text: toMountPoint( @@ -584,4 +582,4 @@ export class QueryStringInputUI extends Component { } } -export const QueryStringInput = injectI18n(withKibana(QueryStringInputUI)); +export const QueryStringInput = withKibana(QueryStringInputUI); From ac5e7aa81ee5cbe0074398500bf7c1fef2f5d23e Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 2 Mar 2020 11:08:19 +0100 Subject: [PATCH 02/26] [Upgrade Assistant] Remove "boom" from reindex service (#58715) * Removed Boom from reindex-service The reindex service had logic inside it for mapping errors to HTTP. This has been moved to the route handlers and also removed Boom. There is one more instance of Boom use inside of reindex-actions but that comes from Saved Objects which should probably be removed at a later stage. * Fix import path Specify the full relative import path to the kibana's core server folder * Remove unnecessary if statement Co-authored-by: Elastic Machine --- .../server/lib/reindexing/error.ts | 35 +++++++++++ .../server/lib/reindexing/error_symbols.ts | 15 +++++ .../server/lib/reindexing/reindex_service.ts | 24 ++++--- .../server/routes/reindex_indices.ts | 62 +++++++++++++------ 4 files changed, 106 insertions(+), 30 deletions(-) create mode 100644 x-pack/plugins/upgrade_assistant/server/lib/reindexing/error.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/lib/reindexing/error_symbols.ts diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error.ts new file mode 100644 index 0000000000000..b7bc197fbd162 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error.ts @@ -0,0 +1,35 @@ +/* + * 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 { + AccessForbidden, + IndexNotFound, + CannotCreateIndex, + ReindexTaskCannotBeDeleted, + ReindexTaskFailed, + ReindexAlreadyInProgress, + MultipleReindexJobsFound, +} from './error_symbols'; + +export class ReindexError extends Error { + constructor(message: string, public readonly symbol: symbol) { + super(message); + } +} + +export const createErrorFactory = (symbol: symbol) => (message: string) => { + return new ReindexError(message, symbol); +}; + +export const error = { + indexNotFound: createErrorFactory(IndexNotFound), + accessForbidden: createErrorFactory(AccessForbidden), + cannotCreateIndex: createErrorFactory(CannotCreateIndex), + reindexTaskFailed: createErrorFactory(ReindexTaskFailed), + reindexTaskCannotBeDeleted: createErrorFactory(ReindexTaskCannotBeDeleted), + reindexAlreadyInProgress: createErrorFactory(ReindexAlreadyInProgress), + multipleReindexJobsFound: createErrorFactory(MultipleReindexJobsFound), +}; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error_symbols.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error_symbols.ts new file mode 100644 index 0000000000000..9e49d280d1be2 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error_symbols.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ + +export const AccessForbidden = Symbol('AccessForbidden'); +export const IndexNotFound = Symbol('IndexNotFound'); +export const CannotCreateIndex = Symbol('CannotCreateIndex'); + +export const ReindexTaskFailed = Symbol('ReindexTaskFailed'); +export const ReindexTaskCannotBeDeleted = Symbol('ReindexTaskCannotBeDeleted'); +export const ReindexAlreadyInProgress = Symbol('ReindexAlreadyInProgress'); + +export const MultipleReindexJobsFound = Symbol('MultipleReindexJobsFound'); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts index 8f1df5b34372b..b274743bdf279 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts @@ -3,8 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import Boom from 'boom'; import { APICaller, Logger } from 'src/core/server'; import { first } from 'rxjs/operators'; @@ -24,6 +22,8 @@ import { import { ReindexActions } from './reindex_actions'; import { LicensingPluginSetup } from '../../../../licensing/server'; +import { error } from './error'; + const VERSION_REGEX = new RegExp(/^([1-9]+)\.([0-9]+)\.([0-9]+)/); const ML_INDICES = ['.ml-state', '.ml-anomalies', '.ml-config']; const WATCHER_INDICES = ['.watches', '.triggered-watches']; @@ -284,7 +284,7 @@ export const reindexServiceFactory = ( const flatSettings = await actions.getFlatSettings(indexName); if (!flatSettings) { - throw Boom.notFound(`Index ${indexName} does not exist.`); + throw error.indexNotFound(`Index ${indexName} does not exist.`); } const { settings, mappings } = transformFlatSettings(flatSettings); @@ -298,7 +298,7 @@ export const reindexServiceFactory = ( }); if (!createIndex.acknowledged) { - throw Boom.badImplementation(`Index could not be created: ${newIndexName}`); + throw error.cannotCreateIndex(`Index could not be created: ${newIndexName}`); } return actions.updateReindexOp(reindexOp, { @@ -363,7 +363,7 @@ export const reindexServiceFactory = ( if (taskResponse.task.status.created < count) { // Include the entire task result in the error message. This should be guaranteed // to be JSON-serializable since it just came back from Elasticsearch. - throw Boom.badData(`Reindexing failed: ${JSON.stringify(taskResponse)}`); + throw error.reindexTaskFailed(`Reindexing failed: ${JSON.stringify(taskResponse)}`); } // Update the status @@ -380,7 +380,7 @@ export const reindexServiceFactory = ( }); if (deleteTaskResp.result !== 'deleted') { - throw Boom.badImplementation(`Could not delete reindexing task ${taskId}`); + throw error.reindexTaskCannotBeDeleted(`Could not delete reindexing task ${taskId}`); } return reindexOp; @@ -414,7 +414,7 @@ export const reindexServiceFactory = ( }); if (!aliasResponse.acknowledged) { - throw Boom.badImplementation(`Index aliases could not be created.`); + throw error.cannotCreateIndex(`Index aliases could not be created.`); } return actions.updateReindexOp(reindexOp, { @@ -520,7 +520,7 @@ export const reindexServiceFactory = ( async createReindexOperation(indexName: string) { const indexExists = await callAsUser('indices.exists', { index: indexName }); if (!indexExists) { - throw Boom.notFound(`Index ${indexName} does not exist in this cluster.`); + throw error.indexNotFound(`Index ${indexName} does not exist in this cluster.`); } const existingReindexOps = await actions.findReindexOperations(indexName); @@ -533,7 +533,9 @@ export const reindexServiceFactory = ( // Delete the existing one if it failed or was cancelled to give a chance to retry. await actions.deleteReindexOp(existingOp); } else { - throw Boom.badImplementation(`A reindex operation already in-progress for ${indexName}`); + throw error.reindexAlreadyInProgress( + `A reindex operation already in-progress for ${indexName}` + ); } } @@ -547,7 +549,9 @@ export const reindexServiceFactory = ( if (findResponse.total === 0) { return null; } else if (findResponse.total > 1) { - throw Boom.badImplementation(`More than one reindex operation found for ${indexName}`); + throw error.multipleReindexJobsFound( + `More than one reindex operation found for ${indexName}` + ); } return findResponse.saved_objects[0]; diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts index a910145474061..72c2f2c29b72e 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts @@ -5,7 +5,12 @@ */ import { schema } from '@kbn/config-schema'; -import { Logger, ElasticsearchServiceSetup, SavedObjectsClient } from 'src/core/server'; +import { + Logger, + ElasticsearchServiceSetup, + SavedObjectsClient, + kibanaResponseFactory, +} from '../../../../../src/core/server'; import { ReindexStatus } from '../../common/types'; import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; import { reindexServiceFactory, ReindexWorker } from '../lib/reindexing'; @@ -13,6 +18,16 @@ import { CredentialStore } from '../lib/reindexing/credential_store'; import { reindexActionsFactory } from '../lib/reindexing/reindex_actions'; import { RouteDependencies } from '../types'; import { LicensingPluginSetup } from '../../../licensing/server'; +import { ReindexError } from '../lib/reindexing/error'; +import { + AccessForbidden, + IndexNotFound, + CannotCreateIndex, + ReindexAlreadyInProgress, + ReindexTaskCannotBeDeleted, + ReindexTaskFailed, + MultipleReindexJobsFound, +} from '../lib/reindexing/error_symbols'; interface CreateReindexWorker { logger: Logger; @@ -33,6 +48,29 @@ export function createReindexWorker({ return new ReindexWorker(savedObjects, credentialStore, adminClient, logger, licensing); } +const mapAnyErrorToKibanaHttpResponse = (e: any) => { + if (e instanceof ReindexError) { + switch (e.symbol) { + case AccessForbidden: + return kibanaResponseFactory.forbidden({ body: e.message }); + case IndexNotFound: + return kibanaResponseFactory.notFound({ body: e.message }); + case CannotCreateIndex: + case ReindexTaskCannotBeDeleted: + return kibanaResponseFactory.internalError({ body: e.message }); + case ReindexTaskFailed: + // Bad data + return kibanaResponseFactory.customError({ body: e.message, statusCode: 422 }); + case ReindexAlreadyInProgress: + case MultipleReindexJobsFound: + return kibanaResponseFactory.badRequest({ body: e.message }); + default: + // nothing matched + } + } + return kibanaResponseFactory.internalError({ body: e }); +}; + export function registerReindexIndicesRoutes( { credentialStore, router, licensing, log }: RouteDependencies, getWorker: () => ReindexWorker @@ -94,7 +132,7 @@ export function registerReindexIndicesRoutes( return response.ok({ body: reindexOp.attributes }); } catch (e) { - return response.internalError({ body: e }); + return mapAnyErrorToKibanaHttpResponse(e); } } ) @@ -150,15 +188,7 @@ export function registerReindexIndicesRoutes( }, }); } catch (e) { - if (!e.isBoom) { - return response.internalError({ body: e }); - } - return response.customError({ - body: { - message: e.message, - }, - statusCode: e.statusCode, - }); + return mapAnyErrorToKibanaHttpResponse(e); } } ) @@ -201,15 +231,7 @@ export function registerReindexIndicesRoutes( return response.ok({ body: { acknowledged: true } }); } catch (e) { - if (!e.isBoom) { - return response.internalError({ body: e }); - } - return response.customError({ - body: { - message: e.message, - }, - statusCode: e.statusCode, - }); + return mapAnyErrorToKibanaHttpResponse(e); } } ) From d7601cc7431e1fdf9a1d4bc39640b10c897c6f3d Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Mon, 2 Mar 2020 13:15:29 +0100 Subject: [PATCH 03/26] Retry migration operations which fail due to snapshot in progress (#58884) Co-authored-by: Elastic Machine --- .../server/elasticsearch/retry_call_cluster.test.ts | 13 +++++++++++++ src/core/server/elasticsearch/retry_call_cluster.ts | 13 +++---------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/core/server/elasticsearch/retry_call_cluster.test.ts b/src/core/server/elasticsearch/retry_call_cluster.test.ts index b5a5185ab39d9..4f391f0aba34b 100644 --- a/src/core/server/elasticsearch/retry_call_cluster.test.ts +++ b/src/core/server/elasticsearch/retry_call_cluster.test.ts @@ -89,6 +89,19 @@ describe('migrationsRetryCallCluster', () => { }); }); + it('retries ES API calls that rejects with snapshot_in_progress_exception', () => { + expect.assertions(1); + const callEsApi = jest.fn(); + let i = 0; + callEsApi.mockImplementation(() => { + return i++ <= 2 + ? Promise.reject({ body: { error: { type: 'snapshot_in_progress_exception' } } }) + : Promise.resolve('success'); + }); + const retried = migrationsRetryCallCluster(callEsApi, mockLogger.get('mock log'), 1); + return expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); + }); + it('rejects when ES API calls reject with other errors', async () => { expect.assertions(3); const callEsApi = jest.fn(); diff --git a/src/core/server/elasticsearch/retry_call_cluster.ts b/src/core/server/elasticsearch/retry_call_cluster.ts index ea3cc0b90c077..901b801159cb6 100644 --- a/src/core/server/elasticsearch/retry_call_cluster.ts +++ b/src/core/server/elasticsearch/retry_call_cluster.ts @@ -64,7 +64,8 @@ export function migrationsRetryCallCluster( error instanceof esErrors.AuthenticationException || error instanceof esErrors.AuthorizationException || // @ts-ignore - error instanceof esErrors.Gone + error instanceof esErrors.Gone || + error?.body?.error?.type === 'snapshot_in_progress_exception' ); }, timer(delay), @@ -85,15 +86,7 @@ export function migrationsRetryCallCluster( * * @param apiCaller */ - -// TODO: Replace with APICaller from './scoped_cluster_client' once #46668 is merged -export function retryCallCluster( - apiCaller: ( - endpoint: string, - clientParams: Record, - options?: CallAPIOptions - ) => Promise -) { +export function retryCallCluster(apiCaller: APICaller) { return (endpoint: string, clientParams: Record = {}, options?: CallAPIOptions) => { return defer(() => apiCaller(endpoint, clientParams, options)) .pipe( From 810a6b474817a19e19e49d144f17014f16221c8d Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 2 Mar 2020 13:37:20 +0100 Subject: [PATCH 04/26] merge only plain objects (#59011) --- src/core/utils/merge.test.ts | 24 ++++++++++++++++++++++++ src/core/utils/merge.ts | 4 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/core/utils/merge.test.ts b/src/core/utils/merge.test.ts index c857e980dec21..7ef07a83399ac 100644 --- a/src/core/utils/merge.test.ts +++ b/src/core/utils/merge.test.ts @@ -17,6 +17,7 @@ * under the License. */ +// eslint-disable-next-line max-classes-per-file import { merge } from './merge'; describe('merge', () => { @@ -62,6 +63,29 @@ describe('merge', () => { expect(merge({ a: 0 }, { a: 1 }, {})).toEqual({ a: 1 }); }); + test('does not merge class instances', () => { + class Folder { + constructor(public readonly path: string) {} + getPath() { + return this.path; + } + } + class File { + constructor(public readonly content: string) {} + getContent() { + return this.content; + } + } + const folder = new Folder('/etc'); + const file = new File('yolo'); + + const result = merge({}, { content: folder }, { content: file }); + expect(result).toStrictEqual({ + content: file, + }); + expect(result.content.getContent()).toBe('yolo'); + }); + test(`doesn't pollute prototypes`, () => { merge({}, JSON.parse('{ "__proto__": { "foo": "bar" } }')); merge({}, JSON.parse('{ "constructor": { "prototype": { "foo": "bar" } } }')); diff --git a/src/core/utils/merge.ts b/src/core/utils/merge.ts index 8e5d9f4860d95..43878c27b1e19 100644 --- a/src/core/utils/merge.ts +++ b/src/core/utils/merge.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import { isPlainObject } from 'lodash'; /** * Deeply merges two objects, omitting undefined values, and not deeply merging Arrays. * @@ -60,7 +60,7 @@ export function merge>( ) as TReturn; } -const isMergable = (obj: any) => typeof obj === 'object' && obj !== null && !Array.isArray(obj); +const isMergable = (obj: any) => isPlainObject(obj); const mergeObjects = , U extends Record>( baseObj: T, From c9ebeb7cff9a8b1f25623ee4950074f7c7bc24c5 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Mon, 2 Mar 2020 13:07:02 +0000 Subject: [PATCH 05/26] explicit ui exports from data plugin (#57764) * explicit ui exports * Fix imports in data plugin Co-authored-by: Elastic Machine --- src/plugins/data/public/index.ts | 15 ++++++++++++++- .../ui/query_string_input/query_string_input.tsx | 3 ++- .../data/public/ui/search_bar/search_bar.tsx | 4 +--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 5dcf51ecc81eb..a5f4ce2ce3c58 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -286,6 +286,19 @@ export { export { IRequestTypesMap, IResponseTypesMap } from './search'; export * from './search'; +/* + * UI components + */ + +export { + SearchBar, + SearchBarProps, + StatefulSearchBarProps, + FilterBar, + QueryStringInput, + IndexPatternSelect, +} from './ui'; + /** * Types to be shared externally * @public @@ -310,7 +323,7 @@ export { TimefilterContract, TimeHistoryContract, } from './query'; -export * from './ui'; + export { // kbn field types castEsToKbnFieldTypeName, diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 018c2927031d0..a51362d0ba92e 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -34,13 +34,14 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { debounce, compact, isEqual } from 'lodash'; import { Toast } from 'src/core/public'; -import { IDataPluginServices, IIndexPattern, SuggestionsComponent, Query } from '../..'; +import { IDataPluginServices, IIndexPattern, Query } from '../..'; import { QuerySuggestion, QuerySuggestionTypes } from '../../autocomplete'; import { withKibana, KibanaReactContextValue, toMountPoint } from '../../../../kibana_react/public'; import { fetchIndexPatterns } from './fetch_index_patterns'; import { QueryLanguageSwitcher } from './language_switcher'; import { PersistedLog, getQueryLog, matchPairs, toUser, fromUser } from '../../query'; +import { SuggestionsComponent } from '..'; interface Props { kibana: KibanaReactContextValue; diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index 66ad4dfb12e97..5083a1e68c6dd 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -33,12 +33,10 @@ import { IIndexPattern, FilterBar, SavedQuery, - SavedQueryMeta, - SaveQueryForm, - SavedQueryManagementComponent, } from '../..'; import { QueryBarTopRow } from '../query_string_input/query_bar_top_row'; import { SavedQueryAttributes, TimeHistoryContract } from '../../query'; +import { SavedQueryMeta, SavedQueryManagementComponent, SaveQueryForm } from '..'; interface SearchBarInjectedDeps { kibana: KibanaReactContextValue; From b5dd99c4667e4ebf452073eee922c4bd34da6cce Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Mon, 2 Mar 2020 16:50:44 +0300 Subject: [PATCH 06/26] Converted terms_other_bucket_helper to TS. Migrated tests to jest. (#58143) * Converted terms_other_bucket_helper to TS. Migrated tests to jest. * Fixed some remarks * fix PR comments * Fixed tests * Fixed types Co-authored-by: Elastic Machine Co-authored-by: Alexey Antonov --- .../_terms_other_bucket_helper.test.ts} | 218 ++++++++++++------ ...elper.js => _terms_other_bucket_helper.ts} | 135 +++++++---- .../data/public/search/aggs/buckets/terms.ts | 1 - 3 files changed, 233 insertions(+), 121 deletions(-) rename src/legacy/core_plugins/data/public/search/aggs/{__tests__/buckets/_terms_other_bucket_helper.js => buckets/_terms_other_bucket_helper.test.ts} (54%) rename src/legacy/core_plugins/data/public/search/aggs/buckets/{_terms_other_bucket_helper.js => _terms_other_bucket_helper.ts} (65%) diff --git a/src/legacy/core_plugins/data/public/search/aggs/__tests__/buckets/_terms_other_bucket_helper.js b/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts similarity index 54% rename from src/legacy/core_plugins/data/public/search/aggs/__tests__/buckets/_terms_other_bucket_helper.js rename to src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts index 749dad377f2e2..976ab57c00b63 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/__tests__/buckets/_terms_other_bucket_helper.js +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts @@ -17,39 +17,73 @@ * under the License. */ -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; import { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket, -} from '../../buckets/_terms_other_bucket_helper'; -import { start as visualizationsStart } from '../../../../../../../core_plugins/visualizations/public/np_ready/public/legacy'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +} from './_terms_other_bucket_helper'; +import { AggConfigs, CreateAggConfigParams } from '../agg_configs'; +import { BUCKET_TYPES } from './bucket_agg_types'; +import { IBucketAggConfig } from './_bucket_agg_type'; +import { mockDataServices, mockAggTypesRegistry } from '../test_helpers'; -const visConfigSingleTerm = { - type: 'pie', +const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: [ + { + name: 'field', + }, + ], +} as any; + +const singleTerm = { aggs: [ { - type: 'terms', - schema: 'segment', - params: { field: 'machine.os.raw', otherBucket: true, missingBucket: true }, + id: '1', + type: BUCKET_TYPES.TERMS, + params: { + field: { + name: 'machine.os.raw', + indexPattern, + filterable: true, + }, + otherBucket: true, + missingBucket: true, + }, }, ], }; -const visConfigNestedTerm = { - type: 'pie', +const nestedTerm = { aggs: [ { - type: 'terms', - schema: 'segment', - params: { field: 'geo.src', size: 2, otherBucket: false, missingBucket: false }, + id: '1', + type: BUCKET_TYPES.TERMS, + params: { + field: { + name: 'geo.src', + indexPattern, + filterable: true, + }, + size: 2, + otherBucket: false, + missingBucket: false, + }, }, { - type: 'terms', - schema: 'segment', - params: { field: 'machine.os.raw', size: 2, otherBucket: true, missingBucket: true }, + id: '2', + type: BUCKET_TYPES.TERMS, + params: { + field: { + name: 'machine.os.raw', + indexPattern, + filterable: true, + }, + size: 2, + otherBucket: true, + missingBucket: true, + }, }, ], }; @@ -183,28 +217,36 @@ const nestedOtherResponse = { status: 200, }; -describe('Terms Agg Other bucket helper', () => { - let vis; +jest.mock('ui/new_platform'); - function init(aggConfig) { - ngMock.module('kibana'); - ngMock.inject(Private => { - const indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); +describe('Terms Agg Other bucket helper', () => { + const typesRegistry = mockAggTypesRegistry(); + const getAggConfigs = (aggs: CreateAggConfigParams[] = []) => { + return new AggConfigs(indexPattern, [...aggs], { typesRegistry }); + }; - vis = new visualizationsStart.Vis(indexPattern, aggConfig); - }); - } + beforeEach(() => { + mockDataServices(); + }); describe('buildOtherBucketAgg', () => { - it('returns a function', () => { - init(visConfigSingleTerm); - const agg = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[0], singleTermResponse); - expect(agg).to.be.a('function'); + test('returns a function', () => { + const aggConfigs = getAggConfigs(singleTerm.aggs); + const agg = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[0] as IBucketAggConfig, + singleTermResponse + ); + expect(typeof agg).toBe('function'); }); - it('correctly builds query with single terms agg', () => { - init(visConfigSingleTerm); - const agg = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[0], singleTermResponse)(); + test('correctly builds query with single terms agg', () => { + const aggConfigs = getAggConfigs(singleTerm.aggs); + const agg = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[0] as IBucketAggConfig, + singleTermResponse + ); const expectedResponse = { aggs: undefined, filters: { @@ -223,13 +265,19 @@ describe('Terms Agg Other bucket helper', () => { }, }, }; - - expect(agg['other-filter']).to.eql(expectedResponse); + expect(agg).toBeDefined(); + if (agg) { + expect(agg()['other-filter']).toEqual(expectedResponse); + } }); - it('correctly builds query for nested terms agg', () => { - init(visConfigNestedTerm); - const agg = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[1], nestedTermResponse)(); + test('correctly builds query for nested terms agg', () => { + const aggConfigs = getAggConfigs(nestedTerm.aggs); + const agg = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[1] as IBucketAggConfig, + nestedTermResponse + ); const expectedResponse = { 'other-filter': { aggs: undefined, @@ -267,54 +315,84 @@ describe('Terms Agg Other bucket helper', () => { }, }, }; - - expect(agg).to.eql(expectedResponse); + expect(agg).toBeDefined(); + if (agg) { + expect(agg()).toEqual(expectedResponse); + } }); - it('returns false when nested terms agg has no buckets', () => { - init(visConfigNestedTerm); - const agg = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[1], nestedTermResponseNoResults); - expect(agg).to.eql(false); + test('returns false when nested terms agg has no buckets', () => { + const aggConfigs = getAggConfigs(nestedTerm.aggs); + const agg = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[1] as IBucketAggConfig, + nestedTermResponseNoResults + ); + + expect(agg).toEqual(false); }); }); describe('mergeOtherBucketAggResponse', () => { - it('correctly merges other bucket with single terms agg', () => { - init(visConfigSingleTerm); - const otherAggConfig = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[0], singleTermResponse)(); - const mergedResponse = mergeOtherBucketAggResponse( - vis.aggs, - singleTermResponse, - singleOtherResponse, - vis.aggs.aggs[0], - otherAggConfig + test('correctly merges other bucket with single terms agg', () => { + const aggConfigs = getAggConfigs(singleTerm.aggs); + const otherAggConfig = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[0] as IBucketAggConfig, + singleTermResponse ); - expect(mergedResponse.aggregations['1'].buckets[3].key).to.equal('__other__'); + expect(otherAggConfig).toBeDefined(); + if (otherAggConfig) { + const mergedResponse = mergeOtherBucketAggResponse( + aggConfigs, + singleTermResponse, + singleOtherResponse, + aggConfigs.aggs[0] as IBucketAggConfig, + otherAggConfig() + ); + expect(mergedResponse.aggregations['1'].buckets[3].key).toEqual('__other__'); + } }); - it('correctly merges other bucket with nested terms agg', () => { - init(visConfigNestedTerm); - const otherAggConfig = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[1], nestedTermResponse)(); - const mergedResponse = mergeOtherBucketAggResponse( - vis.aggs, - nestedTermResponse, - nestedOtherResponse, - vis.aggs.aggs[1], - otherAggConfig + test('correctly merges other bucket with nested terms agg', () => { + const aggConfigs = getAggConfigs(nestedTerm.aggs); + const otherAggConfig = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[1] as IBucketAggConfig, + nestedTermResponse ); - expect(mergedResponse.aggregations['1'].buckets[1]['2'].buckets[3].key).to.equal('__other__'); + expect(otherAggConfig).toBeDefined(); + if (otherAggConfig) { + const mergedResponse = mergeOtherBucketAggResponse( + aggConfigs, + nestedTermResponse, + nestedOtherResponse, + aggConfigs.aggs[1] as IBucketAggConfig, + otherAggConfig() + ); + + expect(mergedResponse.aggregations['1'].buckets[1]['2'].buckets[3].key).toEqual( + '__other__' + ); + } }); }); describe('updateMissingBucket', () => { - it('correctly updates missing bucket key', () => { - init(visConfigNestedTerm); - const updatedResponse = updateMissingBucket(singleTermResponse, vis.aggs, vis.aggs.aggs[0]); + test('correctly updates missing bucket key', () => { + const aggConfigs = getAggConfigs(nestedTerm.aggs); + const updatedResponse = updateMissingBucket( + singleTermResponse, + aggConfigs, + aggConfigs.aggs[0] as IBucketAggConfig + ); expect( - updatedResponse.aggregations['1'].buckets.find(bucket => bucket.key === '__missing__') - ).to.not.be('undefined'); + updatedResponse.aggregations['1'].buckets.find( + (bucket: Record) => bucket.key === '__missing__' + ) + ).toBeDefined(); }); }); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.js b/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.ts similarity index 65% rename from src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.js rename to src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.ts index ddab360161744..42db37c81eadd 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.js +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.ts @@ -17,21 +17,24 @@ * under the License. */ -import _ from 'lodash'; +import { isNumber, keys, values, find, each, cloneDeep, flatten } from 'lodash'; import { esFilters, esQuery } from '../../../../../../../plugins/data/public'; import { AggGroupNames } from '../agg_groups'; +import { IAggConfigs } from '../agg_configs'; +import { IBucketAggConfig } from './_bucket_agg_type'; /** * walks the aggregation DSL and returns DSL starting at aggregation with id of startFromAggId * @param aggNestedDsl: aggregation config DSL (top level) * @param startFromId: id of an aggregation from where we want to get the nested DSL */ -const getNestedAggDSL = (aggNestedDsl, startFromAggId) => { +const getNestedAggDSL = (aggNestedDsl: Record, startFromAggId: string): any => { if (aggNestedDsl[startFromAggId]) { return aggNestedDsl[startFromAggId]; } - const nestedAggs = _.values(aggNestedDsl); + const nestedAggs: Array> = values(aggNestedDsl); let aggs; + for (let i = 0; i < nestedAggs.length; i++) { if (nestedAggs[i].aggs && (aggs = getNestedAggDSL(nestedAggs[i].aggs, startFromAggId))) { return aggs; @@ -46,27 +49,34 @@ const getNestedAggDSL = (aggNestedDsl, startFromAggId) => { * @param aggWithOtherBucket: AggConfig of the aggregation with other bucket enabled * @param key: key from the other bucket request for a specific other bucket */ -const getAggResultBuckets = (aggConfigs, response, aggWithOtherBucket, key) => { +const getAggResultBuckets = ( + aggConfigs: IAggConfigs, + response: any, + aggWithOtherBucket: IBucketAggConfig, + key: string +) => { const keyParts = key.split('-'); let responseAgg = response; for (const i in keyParts) { if (keyParts[i]) { - const responseAggs = _.values(responseAgg); + const responseAggs: Array> = values(responseAgg); // If you have multi aggs, we cannot just assume the first one is the `other` bucket, // so we need to loop over each agg until we find it. for (let aggId = 0; aggId < responseAggs.length; aggId++) { - const agg = responseAggs[aggId]; - const aggKey = _.keys(responseAgg)[aggId]; - const aggConfig = _.find(aggConfigs.aggs, agg => agg.id === aggKey); - const bucket = _.find(agg.buckets, (bucket, bucketObjKey) => { - const bucketKey = aggConfig - .getKey(bucket, Number.isInteger(bucketObjKey) ? null : bucketObjKey) - .toString(); - return bucketKey === keyParts[i]; - }); - if (bucket) { - responseAgg = bucket; - break; + const aggById = responseAggs[aggId]; + const aggKey = keys(responseAgg)[aggId]; + const aggConfig = find(aggConfigs.aggs, agg => agg.id === aggKey); + if (aggConfig) { + const aggResultBucket = find(aggById.buckets, (bucket, bucketObjKey) => { + const bucketKey = aggConfig + .getKey(bucket, isNumber(bucketObjKey) ? undefined : bucketObjKey) + .toString(); + return bucketKey === keyParts[i]; + }); + if (aggResultBucket) { + responseAgg = aggResultBucket; + break; + } } } } @@ -82,21 +92,20 @@ const getAggResultBuckets = (aggConfigs, response, aggWithOtherBucket, key) => { * @param responseAggs: array of aggregations from response * @param aggId: id of the aggregation with missing bucket */ -const getAggConfigResultMissingBuckets = (responseAggs, aggId) => { +const getAggConfigResultMissingBuckets = (responseAggs: any, aggId: string) => { const missingKey = '__missing__'; - let resultBuckets = []; + let resultBuckets: Array> = []; if (responseAggs[aggId]) { - const matchingBucket = responseAggs[aggId].buckets.find(bucket => bucket.key === missingKey); + const matchingBucket = responseAggs[aggId].buckets.find( + (bucket: Record) => bucket.key === missingKey + ); if (matchingBucket) resultBuckets.push(matchingBucket); return resultBuckets; } - _.each(responseAggs, agg => { + each(responseAggs, agg => { if (agg.buckets) { - _.each(agg.buckets, bucket => { - resultBuckets = [ - ...resultBuckets, - ...getAggConfigResultMissingBuckets(bucket, aggId, missingKey), - ]; + each(agg.buckets, bucket => { + resultBuckets = [...resultBuckets, ...getAggConfigResultMissingBuckets(bucket, aggId)]; }); } }); @@ -110,13 +119,24 @@ const getAggConfigResultMissingBuckets = (responseAggs, aggId) => { * @param key: the key for this specific other bucket * @param otherAgg: AggConfig of the aggregation with other bucket */ -const getOtherAggTerms = (requestAgg, key, otherAgg) => { +const getOtherAggTerms = ( + requestAgg: Record, + key: string, + otherAgg: IBucketAggConfig +) => { return requestAgg['other-filter'].filters.filters[key].bool.must_not - .filter(filter => filter.match_phrase && filter.match_phrase[otherAgg.params.field.name]) - .map(filter => filter.match_phrase[otherAgg.params.field.name]); + .filter( + (filter: Record) => + filter.match_phrase && filter.match_phrase[otherAgg.params.field.name] + ) + .map((filter: Record) => filter.match_phrase[otherAgg.params.field.name]); }; -export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => { +export const buildOtherBucketAgg = ( + aggConfigs: IAggConfigs, + aggWithOtherBucket: IBucketAggConfig, + response: any +) => { const bucketAggs = aggConfigs.aggs.filter(agg => agg.type.type === AggGroupNames.Buckets); const index = bucketAggs.findIndex(agg => agg.id === aggWithOtherBucket.id); const aggs = aggConfigs.toDsl(); @@ -130,6 +150,7 @@ export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => params: { filters: [], }, + enabled: false, }, { addToAggConfigs: false, @@ -145,25 +166,31 @@ export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => let noAggBucketResults = false; // recursively create filters for all parent aggregation buckets - const walkBucketTree = (aggIndex, aggs, aggId, filters, key) => { + const walkBucketTree = ( + aggIndex: number, + aggregations: any, + aggId: string, + filters: any[], + key: string + ) => { // make sure there are actually results for the buckets - if (aggs[aggId].buckets.length < 1) { + if (aggregations[aggId].buckets.length < 1) { noAggBucketResults = true; return; } - const agg = aggs[aggId]; + const agg = aggregations[aggId]; const newAggIndex = aggIndex + 1; const newAgg = bucketAggs[newAggIndex]; const currentAgg = bucketAggs[aggIndex]; if (aggIndex < index) { - _.each(agg.buckets, (bucket, bucketObjKey) => { + each(agg.buckets, (bucket: any, bucketObjKey) => { const bucketKey = currentAgg.getKey( bucket, - Number.isInteger(bucketObjKey) ? null : bucketObjKey + isNumber(bucketObjKey) ? undefined : bucketObjKey ); - const filter = _.cloneDeep(bucket.filters) || currentAgg.createFilter(bucketKey); - const newFilters = _.flatten([...filters, filter]); + const filter = cloneDeep(bucket.filters) || currentAgg.createFilter(bucketKey); + const newFilters = flatten([...filters, filter]); walkBucketTree( newAggIndex, bucket, @@ -177,7 +204,7 @@ export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => if ( !aggWithOtherBucket.params.missingBucket || - agg.buckets.some(bucket => bucket.key === '__missing__') + agg.buckets.some((bucket: { key: string }) => bucket.key === '__missing__') ) { filters.push( esFilters.buildExistsFilter( @@ -188,7 +215,7 @@ export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => } // create not filters for all the buckets - _.each(agg.buckets, bucket => { + each(agg.buckets, bucket => { if (bucket.key === '__missing__') return; const filter = currentAgg.createFilter(bucket.key); filter.meta.negate = true; @@ -214,15 +241,15 @@ export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => }; export const mergeOtherBucketAggResponse = ( - aggsConfig, - response, - otherResponse, - otherAgg, - requestAgg + aggsConfig: IAggConfigs, + response: any, + otherResponse: any, + otherAgg: IBucketAggConfig, + requestAgg: Record ) => { - const updatedResponse = _.cloneDeep(response); - _.each(otherResponse.aggregations['other-filter'].buckets, (bucket, key) => { - if (!bucket.doc_count) return; + const updatedResponse = cloneDeep(response); + each(otherResponse.aggregations['other-filter'].buckets, (bucket, key) => { + if (!bucket.doc_count || key === undefined) return; const bucketKey = key.replace(/^-/, ''); const aggResultBuckets = getAggResultBuckets( aggsConfig, @@ -241,7 +268,11 @@ export const mergeOtherBucketAggResponse = ( bucket.filters = [phraseFilter]; bucket.key = '__other__'; - if (aggResultBuckets.some(bucket => bucket.key === '__missing__')) { + if ( + aggResultBuckets.some( + (aggResultBucket: Record) => aggResultBucket.key === '__missing__' + ) + ) { bucket.filters.push( esFilters.buildExistsFilter(otherAgg.params.field, otherAgg.params.field.indexPattern) ); @@ -251,8 +282,12 @@ export const mergeOtherBucketAggResponse = ( return updatedResponse; }; -export const updateMissingBucket = (response, aggConfigs, agg) => { - const updatedResponse = _.cloneDeep(response); +export const updateMissingBucket = ( + response: any, + aggConfigs: IAggConfigs, + agg: IBucketAggConfig +) => { + const updatedResponse = cloneDeep(response); const aggResultBuckets = getAggConfigResultMissingBuckets(updatedResponse.aggregations, agg.id); aggResultBuckets.forEach(bucket => { bucket.key = '__missing__'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.ts index 0ed44aa876744..8fd95c86d8476 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.ts @@ -39,7 +39,6 @@ import { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket, - // @ts-ignore } from './_terms_other_bucket_helper'; import { Schemas } from '../schemas'; import { AggGroupNames } from '../agg_groups'; From a3be4e22220df8724d7ceb5667f4af37647042ea Mon Sep 17 00:00:00 2001 From: kqualters-elastic <56408403+kqualters-elastic@users.noreply.github.com> Date: Mon, 2 Mar 2020 09:20:21 -0500 Subject: [PATCH 07/26] [Endpoint] add resolver middleware (#58288) * Add resolver middleware * Update types to match events, use sample events in useCamera tests * add predicate to convert alertdata to legacy endpoint event * Use mock event generator in tests * Guard against events not having agent or endpoint fields Co-authored-by: Robert Austin --- x-pack/plugins/endpoint/common/types.ts | 34 +++- .../public/applications/endpoint/index.tsx | 76 ++++--- .../store/alerts/mock_alert_result_list.ts | 1 + .../endpoint/store/alerts/selectors.ts | 23 ++- .../endpoint/view/alerts/index.test.tsx | 21 +- .../endpoint/view/alerts/index.tsx | 9 +- .../endpoint/view/alerts/resolver.tsx | 35 ++++ .../resolver/models/indexed_process_tree.ts | 20 +- .../resolver/models/process_event.test.ts | 10 +- .../resolver/models/process_event.ts | 20 +- .../models/process_event_test_helpers.ts | 43 ++-- .../embeddables/resolver/store/actions.ts | 31 ++- .../data/__snapshots__/graphing.test.ts.snap | 189 +++++++++--------- .../embeddables/resolver/store/data/action.ts | 4 +- .../resolver/store/data/graphing.test.ts | 73 +++---- .../resolver/store/data/reducer.ts | 10 +- .../resolver/store/data/selectors.ts | 14 +- .../embeddables/resolver/store/index.ts | 10 +- .../embeddables/resolver/store/methods.ts | 5 +- .../embeddables/resolver/store/middleware.ts | 45 +++++ .../embeddables/resolver/store/selectors.ts | 5 + .../public/embeddables/resolver/types.ts | 16 +- .../embeddables/resolver/view/index.tsx | 77 ++++--- .../embeddables/resolver/view/panel.tsx | 17 +- .../resolver/view/process_event_dot.tsx | 7 +- .../resolver/view/use_camera.test.tsx | 49 +++-- x-pack/plugins/endpoint/public/plugin.ts | 11 +- .../routes/resolver/queries/children.test.ts | 8 +- .../resolver/queries/related_events.test.ts | 8 +- .../routes/resolver/utils/pagination.ts | 8 +- 30 files changed, 578 insertions(+), 301 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index d3df972290759..6d904fda6f747 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -115,6 +115,10 @@ export type AlertEvent = Immutable<{ score: number; }; }; + process?: { + unique_pid: number; + pid: number; + }; host: { hostname: string; ip: string; @@ -122,10 +126,9 @@ export type AlertEvent = Immutable<{ name: string; }; }; - process: { - pid: number; - }; thread: {}; + endpoint?: {}; + endgame?: {}; }>; /** @@ -186,22 +189,34 @@ export interface ESTotal { export type AlertHits = SearchResponse['hits']['hits']; export interface LegacyEndpointEvent { - '@timestamp': Date; + '@timestamp': number; endgame: { - event_type_full: string; - event_subtype_full: string; + pid?: number; + ppid?: number; + event_type_full?: string; + event_subtype_full?: string; + event_timestamp?: number; + event_type?: number; unique_pid: number; - unique_ppid: number; - serial_event_id: number; + unique_ppid?: number; + machine_id?: string; + process_name?: string; + process_path?: string; + timestamp_utc?: string; + serial_event_id?: number; }; agent: { id: string; type: string; + version: string; }; + process?: object; + rule?: object; + user?: object; } export interface EndpointEvent { - '@timestamp': Date; + '@timestamp': number; event: { category: string; type: string; @@ -216,6 +231,7 @@ export interface EndpointEvent { }; }; agent: { + id: string; type: string; }; } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 7ab66817a0888..296587706e6ac 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -11,6 +11,7 @@ import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; import { Route, Switch, BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; import { Store } from 'redux'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { RouteCapture } from './view/route_capture'; import { appStoreFactory } from './store'; import { AlertIndex } from './view/alerts'; @@ -24,9 +25,7 @@ import { HeaderNavigation } from './components/header_nav'; export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMountParameters) { coreStart.http.get('/api/endpoint/hello-world'); const store = appStoreFactory(coreStart); - - ReactDOM.render(, element); - + ReactDOM.render(, element); return () => { ReactDOM.unmountComponentAtNode(element); }; @@ -35,35 +34,46 @@ export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMou interface RouterProps { basename: string; store: Store; + coreStart: CoreStart; } -const AppRoot: React.FunctionComponent = React.memo(({ basename, store }) => ( - - - - - - - ( -

- -

- )} - /> - - } /> - - ( - - )} - /> -
-
-
-
-
-)); +const AppRoot: React.FunctionComponent = React.memo( + ({ basename, store, coreStart: { http } }) => ( + + + + + + + + ( +

+ +

+ )} + /> + + + + ( + + )} + /> +
+
+
+
+
+
+ ) +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts index b90f897ea2229..8eadb3e7fb3df 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts @@ -43,6 +43,7 @@ export const mockAlertResultList: (options?: { }, process: { pid: 107, + unique_pid: 1, }, host: { hostname: 'HD-c15-bc09190a', diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts index 54add85f0fe04..f217e3cda9191 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts @@ -9,13 +9,13 @@ import { createSelector, createStructuredSelector as createStructuredSelectorWithBadType, } from 'reselect'; -import { Immutable } from '../../../../../common/types'; import { AlertListState, AlertingIndexUIQueryParams, AlertsAPIQueryParams, CreateStructuredSelector, } from '../../types'; +import { Immutable, LegacyEndpointEvent } from '../../../../../common/types'; const createStructuredSelector: CreateStructuredSelector = createStructuredSelectorWithBadType; /** @@ -92,3 +92,24 @@ export const hasSelectedAlert: (state: AlertListState) => boolean = createSelect uiQueryParams, ({ selected_alert: selectedAlert }) => selectedAlert !== undefined ); + +/** + * Determine if the alert event is most likely compatible with LegacyEndpointEvent. + */ +function isAlertEventLegacyEndpointEvent(event: { endgame?: {} }): event is LegacyEndpointEvent { + return event.endgame !== undefined && 'unique_pid' in event.endgame; +} + +export const selectedEvent: ( + state: AlertListState +) => LegacyEndpointEvent | undefined = createSelector( + uiQueryParams, + alertListData, + ({ selected_alert: selectedAlert }, alertList) => { + const found = alertList.find(alert => alert.event.id === selectedAlert); + if (!found) { + return found; + } + return isAlertEventLegacyEndpointEvent(found) ? found : undefined; + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx index 37847553d512a..fe362f21a178e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx @@ -11,6 +11,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { AlertIndex } from './index'; import { appStoreFactory } from '../../store'; import { coreMock } from 'src/core/public/mocks'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; import { fireEvent, waitForElement, act } from '@testing-library/react'; import { RouteCapture } from '../route_capture'; import { createMemoryHistory, MemoryHistory } from 'history'; @@ -44,6 +45,7 @@ describe('when on the alerting page', () => { * Create a store, with the middleware disabled. We don't want side effects being created by our code in this test. */ store = appStoreFactory(coreMock.createStart(), true); + /** * Render the test component, use this after setting up anything in `beforeEach`. */ @@ -56,13 +58,15 @@ describe('when on the alerting page', () => { */ return reactTestingLibrary.render( - - - - - - - + + + + + + + + + ); }; @@ -136,6 +140,9 @@ describe('when on the alerting page', () => { it('should show the flyout', async () => { await render().findByTestId('alertDetailFlyout'); }); + it('should render resolver', async () => { + await render().findByTestId('alertResolver'); + }); describe('when the user clicks the close button on the flyout', () => { let renderResult: reactTestingLibrary.RenderResult; beforeEach(async () => { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx index 6f88727575557..3c229484ede4e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx @@ -25,6 +25,7 @@ import { urlFromQueryParams } from './url_from_query_params'; import { AlertData } from '../../../../../common/types'; import * as selectors from '../../store/alerts/selectors'; import { useAlertListSelector } from './hooks/use_alerts_selector'; +import { AlertDetailResolver } from './resolver'; export const AlertIndex = memo(() => { const history = useHistory(); @@ -86,6 +87,7 @@ export const AlertIndex = memo(() => { const alertListData = useAlertListSelector(selectors.alertListData); const hasSelectedAlert = useAlertListSelector(selectors.hasSelectedAlert); const queryParams = useAlertListSelector(selectors.uiQueryParams); + const selectedEvent = useAlertListSelector(selectors.selectedEvent); const onChangeItemsPerPage = useCallback( newPageSize => { @@ -132,12 +134,11 @@ export const AlertIndex = memo(() => { } const row = alertListData[rowIndex % pageSize]; - if (columnId === 'alert_type') { return ( {i18n.translate( 'xpack.endpoint.application.endpoint.alerts.alertType.maliciousFileDescription', @@ -213,7 +214,9 @@ export const AlertIndex = memo(() => { - + + + )} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx new file mode 100644 index 0000000000000..c7ef7f73dfe05 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx @@ -0,0 +1,35 @@ +/* + * 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 styled from 'styled-components'; +import { Provider } from 'react-redux'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { Resolver } from '../../../../embeddables/resolver/view'; +import { EndpointPluginServices } from '../../../../plugin'; +import { LegacyEndpointEvent } from '../../../../../common/types'; +import { storeFactory } from '../../../../embeddables/resolver/store'; + +export const AlertDetailResolver = styled( + React.memo( + ({ className, selectedEvent }: { className?: string; selectedEvent?: LegacyEndpointEvent }) => { + const context = useKibana(); + const { store } = storeFactory(context); + return ( +
+ + + +
+ ); + } + ) +)` + height: 100%; + width: 100%; + display: flex; + flex-grow: 1; +`; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts index 0eb3505096b4a..6892bf11ecff2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts @@ -5,15 +5,16 @@ */ import { uniquePidForProcess, uniqueParentPidForProcess } from './process_event'; -import { IndexedProcessTree, ProcessEvent } from '../types'; +import { IndexedProcessTree } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; import { levelOrder as baseLevelOrder } from '../lib/tree_sequencers'; /** * Create a new IndexedProcessTree from an array of ProcessEvents */ -export function factory(processes: ProcessEvent[]): IndexedProcessTree { - const idToChildren = new Map(); - const idToValue = new Map(); +export function factory(processes: LegacyEndpointEvent[]): IndexedProcessTree { + const idToChildren = new Map(); + const idToValue = new Map(); for (const process of processes) { idToValue.set(uniquePidForProcess(process), process); @@ -35,7 +36,10 @@ export function factory(processes: ProcessEvent[]): IndexedProcessTree { /** * Returns an array with any children `ProcessEvent`s of the passed in `process` */ -export function children(tree: IndexedProcessTree, process: ProcessEvent): ProcessEvent[] { +export function children( + tree: IndexedProcessTree, + process: LegacyEndpointEvent +): LegacyEndpointEvent[] { const id = uniquePidForProcess(process); const processChildren = tree.idToChildren.get(id); return processChildren === undefined ? [] : processChildren; @@ -46,8 +50,8 @@ export function children(tree: IndexedProcessTree, process: ProcessEvent): Proce */ export function parent( tree: IndexedProcessTree, - childProcess: ProcessEvent -): ProcessEvent | undefined { + childProcess: LegacyEndpointEvent +): LegacyEndpointEvent | undefined { const uniqueParentPid = uniqueParentPidForProcess(childProcess); if (uniqueParentPid === undefined) { return undefined; @@ -70,7 +74,7 @@ export function root(tree: IndexedProcessTree) { if (size(tree) === 0) { return null; } - let current: ProcessEvent = tree.idToProcess.values().next().value; + let current: LegacyEndpointEvent = tree.idToProcess.values().next().value; while (parent(tree, current) !== undefined) { current = parent(tree, current)!; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts index 3177671a30001..3916396f7402c 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts @@ -4,22 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ import { eventType } from './process_event'; -import { ProcessEvent } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; import { mockProcessEvent } from './process_event_test_helpers'; describe('process event', () => { describe('eventType', () => { - let event: ProcessEvent; + let event: LegacyEndpointEvent; beforeEach(() => { event = mockProcessEvent({ - data_buffer: { - node_id: 1, + endgame: { + unique_pid: 1, event_type_full: 'process_event', }, }); }); it("returns the right value when the subType is 'creation_event'", () => { - event.data_buffer.event_subtype_full = 'creation_event'; + event.endgame.event_subtype_full = 'creation_event'; expect(eventType(event)).toEqual('processCreated'); }); }); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts index c8496b8e6e7a5..876168d2ed96a 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts @@ -4,23 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessEvent } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; /** * Returns true if the process's eventType is either 'processCreated' or 'processRan'. * Resolver will only render 'graphable' process events. */ -export function isGraphableProcess(event: ProcessEvent) { - return eventType(event) === 'processCreated' || eventType(event) === 'processRan'; +export function isGraphableProcess(passedEvent: LegacyEndpointEvent) { + return eventType(passedEvent) === 'processCreated' || eventType(passedEvent) === 'processRan'; } /** * Returns a custom event type for a process event based on the event's metadata. */ -export function eventType(event: ProcessEvent) { +export function eventType(passedEvent: LegacyEndpointEvent) { const { - data_buffer: { event_type_full: type, event_subtype_full: subType }, - } = event; + endgame: { event_type_full: type, event_subtype_full: subType }, + } = passedEvent; if (type === 'process_event') { if (subType === 'creation_event' || subType === 'fork_event' || subType === 'exec_event') { @@ -41,13 +41,13 @@ export function eventType(event: ProcessEvent) { /** * Returns the process event's pid */ -export function uniquePidForProcess(event: ProcessEvent) { - return event.data_buffer.node_id; +export function uniquePidForProcess(event: LegacyEndpointEvent) { + return event.endgame.unique_pid; } /** * Returns the process event's parent pid */ -export function uniqueParentPidForProcess(event: ProcessEvent) { - return event.data_buffer.source_id; +export function uniqueParentPidForProcess(event: LegacyEndpointEvent) { + return event.endgame.unique_ppid; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts index 9a6f19adcc101..e88837d325108 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts @@ -4,33 +4,46 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessEvent } from '../types'; -type DeepPartial = { [K in keyof T]?: DeepPartial }; +import { LegacyEndpointEvent } from '../../../../common/types'; +type DeepPartial = { [K in keyof T]?: DeepPartial }; /** * Creates a mock process event given the 'parts' argument, which can * include all or some process event fields as determined by the ProcessEvent type. * The only field that must be provided is the event's 'node_id' field. * The other fields are populated by the function unless provided in 'parts' */ -export function mockProcessEvent( - parts: { - data_buffer: { node_id: ProcessEvent['data_buffer']['node_id'] }; - } & DeepPartial -): ProcessEvent { - const { data_buffer: dataBuffer } = parts; +export function mockProcessEvent(parts: { + endgame: { + unique_pid: LegacyEndpointEvent['endgame']['unique_pid']; + unique_ppid?: LegacyEndpointEvent['endgame']['unique_ppid']; + process_name?: LegacyEndpointEvent['endgame']['process_name']; + event_subtype_full?: LegacyEndpointEvent['endgame']['event_subtype_full']; + event_type_full?: LegacyEndpointEvent['endgame']['event_type_full']; + } & DeepPartial; +}): LegacyEndpointEvent { + const { endgame: dataBuffer } = parts; return { - event_timestamp: 1, - event_type: 1, - machine_id: '', - ...parts, - data_buffer: { - timestamp_utc: '2019-09-24 01:47:47Z', + endgame: { + ...dataBuffer, + event_timestamp: 1, + event_type: 1, + unique_ppid: 0, + unique_pid: 1, + machine_id: '', event_subtype_full: 'creation_event', event_type_full: 'process_event', process_name: '', process_path: '', - ...dataBuffer, + timestamp_utc: '', + serial_event_id: 1, + }, + '@timestamp': 1582233383000, + agent: { + type: '', + id: '', + version: '', }, + ...parts, }; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts index 25f196c76a290..ecba0ec404d44 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts @@ -3,9 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ProcessEvent } from '../types'; import { CameraAction } from './camera'; import { DataAction } from './data'; +import { LegacyEndpointEvent } from '../../../../common/types'; /** * When the user wants to bring a process node front-and-center on the map. @@ -16,7 +16,7 @@ interface UserBroughtProcessIntoView { /** * Used to identify the process node that should be brought into view. */ - readonly process: ProcessEvent; + readonly process: LegacyEndpointEvent; /** * The time (since epoch in milliseconds) when the action was dispatched. */ @@ -24,4 +24,29 @@ interface UserBroughtProcessIntoView { }; } -export type ResolverAction = CameraAction | DataAction | UserBroughtProcessIntoView; +/** + * Used when the alert list selects an alert and the flyout shows resolver. + */ +interface UserChangedSelectedEvent { + readonly type: 'userChangedSelectedEvent'; + readonly payload: { + /** + * Optional because they could have unselected the event. + */ + selectedEvent?: LegacyEndpointEvent; + }; +} + +/** + * Triggered by middleware when the data for resolver needs to be loaded. Used to set state in redux to 'loading'. + */ +interface AppRequestedResolverData { + readonly type: 'appRequestedResolverData'; +} + +export type ResolverAction = + | CameraAction + | DataAction + | UserBroughtProcessIntoView + | UserChangedSelectedEvent + | AppRequestedResolverData; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap index 1dc17054b9f47..b88652097eb5c 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap @@ -12,17 +12,18 @@ Object { "edgeLineSegments": Array [], "processNodePositions": Map { Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 0, "process_name": "", - "process_path": "", - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 0, -0.8164965809277259, @@ -167,136 +168,137 @@ Object { ], "processNodePositions": Map { Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 0, "process_name": "", - "process_path": "", - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 0, -0.8164965809277259, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "already_running", "event_type_full": "process_event", - "node_id": 1, - "process_name": "", - "process_path": "", - "source_id": 0, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 1, + "unique_ppid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 0, -82.46615467370032, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 2, - "process_name": "", - "process_path": "", - "source_id": 0, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 2, + "unique_ppid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 141.4213562373095, -0.8164965809277259, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 3, - "process_name": "", - "process_path": "", - "source_id": 1, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 3, + "unique_ppid": 1, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 35.35533905932738, -143.70339824327976, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 4, - "process_name": "", - "process_path": "", - "source_id": 1, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 4, + "unique_ppid": 1, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 106.06601717798213, -102.87856919689347, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 5, - "process_name": "", - "process_path": "", - "source_id": 2, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 5, + "unique_ppid": 2, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 176.7766952966369, -62.053740150507174, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 6, - "process_name": "", - "process_path": "", - "source_id": 2, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 6, + "unique_ppid": 2, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 247.48737341529164, -21.228911104120883, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 7, - "process_name": "", - "process_path": "", - "source_id": 6, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 7, + "unique_ppid": 6, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 318.1980515339464, -62.05374015050717, @@ -321,34 +323,35 @@ Object { ], "processNodePositions": Map { Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 0, "process_name": "", - "process_path": "", - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 0, -0.8164965809277259, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "already_running", "event_type_full": "process_event", - "node_id": 1, - "process_name": "", - "process_path": "", - "source_id": 0, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 1, + "unique_ppid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 70.71067811865476, -41.641325627314025, diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts index 900b9bda571da..f34d7c08ce08c 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessEvent } from '../../types'; +import { LegacyEndpointEvent } from '../../../../../common/types'; interface ServerReturnedResolverData { readonly type: 'serverReturnedResolverData'; readonly payload: { readonly data: { readonly result: { - readonly search_results: readonly ProcessEvent[]; + readonly search_results: readonly LegacyEndpointEvent[]; }; }; }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts index fac70433f14b2..f01136fe20ebf 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts @@ -7,20 +7,21 @@ import { Store, createStore } from 'redux'; import { DataAction } from './action'; import { dataReducer } from './reducer'; -import { DataState, ProcessEvent } from '../../types'; +import { DataState } from '../../types'; +import { LegacyEndpointEvent } from '../../../../../common/types'; import { graphableProcesses, processNodePositionsAndEdgeLineSegments } from './selectors'; import { mockProcessEvent } from '../../models/process_event_test_helpers'; describe('resolver graph layout', () => { - let processA: ProcessEvent; - let processB: ProcessEvent; - let processC: ProcessEvent; - let processD: ProcessEvent; - let processE: ProcessEvent; - let processF: ProcessEvent; - let processG: ProcessEvent; - let processH: ProcessEvent; - let processI: ProcessEvent; + let processA: LegacyEndpointEvent; + let processB: LegacyEndpointEvent; + let processC: LegacyEndpointEvent; + let processD: LegacyEndpointEvent; + let processE: LegacyEndpointEvent; + let processF: LegacyEndpointEvent; + let processG: LegacyEndpointEvent; + let processH: LegacyEndpointEvent; + let processI: LegacyEndpointEvent; let store: Store; beforeEach(() => { @@ -37,75 +38,75 @@ describe('resolver graph layout', () => { * */ processA = mockProcessEvent({ - data_buffer: { + endgame: { process_name: '', event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 0, + unique_pid: 0, }, }); processB = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'already_running', - node_id: 1, - source_id: 0, + unique_pid: 1, + unique_ppid: 0, }, }); processC = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 2, - source_id: 0, + unique_pid: 2, + unique_ppid: 0, }, }); processD = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 3, - source_id: 1, + unique_pid: 3, + unique_ppid: 1, }, }); processE = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 4, - source_id: 1, + unique_pid: 4, + unique_ppid: 1, }, }); processF = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 5, - source_id: 2, + unique_pid: 5, + unique_ppid: 2, }, }); processG = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 6, - source_id: 2, + unique_pid: 6, + unique_ppid: 2, }, }); processH = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 7, - source_id: 6, + unique_pid: 7, + unique_ppid: 6, }, }); processI = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'termination_event', - node_id: 8, - source_id: 0, + unique_pid: 8, + unique_ppid: 0, }, }); store = createStore(dataReducer, undefined); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts index 848d814808bac..a3184389a794e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts @@ -6,11 +6,11 @@ import { Reducer } from 'redux'; import { DataState, ResolverAction } from '../../types'; -import { sampleData } from './sample'; function initialState(): DataState { return { - results: sampleData.data.result.search_results, + results: [], + isLoading: false, }; } @@ -24,6 +24,12 @@ export const dataReducer: Reducer = (state = initialS return { ...state, results: search_results, + isLoading: false, + }; + } else if (action.type === 'appRequestedResolverData') { + return { + ...state, + isLoading: true, }; } else { return state; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 75b477dd7c7fc..304abbb06880b 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -7,7 +7,6 @@ import { createSelector } from 'reselect'; import { DataState, - ProcessEvent, IndexedProcessTree, ProcessWidths, ProcessPositions, @@ -15,6 +14,7 @@ import { ProcessWithWidthMetadata, Matrix3, } from '../../types'; +import { LegacyEndpointEvent } from '../../../../../common/types'; import { Vector2 } from '../../types'; import { add as vector2Add, applyMatrix3 } from '../../lib/vector2'; import { isGraphableProcess } from '../../models/process_event'; @@ -29,6 +29,10 @@ import { const unit = 100; const distanceBetweenNodesInUnits = 1; +export function isLoading(state: DataState) { + return state.isLoading; +} + /** * An isometric projection is a method for representing three dimensional objects in 2 dimensions. * More information about isometric projections can be found here https://en.wikipedia.org/wiki/Isometric_projection. @@ -108,7 +112,7 @@ export const graphableProcesses = createSelector( * */ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): ProcessWidths { - const widths = new Map(); + const widths = new Map(); if (size(indexedProcessTree) === 0) { return widths; @@ -309,13 +313,13 @@ function processPositions( indexedProcessTree: IndexedProcessTree, widths: ProcessWidths ): ProcessPositions { - const positions = new Map(); + const positions = new Map(); /** * This algorithm iterates the tree in level order. It keeps counters that are reset for each parent. * By keeping track of the last parent node, we can know when we are dealing with a new set of siblings and * reset the counters. */ - let lastProcessedParentNode: ProcessEvent | undefined; + let lastProcessedParentNode: LegacyEndpointEvent | undefined; /** * Nodes are positioned relative to their siblings. We walk this in level order, so we handle * children left -> right. @@ -420,7 +424,7 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( * Transform the positions of nodes and edges so they seem like they are on an isometric grid. */ const transformedEdgeLineSegments: EdgeLineSegment[] = []; - const transformedPositions = new Map(); + const transformedPositions = new Map(); for (const [processEvent, position] of positions) { transformedPositions.set(processEvent, applyMatrix3(position, isometricTransformMatrix)); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts index b17572bbc4ab4..2a20c73347348 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts @@ -6,17 +6,21 @@ import { createStore, applyMiddleware, Store } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly'; +import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public'; import { ResolverAction, ResolverState } from '../types'; +import { EndpointPluginServices } from '../../../plugin'; import { resolverReducer } from './reducer'; +import { resolverMiddlewareFactory } from './middleware'; -export const storeFactory = (): { store: Store } => { +export const storeFactory = ( + context?: KibanaReactContextValue +): { store: Store } => { const actionsBlacklist: Array = ['userMovedPointer']; const composeEnhancers = composeWithDevTools({ name: 'Resolver', actionsBlacklist, }); - - const middlewareEnhancer = applyMiddleware(); + const middlewareEnhancer = applyMiddleware(resolverMiddlewareFactory(context)); const store = createStore(resolverReducer, composeEnhancers(middlewareEnhancer)); return { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts index 8808160c9c631..9f06643626f50 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts @@ -6,7 +6,8 @@ import { animatePanning } from './camera/methods'; import { processNodePositionsAndEdgeLineSegments } from './selectors'; -import { ResolverState, ProcessEvent } from '../types'; +import { ResolverState } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; const animationDuration = 1000; @@ -16,7 +17,7 @@ const animationDuration = 1000; export function animateProcessIntoView( state: ResolverState, startTime: number, - process: ProcessEvent + process: LegacyEndpointEvent ): ResolverState { const { processNodePositions } = processNodePositionsAndEdgeLineSegments(state); const position = processNodePositions.get(process); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts new file mode 100644 index 0000000000000..900aece60618d --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts @@ -0,0 +1,45 @@ +/* + * 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 { Dispatch, MiddlewareAPI } from 'redux'; +import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public'; +import { EndpointPluginServices } from '../../../plugin'; +import { ResolverState, ResolverAction } from '../types'; + +type MiddlewareFactory = ( + context?: KibanaReactContextValue +) => ( + api: MiddlewareAPI, S> +) => (next: Dispatch) => (action: ResolverAction) => unknown; + +export const resolverMiddlewareFactory: MiddlewareFactory = context => { + return api => next => async (action: ResolverAction) => { + next(action); + if (action.type === 'userChangedSelectedEvent') { + if (context?.services.http) { + api.dispatch({ type: 'appRequestedResolverData' }); + const uniquePid = action.payload.selectedEvent?.endgame?.unique_pid; + const legacyEndpointID = action.payload.selectedEvent?.agent?.id; + const [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ + context.services.http.get(`/api/endpoint/resolver/${uniquePid}`, { + query: { legacyEndpointID }, + }), + context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`, { + query: { legacyEndpointID }, + }), + context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`, { + query: { legacyEndpointID }, + }), + ]); + const response = [...lifecycle, ...children, ...relatedEvents]; + api.dispatch({ + type: 'serverReturnedResolverData', + payload: { data: { result: { search_results: response } } }, + }); + } + } + }; +}; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index 25d08a8c347ed..708eb684ebd3e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -68,6 +68,11 @@ function dataStateSelector(state: ResolverState) { return state.data; } +/** + * Whether or not the resolver is pending fetching data + */ +export const isLoading = composeSelectors(dataStateSelector, dataSelectors.isLoading); + /** * Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a * concern-specific selector. `selector` should return the concern-specific state. diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 6c6936d377dea..4c2a1ea5ac21f 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -8,6 +8,7 @@ import { Store } from 'redux'; import { ResolverAction } from './store/actions'; export { ResolverAction } from './store/actions'; +import { LegacyEndpointEvent } from '../../../common/types'; /** * Redux state for the Resolver feature. Properties on this interface are populated via multiple reducers using redux's `combineReducers`. @@ -114,7 +115,8 @@ export type CameraState = { * State for `data` reducer which handles receiving Resolver data from the backend. */ export interface DataState { - readonly results: readonly ProcessEvent[]; + readonly results: readonly LegacyEndpointEvent[]; + isLoading: boolean; } export type Vector2 = readonly [number, number]; @@ -182,21 +184,21 @@ export interface IndexedProcessTree { /** * Map of ID to a process's children */ - idToChildren: Map; + idToChildren: Map; /** * Map of ID to process */ - idToProcess: Map; + idToProcess: Map; } /** * A map of ProcessEvents (representing process nodes) to the 'width' of their subtrees as calculated by `widthsOfProcessSubtrees` */ -export type ProcessWidths = Map; +export type ProcessWidths = Map; /** * Map of ProcessEvents (representing process nodes) to their positions. Calculated by `processPositions` */ -export type ProcessPositions = Map; +export type ProcessPositions = Map; /** * An array of vectors2 forming an polyline. Used to connect process nodes in the graph. */ @@ -206,11 +208,11 @@ export type EdgeLineSegment = Vector2[]; * Used to provide precalculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph. */ export type ProcessWithWidthMetadata = { - process: ProcessEvent; + process: LegacyEndpointEvent; width: number; } & ( | { - parent: ProcessEvent; + parent: LegacyEndpointEvent; parentWidth: number; isOnlyChild: boolean; firstChildWidth: number; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx index d71a4d87b7eab..52a0872f269f5 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -4,15 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { useSelector } from 'react-redux'; +import React, { useLayoutEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { EuiLoadingSpinner } from '@elastic/eui'; import * as selectors from '../store/selectors'; import { EdgeLine } from './edge_line'; import { Panel } from './panel'; import { GraphControls } from './graph_controls'; import { ProcessEventDot } from './process_event_dot'; import { useCamera } from './use_camera'; +import { ResolverAction } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; const StyledPanel = styled(Panel)` position: absolute; @@ -31,35 +34,57 @@ const StyledGraphControls = styled(GraphControls)` `; export const Resolver = styled( - React.memo(function Resolver({ className }: { className?: string }) { + React.memo(function Resolver({ + className, + selectedEvent, + }: { + className?: string; + selectedEvent?: LegacyEndpointEvent; + }) { const { processNodePositions, edgeLineSegments } = useSelector( selectors.processNodePositionsAndEdgeLineSegments ); + const dispatch: (action: ResolverAction) => unknown = useDispatch(); const { projectionMatrix, ref, onMouseDown } = useCamera(); + const isLoading = useSelector(selectors.isLoading); + useLayoutEffect(() => { + dispatch({ + type: 'userChangedSelectedEvent', + payload: { selectedEvent }, + }); + }, [dispatch, selectedEvent]); return (
-
- {Array.from(processNodePositions).map(([processEvent, position], index) => ( - - ))} - {edgeLineSegments.map(([startPosition, endPosition], index) => ( - - ))} -
- - + {isLoading ? ( +
+ +
+ ) : ( + <> +
+ {Array.from(processNodePositions).map(([processEvent, position], index) => ( + + ))} + {edgeLineSegments.map(([startPosition, endPosition], index) => ( + + ))} +
+ + + + )}
); }) @@ -72,6 +97,12 @@ export const Resolver = styled( display: flex; flex-grow: 1; } + .loading-container { + display: flex; + align-items: center; + justify-content: center; + flex-grow: 1; + } /** * The placeholder components use absolute positioning. */ diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx index c75b73b4bceaf..84c299698bb32 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx @@ -11,7 +11,7 @@ import euiVars from '@elastic/eui/dist/eui_theme_light.json'; import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { SideEffectContext } from './side_effect_context'; -import { ProcessEvent } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as selectors from '../store/selectors'; @@ -38,7 +38,7 @@ export const Panel = memo(function Event({ className }: { className?: string }) interface ProcessTableView { name: string; timestamp?: Date; - event: ProcessEvent; + event: LegacyEndpointEvent; } const { processNodePositions } = useSelector(selectors.processNodePositionsAndEdgeLineSegments); @@ -47,11 +47,16 @@ export const Panel = memo(function Event({ className }: { className?: string }) const processTableView: ProcessTableView[] = useMemo( () => [...processNodePositions.keys()].map(processEvent => { - const { data_buffer } = processEvent; - const date = new Date(data_buffer.timestamp_utc); + let dateTime; + if (processEvent.endgame.timestamp_utc) { + const date = new Date(processEvent.endgame.timestamp_utc); + if (isFinite(date.getTime())) { + dateTime = date; + } + } return { - name: data_buffer.process_name, - timestamp: isFinite(date.getTime()) ? date : undefined, + name: processEvent.endgame.process_name ? processEvent.endgame.process_name : '', + timestamp: dateTime, event: processEvent, }; }), diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx index 384fbf90ed984..034780c7ba14c 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx @@ -7,7 +7,8 @@ import React from 'react'; import styled from 'styled-components'; import { applyMatrix3 } from '../lib/vector2'; -import { Vector2, ProcessEvent, Matrix3 } from '../types'; +import { Vector2, Matrix3 } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; /** * A placeholder view for a process node. @@ -31,7 +32,7 @@ export const ProcessEventDot = styled( /** * An event which contains details about the process node. */ - event: ProcessEvent; + event: LegacyEndpointEvent; /** * projectionMatrix which can be used to convert `position` to screen coordinates. */ @@ -48,7 +49,7 @@ export const ProcessEventDot = styled( }; return ( - name: {event.data_buffer.process_name} + name: {event.endgame.process_name}
x: {position[0]}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx index f4abb51f062f2..1948c6cae505b 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx @@ -10,16 +10,12 @@ import { useCamera } from './use_camera'; import { Provider } from 'react-redux'; import * as selectors from '../store/selectors'; import { storeFactory } from '../store'; -import { - Matrix3, - ResolverAction, - ResolverStore, - ProcessEvent, - SideEffectSimulator, -} from '../types'; +import { Matrix3, ResolverAction, ResolverStore, SideEffectSimulator } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; import { SideEffectContext } from './side_effect_context'; import { applyMatrix3 } from '../lib/vector2'; import { sideEffectSimulator } from './side_effect_simulator'; +import { mockProcessEvent } from '../models/process_event_test_helpers'; describe('useCamera on an unpainted element', () => { let element: HTMLElement; @@ -28,6 +24,7 @@ describe('useCamera on an unpainted element', () => { let reactRenderResult: RenderResult; let store: ResolverStore; let simulator: SideEffectSimulator; + beforeEach(async () => { ({ store } = storeFactory()); @@ -136,17 +133,45 @@ describe('useCamera on an unpainted element', () => { expect(simulator.mock.requestAnimationFrame).not.toHaveBeenCalled(); }); describe('when the camera begins animation', () => { - let process: ProcessEvent; + let process: LegacyEndpointEvent; beforeEach(() => { - // At this time, processes are provided via mock data. In the future, this test will have to provide those mocks. - const processes: ProcessEvent[] = [ + const events: LegacyEndpointEvent[] = []; + const numberOfEvents: number = Math.floor(Math.random() * 10 + 1); + + for (let index = 0; index < numberOfEvents; index++) { + const uniquePpid = index === 0 ? undefined : index - 1; + events.push( + mockProcessEvent({ + endgame: { + unique_pid: index, + unique_ppid: uniquePpid, + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + }, + }) + ); + } + const serverResponseAction: ResolverAction = { + type: 'serverReturnedResolverData', + payload: { + data: { + result: { + search_results: events, + }, + }, + }, + }; + act(() => { + store.dispatch(serverResponseAction); + }); + const processes: LegacyEndpointEvent[] = [ ...selectors .processNodePositionsAndEdgeLineSegments(store.getState()) .processNodePositions.keys(), ]; process = processes[processes.length - 1]; simulator.controls.time = 0; - const action: ResolverAction = { + const cameraAction: ResolverAction = { type: 'userBroughtProcessIntoView', payload: { time: simulator.controls.time, @@ -154,7 +179,7 @@ describe('useCamera on an unpainted element', () => { }, }; act(() => { - store.dispatch(action); + store.dispatch(cameraAction); }); }); diff --git a/x-pack/plugins/endpoint/public/plugin.ts b/x-pack/plugins/endpoint/public/plugin.ts index 355364253b2a5..0e10fe680e9f0 100644 --- a/x-pack/plugins/endpoint/public/plugin.ts +++ b/x-pack/plugins/endpoint/public/plugin.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; +import { Plugin, CoreSetup, AppMountParameters, CoreStart } from 'kibana/public'; import { IEmbeddableSetup } from 'src/plugins/embeddable/public'; import { i18n } from '@kbn/i18n'; import { ResolverEmbeddableFactory } from './embeddables/resolver'; @@ -17,6 +17,15 @@ export interface EndpointPluginSetupDependencies { export interface EndpointPluginStartDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface +/** + * Functionality that the endpoint plugin uses from core. + */ +export interface EndpointPluginServices extends Partial { + http: CoreStart['http']; + overlays: CoreStart['overlays'] | undefined; + notifications: CoreStart['notifications'] | undefined; +} + export class EndpointPlugin implements Plugin< diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts index 2dd2e0c2d1d5f..08a906e2884d6 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts @@ -8,7 +8,7 @@ import { EndpointAppConstants } from '../../../../common/types'; describe('children events query', () => { it('generates the correct legacy queries', () => { - const timestamp = new Date(); + const timestamp = new Date().getTime(); expect( new ChildrenQuery('awesome-id', { size: 1, timestamp, eventID: 'foo' }).build('5') ).toStrictEqual({ @@ -38,7 +38,7 @@ describe('children events query', () => { }, }, }, - search_after: [timestamp.getTime(), 'foo'], + search_after: [timestamp, 'foo'], size: 1, sort: [{ '@timestamp': 'asc' }, { 'endgame.serial_event_id': 'asc' }], }, @@ -47,7 +47,7 @@ describe('children events query', () => { }); it('generates the correct non-legacy queries', () => { - const timestamp = new Date(); + const timestamp = new Date().getTime(); expect( new ChildrenQuery(undefined, { size: 1, timestamp, eventID: 'bar' }).build('baz') @@ -84,7 +84,7 @@ describe('children events query', () => { }, }, }, - search_after: [timestamp.getTime(), 'bar'], + search_after: [timestamp, 'bar'], size: 1, sort: [{ '@timestamp': 'asc' }, { 'event.id': 'asc' }], }, diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts index 8ef680a168310..a91c87274b8dd 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts @@ -8,7 +8,7 @@ import { EndpointAppConstants } from '../../../../common/types'; describe('related events query', () => { it('generates the correct legacy queries', () => { - const timestamp = new Date(); + const timestamp = new Date().getTime(); expect( new RelatedEventsQuery('awesome-id', { size: 1, timestamp, eventID: 'foo' }).build('5') ).toStrictEqual({ @@ -39,7 +39,7 @@ describe('related events query', () => { }, }, }, - search_after: [timestamp.getTime(), 'foo'], + search_after: [timestamp, 'foo'], size: 1, sort: [{ '@timestamp': 'asc' }, { 'endgame.serial_event_id': 'asc' }], }, @@ -48,7 +48,7 @@ describe('related events query', () => { }); it('generates the correct non-legacy queries', () => { - const timestamp = new Date(); + const timestamp = new Date().getTime(); expect( new RelatedEventsQuery(undefined, { size: 1, timestamp, eventID: 'bar' }).build('baz') @@ -86,7 +86,7 @@ describe('related events query', () => { }, }, }, - search_after: [timestamp.getTime(), 'bar'], + search_after: [timestamp, 'bar'], size: 1, sort: [{ '@timestamp': 'asc' }, { 'event.id': 'asc' }], }, diff --git a/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts b/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts index 33eb698479308..5a64f3ff9ddb6 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts @@ -11,12 +11,12 @@ import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public export interface PaginationParams { size: number; - timestamp?: Date; + timestamp?: number; eventID?: string; } interface PaginationCursor { - timestamp: Date; + timestamp: number; eventID: string; } @@ -35,7 +35,7 @@ function urlDecodeCursor(value: string): PaginationCursor { const { timestamp, eventID } = JSON.parse(data); // take some extra care to only grab the things we want // convert the timestamp string to date object - return { timestamp: new Date(timestamp), eventID }; + return { timestamp, eventID }; } export function getPaginationParams(limit: number, after?: string): PaginationParams { @@ -62,7 +62,7 @@ export function paginate(pagination: PaginationParams, field: string, query: Jso query.aggs = { total: { value_count: { field } } }; query.size = size; if (timestamp && eventID) { - query.search_after = [timestamp.getTime(), eventID] as Array; + query.search_after = [timestamp, eventID] as Array; } return query; } From 74d0e9297fd2efdc7943d10f2aa1d10ad4c016e9 Mon Sep 17 00:00:00 2001 From: Charlie Pichette <56399229+charlie-pichette@users.noreply.github.com> Date: Mon, 2 Mar 2020 10:46:20 -0500 Subject: [PATCH 08/26] [Endpoint] [Tests] fixes #57946 flaky endpoint policy list test (#58348) * endpoint-161-refactor-management-list-test * fix location of es archive file * issue 57946 fix flaky endpoint policy list test Co-authored-by: Elastic Machine --- x-pack/test/functional/apps/endpoint/policy_list.ts | 5 +++-- x-pack/test/functional/page_objects/endpoint_page.ts | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/endpoint/policy_list.ts b/x-pack/test/functional/apps/endpoint/policy_list.ts index 658e4dcd13e1e..382963bc2b0c7 100644 --- a/x-pack/test/functional/apps/endpoint/policy_list.ts +++ b/x-pack/test/functional/apps/endpoint/policy_list.ts @@ -11,10 +11,11 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); // FLAKY: https://github.com/elastic/kibana/issues/57946 - describe.skip('Endpoint Policy List', function() { + describe('Endpoint Policy List', function() { this.tags(['ciGroup7']); before(async () => { await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/policy'); + await pageObjects.endpoint.waitForTableToHaveData('policyTable'); }); it('loads the Policy List Page', async () => { @@ -26,7 +27,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows policy count total', async () => { const policyTotal = await testSubjects.getVisibleText('policyTotalCount'); - expect(policyTotal).to.equal('0 Policies'); + expect(policyTotal).to.equal('100 Policies'); }); it('includes policy list table', async () => { await testSubjects.existOrFail('policyTable'); diff --git a/x-pack/test/functional/page_objects/endpoint_page.ts b/x-pack/test/functional/page_objects/endpoint_page.ts index 185b95b00527d..6350f51f707f4 100644 --- a/x-pack/test/functional/page_objects/endpoint_page.ts +++ b/x-pack/test/functional/page_objects/endpoint_page.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function EndpointPageProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const retry = getService('retry'); return { /** @@ -58,5 +59,13 @@ export function EndpointPageProvider({ getService }: FtrProviderContext) { ) ); }, + + async waitForTableToHaveData(dataTestSubj: string) { + await retry.waitForWithTimeout('table to have data', 2000, async () => { + const tableData = await this.getEndpointAppTableData(dataTestSubj); + if (tableData[1][0] === 'No items found') return false; + return true; + }); + }, }; } From 1eca34126061e7226c543f86f867d95f0f1d8ea1 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Mon, 2 Mar 2020 09:59:29 -0600 Subject: [PATCH 09/26] [DOCS] Rework of main get started page (#58260) * [DOCS] Rework of main gett started page * Redirect for add-sample-data * Link fix * Review comments Co-authored-by: Elastic Machine --- docs/redirects.asciidoc | 5 +++ docs/user/getting-started.asciidoc | 70 +++++++++++++++++------------- docs/user/index.asciidoc | 4 +- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 3843cc27defd5..8ad5330f3fda5 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -56,6 +56,11 @@ This page has moved. Please see <>. This page has moved. Please see <>. +[role="exclude",id="add-sample-data"] +== Add sample data + +This page has moved. Please see <>. + [role="exclude",id="tilemap"] == Coordinate map diff --git a/docs/user/getting-started.asciidoc b/docs/user/getting-started.asciidoc index c6fe5b5b92d69..d426ec111351c 100644 --- a/docs/user/getting-started.asciidoc +++ b/docs/user/getting-started.asciidoc @@ -1,54 +1,65 @@ [[getting-started]] -= Getting Started += Get started [partintro] -- -You’re new to Kibana and want to give it a try. {kib} has sample data sets and -tutorials to help you get started. +Ready to try out {kib} and see what it can do? To quickest way to get started with {kib} is to set up on Cloud, then add a sample data set that helps you get a handle on the full range of {kib} features. [float] -=== Sample data +[[cloud-set-up]] +== Set up on Cloud -You can use the <> to take {kib} for a test ride without having -to go through the process of loading data yourself. With one click, -you can install a sample data set and start interacting with -{kib} visualizations in seconds. You can access the sample data -from the {kib} home page. +To access {kib} in a single click, run our hosted Elasticsearch Service on Elastic Cloud. -[float] +. Log into the link:https://cloud.elastic.co/[Elasticsearch Service Console]. +If you need an account, register for a link:https://www.elastic.co/cloud/elasticsearch-service/signup[free 14-day trial]. + +. Click *Create deployment*, then give your deployment a name. -=== Add data tutorials -{kib} has built-in *Add Data* tutorials to help you set up -data flows in the Elastic Stack. These tutorials are available -from the Kibana home page. In *Add Data to Kibana*, find the data type -you’re interested in, and click its button to view a list of available tutorials. +. To use the default options, click *Create deployment*. You can modify the other deployment options, but the default options are great to get started. + +Be sure to copy down the password for the `elastic` user and Cloud ID information. You'll need that later. [float] -=== Hands-on experience +[[get-data-in]] +== Get data into {kib} + +The easiest way to get data into {kib} is to add a sample data set. + +{kib} has several sample data sets that you can use before loading your own data: + +* *Sample eCommerce orders* includes visualizations for tracking product-related information, +such as cost, revenue, and price. + +* *Sample flight data* includes visualizations for monitoring flight routes. -The following tutorials walk you through searching, analyzing, -and visualizing data. +* *Sample web logs* includes visualizations for monitoring website traffic. -* <>. You'll -learn to filter and query data, edit visualizations, and interact with dashboards. +To use the sample data sets: -* <>. You'll manually load a data set and build -your own visualizations and dashboard. +. Go to the {kib} home page. + +. Click *Load a data set and a {kib} dashboard*. + +. Click *View data* and view the prepackaged dashboards, maps, and more. + +[role="screenshot"] +image::images/add-sample-data.png[] + +NOTE: The timestamps in the sample data sets are relative to when they are installed. +If you uninstall and reinstall a data set, the timestamps change to reflect the most recent installation. [float] -=== Before you begin +[[getting-started-next-steps]] +== Next steps -Make sure you've <> and established -a <>. +* To get a hands-on experience creating visualizations, follow the <> tutorial. -If you are running our hosted Elasticsearch Service on Elastic Cloud, you access Kibana with a single click. (You can {ess-trial}[sign up for a free trial] and start exploring data in minutes.) +* If you're ready to load an actual data set and build a dashboard, follow the <> tutorial. -- -include::{kib-repo-dir}/getting-started/add-sample-data.asciidoc[] - include::{kib-repo-dir}/getting-started/tutorial-sample-data.asciidoc[] include::{kib-repo-dir}/getting-started/tutorial-full-experience.asciidoc[] @@ -60,4 +71,3 @@ include::{kib-repo-dir}/getting-started/tutorial-discovering.asciidoc[] include::{kib-repo-dir}/getting-started/tutorial-visualizing.asciidoc[] include::{kib-repo-dir}/getting-started/tutorial-dashboard.asciidoc[] - diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index 3911d57e05c9a..ff100d0763368 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -1,13 +1,13 @@ include::introduction.asciidoc[] +include::getting-started.asciidoc[] + include::setup.asciidoc[] include::monitoring/configuring-monitoring.asciidoc[] include::security/securing-kibana.asciidoc[] -include::getting-started.asciidoc[] - include::discover.asciidoc[] include::visualize.asciidoc[] From 323bb21df30f3a94a83fcb2e561c46c1b17e0e03 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 2 Mar 2020 11:27:15 -0500 Subject: [PATCH 10/26] [Remote clusters] Add indexManagement as required plugin (#58915) --- x-pack/plugins/remote_clusters/kibana.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/remote_clusters/kibana.json b/x-pack/plugins/remote_clusters/kibana.json index 27ae6802966dd..609d0f67f2c7b 100644 --- a/x-pack/plugins/remote_clusters/kibana.json +++ b/x-pack/plugins/remote_clusters/kibana.json @@ -7,7 +7,8 @@ ], "requiredPlugins": [ "licensing", - "management" + "management", + "indexManagement" ], "optionalPlugins": [ "usageCollection" From e9abe735f226cde41ec71321f4e7bbc623a93d7d Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Mon, 2 Mar 2020 09:36:14 -0700 Subject: [PATCH 11/26] [SIEM] Default the Timeline events filter to show All events (#58953) ## [SIEM] Default the Timeline events filter to show All events The Timeline events filter introduced in `7.6` to support the [detection engine](https://www.elastic.co/guide/en/siem/guide/current/detection-engine-overview.html) defaulted to filtering by `Raw events`, and thus required manually selecting `All events` or `Signal events` from the dropdown to view signals. The new default is `All events`, per the screenshots below: ### Before ![event-filter-before](https://user-images.githubusercontent.com/4459398/75593223-ecc61500-5a41-11ea-8d7d-8db5eccb1eb4.png) ### After ![event-filter-after](https://user-images.githubusercontent.com/4459398/75593238-f5b6e680-5a41-11ea-9e12-2fc1232f58d1.png) --- .../components/open_timeline/helpers.test.ts | 8 ++-- .../timeline/search_or_filter/pick_events.tsx | 1 + .../components/timeline/timeline.test.tsx | 43 +++++++++++++++++++ .../siem/public/store/timeline/defaults.ts | 2 +- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts index 120d644b3b33a..60ebd2578b7c0 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts @@ -236,7 +236,7 @@ describe('helpers', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, - eventType: 'raw', + eventType: 'all', filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -330,7 +330,7 @@ describe('helpers', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, - eventType: 'raw', + eventType: 'all', filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -417,7 +417,7 @@ describe('helpers', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, - eventType: 'raw', + eventType: 'all', filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -539,7 +539,7 @@ describe('helpers', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, - eventType: 'raw', + eventType: 'all', filters: [ { $state: { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx index 76f9e6fe3673a..3117bae745286 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx @@ -77,6 +77,7 @@ const PickEventTypeComponents: React.FC = ({ return ( { expect(wrapper.find('[data-test-subj="table-pagination"]').exists()).toEqual(false); }); + + test('it defaults to showing `All events`', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="pick-event-type"] button').text()).toEqual( + 'All events' + ); + }); }); describe('event wire up', () => { diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/defaults.ts b/x-pack/legacy/plugins/siem/public/store/timeline/defaults.ts index bbaf2a3fb6e30..7f04bb4c4dad0 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/defaults.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/defaults.ts @@ -14,7 +14,7 @@ export const timelineDefaults: SubsetTimelineModel & Pick Date: Mon, 2 Mar 2020 12:01:00 -0500 Subject: [PATCH 12/26] removing references to visTypes uiExports (#58337) --- src/legacy/core_plugins/kibana/public/kibana.js | 2 -- .../plugins/kbn_tp_run_pipeline/public/legacy.ts | 2 -- .../plugins/kbn_tp_custom_visualizations/index.js | 2 +- x-pack/legacy/plugins/canvas/public/legacy_start.ts | 3 --- .../plugins/dashboard_mode/public/dashboard_viewer.js | 4 +--- x-pack/legacy/plugins/lens/index.ts | 1 - x-pack/legacy/plugins/lens/public/legacy.ts | 7 +++++-- x-pack/legacy/plugins/lens/public/legacy_imports.ts | 2 ++ x-pack/legacy/plugins/lens/public/plugin.tsx | 7 ++++++- .../{register_vis_type_alias.ts => vis_type_alias.ts} | 4 ++-- x-pack/legacy/plugins/maps/index.js | 2 +- 11 files changed, 18 insertions(+), 18 deletions(-) rename x-pack/legacy/plugins/lens/public/{register_vis_type_alias.ts => vis_type_alias.ts} (89%) diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index a83d1176a7197..a9f32949628e9 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -26,8 +26,6 @@ import { npSetup } from 'ui/new_platform'; // import the uiExports that we want to "use" import 'uiExports/home'; -import 'uiExports/visTypes'; - import 'uiExports/visualize'; import 'uiExports/savedObjectTypes'; import 'uiExports/fieldFormatEditors'; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts index 39ce2b3077c96..a7cd313038d69 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts @@ -28,8 +28,6 @@ import 'ui/autoload/all'; // Used to run esaggs queries import 'uiExports/fieldFormats'; import 'uiExports/search'; -import 'uiExports/visRequestHandlers'; -import 'uiExports/visResponseHandlers'; // Used for kibana_context function import 'uiExports/savedObjectTypes'; diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js index e15da9daa3cd7..b2497a824ba2b 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js @@ -20,7 +20,7 @@ export default function(kibana) { return new kibana.Plugin({ uiExports: { - visTypes: ['plugins/kbn_tp_custom_visualizations/self_changing_vis/self_changing_vis'], + hacks: ['plugins/kbn_tp_custom_visualizations/self_changing_vis/self_changing_vis'], }, }); } diff --git a/x-pack/legacy/plugins/canvas/public/legacy_start.ts b/x-pack/legacy/plugins/canvas/public/legacy_start.ts index 21bf5aaa6d818..d7d1a940d3b43 100644 --- a/x-pack/legacy/plugins/canvas/public/legacy_start.ts +++ b/x-pack/legacy/plugins/canvas/public/legacy_start.ts @@ -8,9 +8,6 @@ // Import the uiExports that the application uses // These will go away as these plugins are converted to NP import 'ui/autoload/all'; -import 'uiExports/visTypes'; -import 'uiExports/visResponseHandlers'; -import 'uiExports/visRequestHandlers'; import 'uiExports/savedObjectTypes'; import 'uiExports/spyModes'; import 'uiExports/embeddableFactories'; diff --git a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js index e76a204a6f27d..62cd253ff24d9 100644 --- a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js @@ -15,9 +15,7 @@ import { uiModules } from 'ui/modules'; // import the uiExports that we want to "use" import 'uiExports/contextMenuActions'; -import 'uiExports/visTypes'; -import 'uiExports/visResponseHandlers'; -import 'uiExports/visRequestHandlers'; + import 'uiExports/inspectorViews'; import 'uiExports/interpreter'; import 'uiExports/savedObjectTypes'; diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts index bb0bf9b67ee2c..5eda6c4b4ff7a 100644 --- a/x-pack/legacy/plugins/lens/index.ts +++ b/x-pack/legacy/plugins/lens/index.ts @@ -33,7 +33,6 @@ export const lens: LegacyPluginInitializer = kibana => { embeddableFactories: [`plugins/${PLUGIN_ID}/legacy`], styleSheetPaths: resolve(__dirname, 'public/index.scss'), mappings, - visTypes: ['plugins/lens/register_vis_type_alias'], savedObjectsManagement: { lens: { defaultSearchField: 'title', diff --git a/x-pack/legacy/plugins/lens/public/legacy.ts b/x-pack/legacy/plugins/lens/public/legacy.ts index 8023bad34de66..1cfd3e198547d 100644 --- a/x-pack/legacy/plugins/lens/public/legacy.ts +++ b/x-pack/legacy/plugins/lens/public/legacy.ts @@ -5,12 +5,15 @@ */ import { npSetup, npStart } from 'ui/new_platform'; -import { getFormat } from './legacy_imports'; +import { getFormat, visualizations } from './legacy_imports'; export * from './types'; import { plugin } from './index'; const pluginInstance = plugin(); -pluginInstance.setup(npSetup.core, { ...npSetup.plugins, __LEGACY: { formatFactory: getFormat } }); +pluginInstance.setup(npSetup.core, { + ...npSetup.plugins, + __LEGACY: { formatFactory: getFormat, visualizations }, +}); pluginInstance.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/lens/public/legacy_imports.ts b/x-pack/legacy/plugins/lens/public/legacy_imports.ts index 9dcc22ddb1bb7..88f189fe3db5a 100644 --- a/x-pack/legacy/plugins/lens/public/legacy_imports.ts +++ b/x-pack/legacy/plugins/lens/public/legacy_imports.ts @@ -5,3 +5,5 @@ */ export { getFormat, FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; +export { setup as visualizations } from '../../../../../src/legacy/core_plugins/visualizations/public/np_ready/public/legacy'; +export { VisualizationsSetup } from '../../../../../src/legacy/core_plugins/visualizations/public'; diff --git a/x-pack/legacy/plugins/lens/public/plugin.tsx b/x-pack/legacy/plugins/lens/public/plugin.tsx index 634d227559835..7f96268fc2e8c 100644 --- a/x-pack/legacy/plugins/lens/public/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/plugin.tsx @@ -38,6 +38,8 @@ import { import { FormatFactory } from './legacy_imports'; import { IEmbeddableSetup, IEmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { EditorFrameStart } from './types'; +import { getLensAliasConfig } from './vis_type_alias'; +import { VisualizationsSetup } from './legacy_imports'; export interface LensPluginSetupDependencies { kibanaLegacy: KibanaLegacySetup; @@ -46,6 +48,7 @@ export interface LensPluginSetupDependencies { embeddable: IEmbeddableSetup; __LEGACY: { formatFactory: FormatFactory; + visualizations: VisualizationsSetup; }; } @@ -81,7 +84,7 @@ export class LensPlugin { expressions, data, embeddable, - __LEGACY: { formatFactory }, + __LEGACY: { formatFactory, visualizations }, }: LensPluginSetupDependencies ) { const editorFrameSetupInterface = this.editorFrameService.setup(core, { @@ -100,6 +103,8 @@ export class LensPlugin { this.datatableVisualization.setup(core, dependencies); this.metricVisualization.setup(core, dependencies); + visualizations.types.registerAlias(getLensAliasConfig()); + kibanaLegacy.registerLegacyApp({ id: 'lens', title: NOT_INTERNATIONALIZED_PRODUCT_NAME, diff --git a/x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts b/x-pack/legacy/plugins/lens/public/vis_type_alias.ts similarity index 89% rename from x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts rename to x-pack/legacy/plugins/lens/public/vis_type_alias.ts index f71796268065b..c4e0a20110c81 100644 --- a/x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts +++ b/x-pack/legacy/plugins/lens/public/vis_type_alias.ts @@ -5,10 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { setup as visualizations } from '../../../../../src/legacy/core_plugins/visualizations/public/np_ready/public/legacy'; import { getBasePath, getEditPath } from '../../../../plugins/lens/common'; +import { VisTypeAlias } from '../../../../../src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types'; -visualizations.types.registerAlias({ +export const getLensAliasConfig = (): VisTypeAlias => ({ aliasUrl: getBasePath(), name: 'lens', promotion: { diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index 5cd5a8731a703..8048c21fe9333 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -78,7 +78,7 @@ export function maps(kibana) { }, mappings, migrations, - visTypes: ['plugins/maps/register_vis_type_alias'], + hacks: ['plugins/maps/register_vis_type_alias'], }, config(Joi) { return Joi.object({ From 4696736528adb98e6f3ab3f872c3e14e67ca0873 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 2 Mar 2020 13:26:12 -0500 Subject: [PATCH 13/26] Disallow duplicate percentiles (#57444) (#58299) Added an optional validation step to the number_list component to disallow duplicates, Reworked and consolidated number_list component validations into one method and enabled this option in the percentiles editor. Added unit / integration tests --- .../number_list/number_list.test.tsx | 20 +++++ .../components/number_list/number_list.tsx | 39 ++++------ .../components/number_list/utils.test.ts | 69 +++++++++-------- .../controls/components/number_list/utils.ts | 77 +++++++++++++++---- .../components/controls/percentile_ranks.tsx | 1 + .../components/controls/percentiles.test.tsx | 64 +++++++++++++++ .../components/controls/percentiles.tsx | 2 +- 7 files changed, 199 insertions(+), 73 deletions(-) create mode 100644 src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.test.tsx diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx index 3faf164c365d9..f547f1dee6a39 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx @@ -63,12 +63,31 @@ describe('NumberList', () => { test('should show an order error', () => { defaultProps.numberArray = [3, 1]; + defaultProps.validateAscendingOrder = true; defaultProps.showValidation = true; const comp = mountWithIntl(); expect(comp.find('EuiFormErrorText').length).toBe(1); }); + test('should show a duplicate error', () => { + defaultProps.numberArray = [3, 1, 3]; + defaultProps.disallowDuplicates = true; + defaultProps.showValidation = true; + const comp = mountWithIntl(); + + expect(comp.find('EuiFormErrorText').length).toBeGreaterThan(0); + }); + + test('should show many duplicate errors', () => { + defaultProps.numberArray = [3, 1, 3, 1, 3, 1, 3, 1]; + defaultProps.disallowDuplicates = true; + defaultProps.showValidation = true; + const comp = mountWithIntl(); + + expect(comp.find('EuiFormErrorText').length).toBe(6); + }); + test('should set validity as true', () => { mountWithIntl(); @@ -77,6 +96,7 @@ describe('NumberList', () => { test('should set validity as false when the order is invalid', () => { defaultProps.numberArray = [3, 2]; + defaultProps.validateAscendingOrder = true; const comp = mountWithIntl(); expect(defaultProps.setValidity).lastCalledWith(false); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx index 8e290ceedfeac..a43c66c2e08cc 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx @@ -21,17 +21,14 @@ import React, { Fragment, useState, useEffect, useMemo, useCallback } from 'reac import { EuiSpacer, EuiButtonEmpty, EuiFlexItem, EuiFormErrorText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; import { NumberRow, NumberRowModel } from './number_row'; import { parse, EMPTY_STRING, getRange, - validateOrder, - validateValue, getNextModel, getInitModelList, - getUpdatedModels, + getValidatedModels, hasInvalidValues, } from './utils'; import { useValidation } from '../../utils'; @@ -41,6 +38,7 @@ export interface NumberListProps { numberArray: Array; range?: string; showValidation: boolean; + disallowDuplicates?: boolean; unitName: string; validateAscendingOrder?: boolean; onChange(list: Array): void; @@ -54,31 +52,27 @@ function NumberList({ range, showValidation, unitName, - validateAscendingOrder = true, + validateAscendingOrder = false, + disallowDuplicates = false, onChange, setTouched, setValidity, }: NumberListProps) { const numberRange = useMemo(() => getRange(range), [range]); const [models, setModels] = useState(getInitModelList(numberArray)); - const [ascendingError, setAscendingError] = useState(EMPTY_STRING); // set up validity for each model useEffect(() => { - let id: number | undefined; - if (validateAscendingOrder) { - const { isValidOrder, modelIndex } = validateOrder(numberArray); - id = isValidOrder ? undefined : modelIndex; - setAscendingError( - isValidOrder - ? EMPTY_STRING - : i18n.translate('visDefaultEditor.controls.numberList.invalidAscOrderErrorMessage', { - defaultMessage: 'The values should be in ascending order.', - }) - ); - } - setModels(state => getUpdatedModels(numberArray, state, numberRange, id)); - }, [numberArray, numberRange, validateAscendingOrder]); + setModels(state => + getValidatedModels( + numberArray, + state, + numberRange, + validateAscendingOrder, + disallowDuplicates + ) + ); + }, [numberArray, numberRange, validateAscendingOrder, disallowDuplicates]); // responsible for setting up an initial value ([0]) when there is no default value useEffect(() => { @@ -105,12 +99,10 @@ function NumberList({ onUpdate( models.map(model => { if (model.id === id) { - const { isInvalid, error } = validateValue(parsedValue, numberRange); return { id, value: parsedValue, - isInvalid, - error, + isInvalid: false, }; } return model; @@ -155,7 +147,6 @@ function NumberList({ {models.length - 1 !== arrayIndex && } ))} - {showValidation && ascendingError && {ascendingError}} diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.test.ts b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.test.ts index 89fb5738db379..9cffaadfc956d 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.test.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.test.ts @@ -19,13 +19,12 @@ import { getInitModelList, - getUpdatedModels, - validateOrder, hasInvalidValues, parse, validateValue, getNextModel, getRange, + getValidatedModels, } from './utils'; import { NumberListRange } from './range'; import { NumberRowModel } from './number_row'; @@ -33,6 +32,7 @@ import { NumberRowModel } from './number_row'; describe('NumberList utils', () => { let modelList: NumberRowModel[]; let range: NumberListRange; + let invalidEntry: NumberRowModel; beforeEach(() => { modelList = [ @@ -46,6 +46,12 @@ describe('NumberList utils', () => { maxInclusive: true, within: jest.fn(() => true), }; + invalidEntry = { + value: expect.any(Number), + isInvalid: true, + error: expect.any(String), + id: expect.any(String), + }; }); describe('getInitModelList', () => { @@ -65,27 +71,27 @@ describe('NumberList utils', () => { }); }); - describe('getUpdatedModels', () => { + describe('getValidatedModels', () => { test('should return model list when number list is empty', () => { - const updatedModelList = getUpdatedModels([], modelList, range); + const updatedModelList = getValidatedModels([], modelList, range); expect(updatedModelList).toEqual([{ value: 0, id: expect.any(String), isInvalid: false }]); }); test('should not update model list when number list is the same', () => { - const updatedModelList = getUpdatedModels([1, 2], modelList, range); + const updatedModelList = getValidatedModels([1, 2], modelList, range); expect(updatedModelList).toEqual(modelList); }); test('should update model list when number list was changed', () => { - const updatedModelList = getUpdatedModels([1, 3], modelList, range); + const updatedModelList = getValidatedModels([1, 3], modelList, range); modelList[1].value = 3; expect(updatedModelList).toEqual(modelList); }); test('should update model list when number list increased', () => { - const updatedModelList = getUpdatedModels([1, 2, 3], modelList, range); + const updatedModelList = getValidatedModels([1, 2, 3], modelList, range); expect(updatedModelList).toEqual([ ...modelList, { value: 3, id: expect.any(String), isInvalid: false }, @@ -93,45 +99,46 @@ describe('NumberList utils', () => { }); test('should update model list when number list decreased', () => { - const updatedModelList = getUpdatedModels([2], modelList, range); + const updatedModelList = getValidatedModels([2], modelList, range); expect(updatedModelList).toEqual([{ value: 2, id: '1', isInvalid: false }]); }); test('should update model list when number list has undefined value', () => { - const updatedModelList = getUpdatedModels([1, undefined], modelList, range); + const updatedModelList = getValidatedModels([1, undefined], modelList, range); modelList[1].value = ''; modelList[1].isInvalid = true; expect(updatedModelList).toEqual(modelList); }); - test('should update model list when number order is invalid', () => { - const updatedModelList = getUpdatedModels([1, 3, 2], modelList, range, 2); - expect(updatedModelList).toEqual([ - modelList[0], - { ...modelList[1], value: 3 }, - { value: 2, id: expect.any(String), isInvalid: true }, - ]); + test('should identify when a number is out of order', () => { + const updatedModelList = getValidatedModels([1, 3, 2], modelList, range, true); + expect(updatedModelList[2]).toEqual(invalidEntry); }); - }); - describe('validateOrder', () => { - test('should return true when order is valid', () => { - expect(validateOrder([1, 2])).toEqual({ - isValidOrder: true, - }); + test('should identify when many numbers are out of order', () => { + const updatedModelList = getValidatedModels([1, 3, 2, 3, 4, 2], modelList, range, true); + expect(updatedModelList[2]).toEqual(invalidEntry); + expect(updatedModelList[5]).toEqual(invalidEntry); }); - test('should return true when a number is undefined', () => { - expect(validateOrder([1, undefined])).toEqual({ - isValidOrder: true, - }); + test('should identify a duplicate', () => { + const updatedModelList = getValidatedModels([1, 2, 3, 6, 2], modelList, range, false, true); + expect(updatedModelList[4]).toEqual(invalidEntry); }); - test('should return false when order is invalid', () => { - expect(validateOrder([2, 1])).toEqual({ - isValidOrder: false, - modelIndex: 1, - }); + test('should identify many duplicates', () => { + const updatedModelList = getValidatedModels( + [2, 2, 2, 3, 4, 5, 2, 2, 3], + modelList, + range, + false, + true + ); + expect(updatedModelList[1]).toEqual(invalidEntry); + expect(updatedModelList[2]).toEqual(invalidEntry); + expect(updatedModelList[6]).toEqual(invalidEntry); + expect(updatedModelList[7]).toEqual(invalidEntry); + expect(updatedModelList[8]).toEqual(invalidEntry); }); }); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts index e0f32366fc265..c2ac63c98cbea 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts @@ -49,6 +49,7 @@ function validateValue(value: number | '', numberRange: NumberListRange) { if (value === EMPTY_STRING) { result.isInvalid = true; + result.error = EMPTY_STRING; } else if (!numberRange.within(value)) { result.isInvalid = true; result.error = i18n.translate('visDefaultEditor.controls.numberList.invalidRangeErrorMessage', { @@ -60,19 +61,46 @@ function validateValue(value: number | '', numberRange: NumberListRange) { return result; } -function validateOrder(list: Array) { - const result: { isValidOrder: boolean; modelIndex?: number } = { - isValidOrder: true, +function validateValueAscending( + inputValue: number | '', + index: number, + list: Array +) { + const result: { isInvalidOrder: boolean; error?: string } = { + isInvalidOrder: false, }; - list.forEach((inputValue, index, array) => { - const previousModel = array[index - 1]; - if (previousModel !== undefined && inputValue !== undefined && inputValue <= previousModel) { - result.isValidOrder = false; - result.modelIndex = index; - } - }); + const previousModel = list[index - 1]; + if (previousModel !== undefined && inputValue !== undefined && inputValue <= previousModel) { + result.isInvalidOrder = true; + result.error = i18n.translate( + 'visDefaultEditor.controls.numberList.invalidAscOrderErrorMessage', + { + defaultMessage: 'Value is not in ascending order.', + } + ); + } + return result; +} + +function validateValueUnique( + inputValue: number | '', + index: number, + list: Array +) { + const result: { isDuplicate: boolean; error?: string } = { + isDuplicate: false, + }; + if (inputValue && list.indexOf(inputValue) !== index) { + result.isDuplicate = true; + result.error = i18n.translate( + 'visDefaultEditor.controls.numberList.duplicateValueErrorMessage', + { + defaultMessage: 'Duplicate value.', + } + ); + } return result; } @@ -101,11 +129,12 @@ function getInitModelList(list: Array): NumberRowModel[] { : [defaultModel]; } -function getUpdatedModels( +function getValidatedModels( numberList: Array, modelList: NumberRowModel[], numberRange: NumberListRange, - invalidOrderModelIndex?: number + validateAscendingOrder: boolean = false, + disallowDuplicates: boolean = false ): NumberRowModel[] { if (!numberList.length) { return [defaultModel]; @@ -113,12 +142,27 @@ function getUpdatedModels( return numberList.map((number, index) => { const model = modelList[index] || { id: generateId() }; const newValue: NumberRowModel['value'] = number === undefined ? EMPTY_STRING : number; - const { isInvalid, error } = validateValue(newValue, numberRange); + + const valueResult = numberRange ? validateValue(newValue, numberRange) : { isInvalid: false }; + + const ascendingResult = validateAscendingOrder + ? validateValueAscending(newValue, index, numberList) + : { isInvalidOrder: false }; + + const duplicationResult = disallowDuplicates + ? validateValueUnique(newValue, index, numberList) + : { isDuplicate: false }; + + const allErrors = [valueResult.error, ascendingResult.error, duplicationResult.error] + .filter(Boolean) + .join(' '); + return { ...model, value: newValue, - isInvalid: invalidOrderModelIndex === index ? true : isInvalid, - error, + isInvalid: + valueResult.isInvalid || ascendingResult.isInvalidOrder || duplicationResult.isDuplicate, + error: allErrors === EMPTY_STRING ? undefined : allErrors, }; }); } @@ -132,9 +176,8 @@ export { parse, getRange, validateValue, - validateOrder, getNextModel, getInitModelList, - getUpdatedModels, + getValidatedModels, hasInvalidValues, }; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentile_ranks.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentile_ranks.tsx index c6057b7ce2a99..fb7d8d78b28e3 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentile_ranks.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentile_ranks.tsx @@ -62,6 +62,7 @@ function PercentileRanksEditor({ unitName={i18n.translate('visDefaultEditor.controls.percentileRanks.valueUnitNameText', { defaultMessage: 'value', })} + validateAscendingOrder={true} showValidation={showValidation} onChange={setValue} setTouched={setTouched} diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.test.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.test.tsx new file mode 100644 index 0000000000000..020dbb351b497 --- /dev/null +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.test.tsx @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { AggParamEditorProps } from '../agg_param_props'; +import { IAggConfig } from '../../legacy_imports'; +import { VisState } from 'src/legacy/core_plugins/visualizations/public'; +import { mount } from 'enzyme'; +import { PercentilesEditor } from './percentiles'; + +describe('PercentilesEditor component', () => { + let setValue: jest.Mock; + let setValidity: jest.Mock; + let setTouched: jest.Mock; + let defaultProps: AggParamEditorProps>; + + beforeEach(() => { + setValue = jest.fn(); + setValidity = jest.fn(); + setTouched = jest.fn(); + + defaultProps = { + agg: {} as IAggConfig, + aggParam: {} as any, + formIsTouched: false, + value: [1, 5, 25, 50, 75, 95, 99], + editorConfig: {}, + showValidation: false, + setValue, + setValidity, + setTouched, + state: {} as VisState, + metricAggs: [] as IAggConfig[], + }; + }); + + it('should set valid state to true after adding a unique percentile', () => { + defaultProps.value = [1, 5, 25, 50, 70]; + mount(); + expect(setValidity).lastCalledWith(true); + }); + + it('should set valid state to false after adding a duplicate percentile', () => { + defaultProps.value = [1, 5, 25, 50, 50]; + mount(); + expect(setValidity).lastCalledWith(false); + }); +}); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.tsx index 74e7957bc1944..9f1f26fe5446f 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.tsx @@ -58,7 +58,7 @@ function PercentilesEditor({ labelledbyId={`visEditorPercentileLabel${agg.id}-legend`} numberArray={value} range="[0,100]" - validateAscendingOrder={false} + disallowDuplicates={true} unitName={i18n.translate('visDefaultEditor.controls.percentileRanks.percentUnitNameText', { defaultMessage: 'percent', })} From 2998ec06fe9ded381fe637ab9940dc20a3742aea Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 2 Mar 2020 11:47:35 -0700 Subject: [PATCH 14/26] [Maps] direct Discover "visualize" to open Maps application (#58549) * [Maps] direct Discover visualize to Maps application * pass initial layers to maps app * add functional test * fix parentheses messed up by lint fix * fix i18n expression * move logic into lib * fix typescript errors * use constant for geo_point and geo_shape, more TS noise * use encode_array in an attempt to make TS happy * another round of TS changes * one more thing Co-authored-by: Elastic Machine --- .../np_ready/angular/context_state.ts | 5 +- .../components/field_chooser/field_chooser.js | 17 ++- .../lib/detail_views/string.html | 4 +- .../field_chooser/lib/visualize_url_utils.ts | 108 ++++++++++++++++++ test/functional/page_objects/discover_page.js | 6 - .../maps/public/angular/get_initial_layers.js | 8 +- .../maps/public/angular/map_controller.js | 29 ++++- x-pack/test/functional/apps/maps/discover.js | 50 ++++++++ x-pack/test/functional/apps/maps/index.js | 1 + 9 files changed, 210 insertions(+), 18 deletions(-) create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts create mode 100644 x-pack/test/functional/apps/maps/discover.js diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts index 8fb6140d55e31..bf185f78941de 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts @@ -24,9 +24,9 @@ import { syncStates, BaseStateContainer, } from '../../../../../../../plugins/kibana_utils/public'; -import { esFilters, FilterManager, Filter } from '../../../../../../../plugins/data/public'; +import { esFilters, FilterManager, Filter, Query } from '../../../../../../../plugins/data/public'; -interface AppState { +export interface AppState { /** * Columns displayed in the table, cannot be changed by UI, just in discover's main app */ @@ -47,6 +47,7 @@ interface AppState { * Number of records to be fetched after the anchor records (older records) */ successorCount: number; + query?: Query; } interface GlobalState { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js index a175a1aebebdf..df970ab5f2584 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js @@ -24,7 +24,11 @@ import './discover_field'; import './discover_field_search_directive'; import './discover_index_pattern_directive'; import fieldChooserTemplate from './field_chooser.html'; -import { IndexPatternFieldList } from '../../../../../../../../plugins/data/public'; +import { + IndexPatternFieldList, + KBN_FIELD_TYPES, +} from '../../../../../../../../plugins/data/public'; +import { getMapsAppUrl, isFieldVisualizable, isMapsAppRegistered } from './lib/visualize_url_utils'; export function createFieldChooserDirective($location, config, $route) { return { @@ -186,8 +190,15 @@ export function createFieldChooserDirective($location, config, $route) { return ''; } + if ( + (field.type === KBN_FIELD_TYPES.GEO_POINT || field.type === KBN_FIELD_TYPES.GEO_SHAPE) && + isMapsAppRegistered() + ) { + return getMapsAppUrl(field, $scope.indexPattern, $scope.state, $scope.columns); + } + let agg = {}; - const isGeoPoint = field.type === 'geo_point'; + const isGeoPoint = field.type === KBN_FIELD_TYPES.GEO_POINT; const type = isGeoPoint ? 'tile_map' : 'histogram'; // If we're visualizing a date field, and our index is time based (and thus has a time filter), // then run a date histogram @@ -243,7 +254,7 @@ export function createFieldChooserDirective($location, config, $route) { $scope.computeDetails = function(field, recompute) { if (_.isUndefined(field.details) || recompute) { field.details = { - visualizeUrl: field.visualizable ? getVisualizeUrl(field) : null, + visualizeUrl: isFieldVisualizable(field) ? getVisualizeUrl(field) : null, ...fieldCalculator.getFieldValueCounts({ hits: $scope.hits, field: field, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html index 5d134911fc91b..333dc472e956d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html @@ -79,7 +79,7 @@ @@ -87,7 +87,7 @@ diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts new file mode 100644 index 0000000000000..8dbf3cd79ccb1 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts @@ -0,0 +1,108 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import uuid from 'uuid/v4'; +// @ts-ignore +import rison from 'rison-node'; +import { + IFieldType, + IIndexPattern, + KBN_FIELD_TYPES, +} from '../../../../../../../../../plugins/data/public'; +import { AppState } from '../../../angular/context_state'; +import { getServices } from '../../../../kibana_services'; + +function getMapsAppBaseUrl() { + const mapsAppVisAlias = getServices() + .visualizations.types.getAliases() + .find(({ name }) => { + return name === 'maps'; + }); + return mapsAppVisAlias ? mapsAppVisAlias.aliasUrl : null; +} + +export function isMapsAppRegistered() { + return getServices() + .visualizations.types.getAliases() + .some(({ name }) => { + return name === 'maps'; + }); +} + +export function isFieldVisualizable(field: IFieldType) { + if ( + (field.type === KBN_FIELD_TYPES.GEO_POINT || field.type === KBN_FIELD_TYPES.GEO_SHAPE) && + isMapsAppRegistered() + ) { + return true; + } + return field.visualizable; +} + +export function getMapsAppUrl( + field: IFieldType, + indexPattern: IIndexPattern, + appState: AppState, + columns: string[] +) { + const mapAppParams = new URLSearchParams(); + + // Copy global state + const locationSplit = window.location.href.split('discover?'); + if (locationSplit.length > 1) { + const discoverParams = new URLSearchParams(locationSplit[1]); + const globalStateUrlValue = discoverParams.get('_g'); + if (globalStateUrlValue) { + mapAppParams.set('_g', globalStateUrlValue); + } + } + + // Copy filters and query in app state + const mapsAppState: any = { + filters: appState.filters || [], + }; + if (appState.query) { + mapsAppState.query = appState.query; + } + // @ts-ignore + mapAppParams.set('_a', rison.encode(mapsAppState)); + + // create initial layer descriptor + const hasColumns = columns && columns.length && columns[0] !== '_source'; + mapAppParams.set( + 'initialLayers', + // @ts-ignore + rison.encode_array([ + { + id: uuid(), + label: indexPattern.title, + sourceDescriptor: { + id: uuid(), + type: 'ES_SEARCH', + geoField: field.name, + tooltipProperties: hasColumns ? columns : [], + indexPatternId: indexPattern.id, + }, + visible: true, + type: 'VECTOR', + }, + ]) + ); + + return getServices().addBasePath(`${getMapsAppBaseUrl()}?${mapAppParams.toString()}`); +} diff --git a/test/functional/page_objects/discover_page.js b/test/functional/page_objects/discover_page.js index 5ccc5625849d2..080b8c8ee753f 100644 --- a/test/functional/page_objects/discover_page.js +++ b/test/functional/page_objects/discover_page.js @@ -206,12 +206,6 @@ export function DiscoverPageProvider({ getService, getPageObjects }) { return await testSubjects.getVisibleText('discoverQueryHits'); } - async query(queryString) { - await find.setValue('input[aria-label="Search input"]', queryString); - await find.clickByCssSelector('button[aria-label="Search"]'); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - async getDocHeader() { const header = await find.byCssSelector('thead > tr:nth-child(1)'); return await header.getVisibleText(); diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js index 45ee441716769..3cae75231d28e 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js +++ b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js @@ -9,7 +9,7 @@ import { EMSTMSSource } from '../layers/sources/ems_tms_source'; import chrome from 'ui/chrome'; import { getKibanaTileMap } from '../meta'; -export function getInitialLayers(layerListJSON) { +export function getInitialLayers(layerListJSON, initialLayers = []) { if (layerListJSON) { return JSON.parse(layerListJSON); } @@ -19,7 +19,7 @@ export function getInitialLayers(layerListJSON) { const sourceDescriptor = KibanaTilemapSource.createDescriptor(); const source = new KibanaTilemapSource(sourceDescriptor); const layer = source.createDefaultLayer(); - return [layer.toLayerDescriptor()]; + return [layer.toLayerDescriptor(), ...initialLayers]; } const isEmsEnabled = chrome.getInjected('isEmsEnabled', true); @@ -27,8 +27,8 @@ export function getInitialLayers(layerListJSON) { const descriptor = EMSTMSSource.createDescriptor({ isAutoSelect: true }); const source = new EMSTMSSource(descriptor); const layer = source.createDefaultLayer(); - return [layer.toLayerDescriptor()]; + return [layer.toLayerDescriptor(), ...initialLayers]; } - return []; + return initialLayers; } diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 95c8ff975b1d6..a8e9ae46a3b9a 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -6,6 +6,7 @@ import _ from 'lodash'; import chrome from 'ui/chrome'; +import rison from 'rison-node'; import 'ui/directives/listen'; import 'ui/directives/storage'; import React from 'react'; @@ -66,6 +67,32 @@ const REACT_ANCHOR_DOM_ELEMENT_ID = 'react-maps-root'; const app = uiModules.get(MAP_APP_PATH, []); +function getInitialLayersFromUrlParam() { + const locationSplit = window.location.href.split('?'); + if (locationSplit.length <= 1) { + return []; + } + const mapAppParams = new URLSearchParams(locationSplit[1]); + if (!mapAppParams.has('initialLayers')) { + return []; + } + + try { + return rison.decode_array(mapAppParams.get('initialLayers')); + } catch (e) { + toastNotifications.addWarning({ + title: i18n.translate('xpack.maps.initialLayers.unableToParseTitle', { + defaultMessage: `Inital layers not added to map`, + }), + text: i18n.translate('xpack.maps.initialLayers.unableToParseMessage', { + defaultMessage: `Unable to parse contents of 'initialLayers' parameter. Error: {errorMsg}`, + values: { errorMsg: e.message }, + }), + }); + return []; + } +} + app.controller( 'GisMapController', ($scope, $route, kbnUrl, localStorage, AppState, globalState) => { @@ -333,7 +360,7 @@ app.controller( store.dispatch(setOpenTOCDetails(_.get(uiState, 'openTOCDetails', []))); } - const layerList = getInitialLayers(savedMap.layerListJSON); + const layerList = getInitialLayers(savedMap.layerListJSON, getInitialLayersFromUrlParam()); initialLayerListConfig = copyPersistentState(layerList); store.dispatch(replaceLayerList(layerList)); store.dispatch(setRefreshConfig($scope.refreshConfig)); diff --git a/x-pack/test/functional/apps/maps/discover.js b/x-pack/test/functional/apps/maps/discover.js new file mode 100644 index 0000000000000..ce33596476755 --- /dev/null +++ b/x-pack/test/functional/apps/maps/discover.js @@ -0,0 +1,50 @@ +/* + * 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 expect from '@kbn/expect'; + +export default function({ getService, getPageObjects }) { + const queryBar = getService('queryBar'); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'maps', 'timePicker']); + + describe('discover visualize button', () => { + beforeEach(async () => { + await PageObjects.common.navigateToApp('discover'); + }); + + it('should link geo_shape fields to Maps application', async () => { + await PageObjects.discover.selectIndexPattern('geo_shapes*'); + await PageObjects.discover.clickFieldListItem('geometry'); + await PageObjects.discover.clickFieldListItemVisualize('geometry'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + const doesLayerExist = await PageObjects.maps.doesLayerExist('geo_shapes*'); + expect(doesLayerExist).to.equal(true); + const hits = await PageObjects.maps.getHits(); + expect(hits).to.equal('4'); + }); + + it('should link geo_point fields to Maps application with time and query context', async () => { + await PageObjects.discover.selectIndexPattern('logstash-*'); + await PageObjects.timePicker.setAbsoluteRange( + 'Sep 22, 2015 @ 00:00:00.000', + 'Sep 22, 2015 @ 04:00:00.000' + ); + await queryBar.setQuery('machine.os.raw : "ios"'); + await queryBar.submitQuery(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.discover.clickFieldListItem('geo.coordinates'); + await PageObjects.discover.clickFieldListItemVisualize('geo.coordinates'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + const doesLayerExist = await PageObjects.maps.doesLayerExist('logstash-*'); + expect(doesLayerExist).to.equal(true); + const hits = await PageObjects.maps.getHits(); + expect(hits).to.equal('7'); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 0545fcd1b6453..e8a9d7ba54bc5 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -45,6 +45,7 @@ export default function({ loadTestFile, getService }) { loadTestFile(require.resolve('./import_geojson')); loadTestFile(require.resolve('./layer_errors')); loadTestFile(require.resolve('./embeddable')); + loadTestFile(require.resolve('./discover')); }); }); } From 17b3d8036914f67a6a71e24f7cc6bd42852195b7 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 2 Mar 2020 20:14:24 +0100 Subject: [PATCH 15/26] improve graph missing workspace error message (#58876) --- x-pack/legacy/plugins/graph/public/app.js | 20 ++++++++++++------- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index 7010e1fa773ea..df968681a38e2 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -132,14 +132,20 @@ export function initGraphApp(angularModule, deps) { template: appTemplate, badge: getReadonlyBadge, resolve: { - savedWorkspace: function($route) { + savedWorkspace: function($rootScope, $route, $location) { return $route.current.params.id - ? savedWorkspaceLoader.get($route.current.params.id).catch(function() { - toastNotifications.addDanger( - i18n.translate('xpack.graph.missingWorkspaceErrorMessage', { - defaultMessage: 'Missing workspace', - }) - ); + ? savedWorkspaceLoader.get($route.current.params.id).catch(function(e) { + toastNotifications.addError(e, { + title: i18n.translate('xpack.graph.missingWorkspaceErrorMessage', { + defaultMessage: "Couldn't load graph with ID", + }), + }); + $rootScope.$eval(() => { + $location.path('/home'); + $location.replace(); + }); + // return promise that never returns to prevent the controller from loading + return new Promise(); }) : savedWorkspaceLoader.get(); }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b99a54160bb65..21500c4db9c34 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5734,7 +5734,6 @@ "xpack.graph.listing.table.entityNamePlural": "グラフ", "xpack.graph.listing.table.titleColumnName": "タイトル", "xpack.graph.loadWorkspace.missingIndexPatternErrorMessage": "インデックスパターンが見つかりませんでした", - "xpack.graph.missingWorkspaceErrorMessage": "ワークスペースがありません", "xpack.graph.newGraphTitle": "保存されていないグラフ", "xpack.graph.noDataSourceNotificationMessageText": "データソースが見つかりませんでした。{managementIndexPatternsLink} に移動して Elasticsearch インデックスのインデックスパターンを作成してください。", "xpack.graph.noDataSourceNotificationMessageText.managementIndexPatternLinkText": "管理>インデックスパターン", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index bae8fef5ff280..c9e7ea1ec80de 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5734,7 +5734,6 @@ "xpack.graph.listing.table.entityNamePlural": "图表", "xpack.graph.listing.table.titleColumnName": "标题", "xpack.graph.loadWorkspace.missingIndexPatternErrorMessage": "未找到索引模式", - "xpack.graph.missingWorkspaceErrorMessage": "缺少工作空间", "xpack.graph.newGraphTitle": "未保存图表", "xpack.graph.noDataSourceNotificationMessageText": "未找到数据源。前往 {managementIndexPatternsLink},为您的 Elasticsearch 索引创建索引模式。", "xpack.graph.noDataSourceNotificationMessageText.managementIndexPatternLinkText": "管理 > 索引模式", From cbbc963001be1d200c97a9678d59ac0750a3b7c1 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 2 Mar 2020 20:14:37 +0100 Subject: [PATCH 16/26] show timepicker in timelion and tsvb (#58857) --- .../kibana/public/visualize/np_ready/editor/editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index 415949f88e9d1..2137e413451d2 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -382,7 +382,7 @@ function VisualizeAppController( $scope.showQueryBarTimePicker = () => { // tsvb loads without an indexPattern initially (TODO investigate). // hide timefilter only if timeFieldName is explicitly undefined. - const hasTimeField = $scope.indexPattern ? !!$scope.indexPattern.timeFieldName : true; + const hasTimeField = vis.indexPattern ? !!vis.indexPattern.timeFieldName : true; return vis.type.options.showTimePicker && hasTimeField; }; From a6b166b69c148cbd3c10ddc96bce10a25c6c5476 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 2 Mar 2020 20:14:51 +0100 Subject: [PATCH 17/26] put params into short url instead of behind it (#58846) --- .../share/public/components/url_panel_content.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/plugins/share/public/components/url_panel_content.tsx b/src/plugins/share/public/components/url_panel_content.tsx index d0d4ce55dc1ac..2b77b6f4592a8 100644 --- a/src/plugins/share/public/components/url_panel_content.tsx +++ b/src/plugins/share/public/components/url_panel_content.tsx @@ -183,7 +183,11 @@ export class UrlPanelContent extends Component { }; private getSnapshotUrl = () => { - return this.props.shareableUrl || window.location.href; + let url = this.props.shareableUrl || window.location.href; + if (this.props.isEmbedded) { + url = this.makeUrlEmbeddable(url); + } + return url; }; private makeUrlEmbeddable = (url: string) => { @@ -200,8 +204,7 @@ export class UrlPanelContent extends Component { return; } - const embeddableUrl = this.makeUrlEmbeddable(url); - return ``; + return ``; }; private setUrl = () => { From 2cf863c27b954c593ffc298d7f4524f328d07d7a Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Mon, 2 Mar 2020 14:38:43 -0500 Subject: [PATCH 18/26] [Advanced Settings] Fix a11y of unsaved indicator (#58511) * [Advanced Settings] Fix a11y of unsaved indicator - Reduced size of the indicator bar on the left - Added icons with tooltips to indicated unsaved and invalid states * Snaps * Fix mobile view of bottom bar --- .../management_app/advanced_settings.scss | 30 +- .../field/__snapshots__/field.test.tsx.snap | 432 +++++++++++++++--- .../management_app/components/field/field.tsx | 29 +- .../management_app/components/form/form.tsx | 90 ++-- 4 files changed, 468 insertions(+), 113 deletions(-) diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.scss b/src/plugins/advanced_settings/public/management_app/advanced_settings.scss index 016edb2817da8..66ae9cca3f83b 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.scss +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.scss @@ -22,40 +22,42 @@ margin-top: $euiSize; } + .mgtAdvancedSettings__fieldTitle { + padding-left: $euiSizeS; + margin-left: -$euiSizeS; + } - padding-left: $euiSizeS; - margin-left: -$euiSizeS; - &--unsaved { + &--unsaved .mgtAdvancedSettings__fieldTitle { // Simulates a left side border without shifting content - box-shadow: -$euiSizeXS 0px $euiColorSecondary; + box-shadow: -$euiSizeXS 0px $euiColorWarning; } - &--invalid { + &--invalid .mgtAdvancedSettings__fieldTitle { // Simulates a left side border without shifting content box-shadow: -$euiSizeXS 0px $euiColorDanger; } - @include internetExplorerOnly() { - min-height: 1px; - } - &Row { - padding-left: $euiSizeS; - } @include internetExplorerOnly { + min-height: 1px; + &Row { min-height: 1px; } } } +.mgtAdvancedSettings__fieldTitleUnsavedIcon { + margin-left: $euiSizeS; +} + .mgtAdvancedSettingsForm__unsavedCount { - @include euiBreakpoint('xs', 's') { + @include euiBreakpoint('xs') { display: none; } } -.mgtAdvancedSettingsForm__unsavedCountMessage{ +.mgtAdvancedSettingsForm__unsavedCountMessage { // Simulates a left side border without shifting content - box-shadow: -$euiSizeXS 0px $euiColorSecondary; + box-shadow: -$euiSizeXS 0px $euiColorWarning; padding-left: $euiSizeS; } diff --git a/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap index 2f4d806e60244..dba1678339f24 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap +++ b/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap @@ -17,7 +17,12 @@ exports[`Field for array setting should render as read only if saving is disable fullWidth={true} title={

- Array test setting + + Array test setting + +

} @@ -84,7 +89,12 @@ exports[`Field for array setting should render as read only with help text if ov fullWidth={true} title={

- Array test setting + + Array test setting + +

} @@ -139,7 +149,11 @@ exports[`Field for array setting should render custom setting icon if it is cust fullWidth={true} title={

- Array test setting + + Array test setting + +

} > @@ -195,7 +210,12 @@ exports[`Field for array setting should render default value if there is no user fullWidth={true} title={

- Array test setting + + Array test setting + +

} @@ -240,7 +260,11 @@ exports[`Field for array setting should render unsaved value if there are unsave fullWidth={true} title={

- Array test setting + + Array test setting + +

} > @@ -330,7 +361,12 @@ exports[`Field for array setting should render user value if there is user value fullWidth={true} title={

- Array test setting + + Array test setting + +

} @@ -392,7 +428,12 @@ exports[`Field for boolean setting should render as read only if saving is disab fullWidth={true} title={

- Boolean test setting + + Boolean test setting + +

} @@ -465,7 +506,12 @@ exports[`Field for boolean setting should render as read only with help text if fullWidth={true} title={

- Boolean test setting + + Boolean test setting + +

} @@ -526,7 +572,11 @@ exports[`Field for boolean setting should render custom setting icon if it is cu fullWidth={true} title={

- Boolean test setting + + Boolean test setting + +

} > @@ -588,7 +639,12 @@ exports[`Field for boolean setting should render default value if there is no us fullWidth={true} title={

- Boolean test setting + + Boolean test setting + +

} @@ -639,7 +695,11 @@ exports[`Field for boolean setting should render unsaved value if there are unsa fullWidth={true} title={

- Boolean test setting + + Boolean test setting + +

} > @@ -731,7 +798,12 @@ exports[`Field for boolean setting should render user value if there is user val fullWidth={true} title={

- Boolean test setting + + Boolean test setting + +

} @@ -799,7 +871,12 @@ exports[`Field for image setting should render as read only if saving is disable fullWidth={true} title={

- Image test setting + + Image test setting + +

} @@ -868,7 +945,12 @@ exports[`Field for image setting should render as read only with help text if ov fullWidth={true} title={

- Image test setting + + Image test setting + +

} @@ -921,7 +1003,11 @@ exports[`Field for image setting should render custom setting icon if it is cust fullWidth={true} title={

- Image test setting + + Image test setting + +

} > @@ -979,7 +1066,12 @@ exports[`Field for image setting should render default value if there is no user fullWidth={true} title={

- Image test setting + + Image test setting + +

} @@ -1026,7 +1118,11 @@ exports[`Field for image setting should render unsaved value if there are unsave fullWidth={true} title={

- Image test setting + + Image test setting + +

} > @@ -1113,7 +1216,12 @@ exports[`Field for image setting should render user value if there is user value fullWidth={true} title={

- Image test setting + + Image test setting + +

} @@ -1211,7 +1319,12 @@ exports[`Field for json setting should render as read only if saving is disabled fullWidth={true} title={

- Json test setting + + Json test setting + +

} @@ -1302,7 +1415,12 @@ exports[`Field for json setting should render as read only with help text if ove fullWidth={true} title={

- Json test setting + + Json test setting + +

} @@ -1378,7 +1496,11 @@ exports[`Field for json setting should render custom setting icon if it is custo fullWidth={true} title={

- Json test setting + + Json test setting + +

} > @@ -1480,7 +1603,12 @@ exports[`Field for json setting should render default value if there is no user fullWidth={true} title={

- Json test setting + + Json test setting + +

} @@ -1563,7 +1691,11 @@ exports[`Field for json setting should render unsaved value if there are unsaved fullWidth={true} title={

- Json test setting + + Json test setting + +

} > @@ -1677,7 +1816,12 @@ exports[`Field for json setting should render user value if there is user value fullWidth={true} title={

- Json test setting + + Json test setting + +

} @@ -1760,7 +1904,12 @@ exports[`Field for markdown setting should render as read only if saving is disa fullWidth={true} title={

- Markdown test setting + + Markdown test setting + +

} @@ -1848,7 +1997,12 @@ exports[`Field for markdown setting should render as read only with help text if fullWidth={true} title={

- Markdown test setting + + Markdown test setting + +

} @@ -1924,7 +2078,11 @@ exports[`Field for markdown setting should render custom setting icon if it is c fullWidth={true} title={

- Markdown test setting + + Markdown test setting + +

} > @@ -2001,7 +2160,12 @@ exports[`Field for markdown setting should render default value if there is no u fullWidth={true} title={

- Markdown test setting + + Markdown test setting + +

} @@ -2067,7 +2231,11 @@ exports[`Field for markdown setting should render unsaved value if there are uns fullWidth={true} title={

- Markdown test setting + + Markdown test setting + +

} > @@ -2174,7 +2349,12 @@ exports[`Field for markdown setting should render user value if there is user va fullWidth={true} title={

- Markdown test setting + + Markdown test setting + +

} @@ -2257,7 +2437,12 @@ exports[`Field for number setting should render as read only if saving is disabl fullWidth={true} title={

- Number test setting + + Number test setting + +

} @@ -2324,7 +2509,12 @@ exports[`Field for number setting should render as read only with help text if o fullWidth={true} title={

- Number test setting + + Number test setting + +

} @@ -2379,7 +2569,11 @@ exports[`Field for number setting should render custom setting icon if it is cus fullWidth={true} title={

- Number test setting + + Number test setting + +

} > @@ -2435,7 +2630,12 @@ exports[`Field for number setting should render default value if there is no use fullWidth={true} title={

- Number test setting + + Number test setting + +

} @@ -2480,7 +2680,11 @@ exports[`Field for number setting should render unsaved value if there are unsav fullWidth={true} title={

- Number test setting + + Number test setting + +

} > @@ -2566,7 +2777,12 @@ exports[`Field for number setting should render user value if there is user valu fullWidth={true} title={

- Number test setting + + Number test setting + +

} @@ -2628,7 +2844,12 @@ exports[`Field for select setting should render as read only if saving is disabl fullWidth={true} title={

- Select test setting + + Select test setting + +

} @@ -2711,7 +2932,12 @@ exports[`Field for select setting should render as read only with help text if o fullWidth={true} title={

- Select test setting + + Select test setting + +

} @@ -2782,7 +3008,11 @@ exports[`Field for select setting should render custom setting icon if it is cus fullWidth={true} title={

- Select test setting + + Select test setting + +

} > @@ -2854,7 +3085,12 @@ exports[`Field for select setting should render default value if there is no use fullWidth={true} title={

- Select test setting + + Select test setting + +

} @@ -2915,7 +3151,11 @@ exports[`Field for select setting should render unsaved value if there are unsav fullWidth={true} title={

- Select test setting + + Select test setting + +

} > @@ -3017,7 +3264,12 @@ exports[`Field for select setting should render user value if there is user valu fullWidth={true} title={

- Select test setting + + Select test setting + +

} @@ -3095,7 +3347,12 @@ exports[`Field for string setting should render as read only if saving is disabl fullWidth={true} title={

- String test setting + + String test setting + +

} @@ -3162,7 +3419,12 @@ exports[`Field for string setting should render as read only with help text if o fullWidth={true} title={

- String test setting + + String test setting + +

} @@ -3217,7 +3479,11 @@ exports[`Field for string setting should render custom setting icon if it is cus fullWidth={true} title={

- String test setting + + String test setting + +

} > @@ -3273,7 +3540,12 @@ exports[`Field for string setting should render default value if there is no use fullWidth={true} title={

- String test setting + + String test setting + +

} @@ -3318,7 +3590,11 @@ exports[`Field for string setting should render unsaved value if there are unsav fullWidth={true} title={

- String test setting + + String test setting + +

} > @@ -3404,7 +3687,12 @@ exports[`Field for string setting should render user value if there is user valu fullWidth={true} title={

- String test setting + + String test setting + +

} @@ -3466,7 +3754,12 @@ exports[`Field for stringWithValidation setting should render as read only if sa fullWidth={true} title={

- String test validation setting + + String test validation setting + +

} @@ -3533,7 +3826,12 @@ exports[`Field for stringWithValidation setting should render as read only with fullWidth={true} title={

- String test validation setting + + String test validation setting + +

} @@ -3588,7 +3886,11 @@ exports[`Field for stringWithValidation setting should render custom setting ico fullWidth={true} title={

- String test validation setting + + String test validation setting + +

} > @@ -3644,7 +3947,12 @@ exports[`Field for stringWithValidation setting should render default value if t fullWidth={true} title={

- String test validation setting + + String test validation setting + +

} @@ -3689,7 +3997,11 @@ exports[`Field for stringWithValidation setting should render unsaved value if t fullWidth={true} title={

- String test validation setting + + String test validation setting + +

} > @@ -3775,7 +4094,12 @@ exports[`Field for stringWithValidation setting should render user value if ther fullWidth={true} title={

- String test validation setting + + String test validation setting + +

} diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx index d9c3752d1c0a5..18a1a365709d1 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx @@ -450,9 +450,24 @@ export class Field extends PureComponent { } renderTitle(setting: FieldSetting) { + const { unsavedChanges } = this.props; + const isInvalid = unsavedChanges?.isInvalid; + + const unsavedIconLabel = unsavedChanges + ? isInvalid + ? i18n.translate('advancedSettings.field.invalidIconLabel', { + defaultMessage: 'Invalid', + }) + : i18n.translate('advancedSettings.field.unsavedIconLabel', { + defaultMessage: 'Unsaved', + }) + : undefined; + return (

- {setting.displayName || setting.name} + + {setting.displayName || setting.name} + {setting.isCustom ? ( { ) : ( '' )} + + {unsavedChanges ? ( + + ) : ( + '' + )}

); } diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx index ef433dd990d33..c859e8fdd7136 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx @@ -331,54 +331,56 @@ export class Form extends PureComponent { }); return ( - +

{this.renderCountOfUnsaved()}

+ - - - - {i18n.translate('advancedSettings.form.cancelButtonLabel', { - defaultMessage: 'Cancel changes', - })} - - - - - - {i18n.translate('advancedSettings.form.saveButtonLabel', { - defaultMessage: 'Save changes', - })} - - - - + + {i18n.translate('advancedSettings.form.cancelButtonLabel', { + defaultMessage: 'Cancel changes', + })} + + + + + + {i18n.translate('advancedSettings.form.saveButtonLabel', { + defaultMessage: 'Save changes', + })} + +
From c90cfe208d790c3816e9d4d8cdc21307882e76ed Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Mon, 2 Mar 2020 15:04:27 -0500 Subject: [PATCH 19/26] [CI] Pipeline refactoring (#56447) --- .ci/Jenkinsfile_coverage | 170 ++++++++++----------- .ci/Jenkinsfile_flaky | 81 +++++----- .ci/es-snapshots/Jenkinsfile_build_es | 2 +- .ci/es-snapshots/Jenkinsfile_verify_es | 79 +++++----- Jenkinsfile | 108 ++++++------- packages/kbn-test/src/junit_report_path.ts | 4 +- test/scripts/jenkins_test_setup_oss.sh | 2 +- test/scripts/jenkins_test_setup_xpack.sh | 2 +- vars/agentInfo.groovy | 4 +- vars/catchErrors.groovy | 8 + vars/githubPr.groovy | 4 +- vars/kibanaPipeline.groovy | 164 +++++--------------- vars/retryWithDelay.groovy | 4 +- vars/retryable.groovy | 2 +- vars/workers.groovy | 147 ++++++++++++++++++ 15 files changed, 406 insertions(+), 375 deletions(-) create mode 100644 vars/catchErrors.groovy create mode 100644 vars/workers.groovy diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index fa1e141be93ea..6b8dc31bab34e 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -3,99 +3,91 @@ library 'kibana-pipeline-library' kibanaLibrary.load() // load from the Jenkins instance -stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a little bit - timeout(time: 180, unit: 'MINUTES') { - timestamps { - ansiColor('xterm') { - catchError { +kibanaPipeline(timeoutMinutes: 180) { + catchErrors { + withEnv([ + 'CODE_COVERAGE=1', // Needed for multiple ci scripts, such as remote.ts, test/scripts/*.sh, schema.js, etc. + ]) { + parallel([ + 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), + 'x-pack-intake-agent': { withEnv([ - 'CODE_COVERAGE=1', // Needed for multiple ci scripts, such as remote.ts, test/scripts/*.sh, schema.js, etc. + 'NODE_ENV=test' // Needed for jest tests only ]) { - parallel([ - 'kibana-intake-agent': { - kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh')() - }, - 'x-pack-intake-agent': { - withEnv([ - 'NODE_ENV=test' // Needed for jest tests only - ]) { - kibanaPipeline.intakeWorker('x-pack-intake', './test/scripts/jenkins_xpack.sh')() - } - }, - 'kibana-oss-agent': kibanaPipeline.withWorkers('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - 'oss-ciGroup1': kibanaPipeline.getOssCiGroupWorker(1), - 'oss-ciGroup2': kibanaPipeline.getOssCiGroupWorker(2), - 'oss-ciGroup3': kibanaPipeline.getOssCiGroupWorker(3), - 'oss-ciGroup4': kibanaPipeline.getOssCiGroupWorker(4), - 'oss-ciGroup5': kibanaPipeline.getOssCiGroupWorker(5), - 'oss-ciGroup6': kibanaPipeline.getOssCiGroupWorker(6), - 'oss-ciGroup7': kibanaPipeline.getOssCiGroupWorker(7), - 'oss-ciGroup8': kibanaPipeline.getOssCiGroupWorker(8), - 'oss-ciGroup9': kibanaPipeline.getOssCiGroupWorker(9), - 'oss-ciGroup10': kibanaPipeline.getOssCiGroupWorker(10), - 'oss-ciGroup11': kibanaPipeline.getOssCiGroupWorker(11), - 'oss-ciGroup12': kibanaPipeline.getOssCiGroupWorker(12), - ]), - 'kibana-xpack-agent': kibanaPipeline.withWorkers('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - 'xpack-ciGroup1': kibanaPipeline.getXpackCiGroupWorker(1), - 'xpack-ciGroup2': kibanaPipeline.getXpackCiGroupWorker(2), - 'xpack-ciGroup3': kibanaPipeline.getXpackCiGroupWorker(3), - 'xpack-ciGroup4': kibanaPipeline.getXpackCiGroupWorker(4), - 'xpack-ciGroup5': kibanaPipeline.getXpackCiGroupWorker(5), - 'xpack-ciGroup6': kibanaPipeline.getXpackCiGroupWorker(6), - 'xpack-ciGroup7': kibanaPipeline.getXpackCiGroupWorker(7), - 'xpack-ciGroup8': kibanaPipeline.getXpackCiGroupWorker(8), - 'xpack-ciGroup9': kibanaPipeline.getXpackCiGroupWorker(9), - 'xpack-ciGroup10': kibanaPipeline.getXpackCiGroupWorker(10), - ]), - ]) - kibanaPipeline.jobRunner('tests-l', false) { - kibanaPipeline.downloadCoverageArtifacts() - kibanaPipeline.bash( - ''' - # bootstrap from x-pack folder - source src/dev/ci_setup/setup_env.sh - cd x-pack - yarn kbn bootstrap --prefer-offline - cd .. - # extract archives - mkdir -p /tmp/extracted_coverage - echo extracting intakes - tar -xzf /tmp/downloaded_coverage/coverage/kibana-intake/kibana-coverage.tar.gz -C /tmp/extracted_coverage - tar -xzf /tmp/downloaded_coverage/coverage/x-pack-intake/kibana-coverage.tar.gz -C /tmp/extracted_coverage - echo extracting kibana-oss-tests - tar -xzf /tmp/downloaded_coverage/coverage/kibana-oss-tests/kibana-coverage.tar.gz -C /tmp/extracted_coverage - echo extracting kibana-xpack-tests - tar -xzf /tmp/downloaded_coverage/coverage/kibana-xpack-tests/kibana-coverage.tar.gz -C /tmp/extracted_coverage - # replace path in json files to have valid html report - pwd=$(pwd) - du -sh /tmp/extracted_coverage/target/kibana-coverage/ - echo replacing path in json files - for i in {1..9}; do - sed -i "s|/dev/shm/workspace/kibana|$pwd|g" /tmp/extracted_coverage/target/kibana-coverage/functional/${i}*.json & - done - wait - # merge oss & x-pack reports - echo merging coverage reports - yarn nyc report --temp-dir /tmp/extracted_coverage/target/kibana-coverage/jest --report-dir target/kibana-coverage/jest-combined --reporter=html --reporter=json-summary - yarn nyc report --temp-dir /tmp/extracted_coverage/target/kibana-coverage/functional --report-dir target/kibana-coverage/functional-combined --reporter=html --reporter=json-summary - echo copy mocha reports - mkdir -p target/kibana-coverage/mocha-combined - cp -r /tmp/extracted_coverage/target/kibana-coverage/mocha target/kibana-coverage/mocha-combined - ''', - "run `yarn kbn bootstrap && merge coverage`" - ) - sh 'tar -czf kibana-jest-coverage.tar.gz target/kibana-coverage/jest-combined/*' - kibanaPipeline.uploadCoverageArtifacts("coverage/jest-combined", 'kibana-jest-coverage.tar.gz') - sh 'tar -czf kibana-functional-coverage.tar.gz target/kibana-coverage/functional-combined/*' - kibanaPipeline.uploadCoverageArtifacts("coverage/functional-combined", 'kibana-functional-coverage.tar.gz') - sh 'tar -czf kibana-mocha-coverage.tar.gz target/kibana-coverage/mocha-combined/*' - kibanaPipeline.uploadCoverageArtifacts("coverage/mocha-combined", 'kibana-mocha-coverage.tar.gz') - } + workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh')() } - } - kibanaPipeline.sendMail() + }, + 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), + 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), + 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), + 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), + 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), + 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), + 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), + 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), + 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), + 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), + 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), + 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), + ]), + 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ + 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), + 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), + 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), + 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), + 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), + 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), + 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), + 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), + 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), + 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), + ]), + ]) + workers.base(name: 'coverage-worker', label: 'tests-l', ramDisk: false, bootstrapped: false) { + kibanaPipeline.downloadCoverageArtifacts() + kibanaPipeline.bash( + ''' + # bootstrap from x-pack folder + source src/dev/ci_setup/setup_env.sh + cd x-pack + yarn kbn bootstrap --prefer-offline + cd .. + # extract archives + mkdir -p /tmp/extracted_coverage + echo extracting intakes + tar -xzf /tmp/downloaded_coverage/coverage/kibana-intake/kibana-coverage.tar.gz -C /tmp/extracted_coverage + tar -xzf /tmp/downloaded_coverage/coverage/x-pack-intake/kibana-coverage.tar.gz -C /tmp/extracted_coverage + echo extracting kibana-oss-tests + tar -xzf /tmp/downloaded_coverage/coverage/kibana-oss-tests/kibana-coverage.tar.gz -C /tmp/extracted_coverage + echo extracting kibana-xpack-tests + tar -xzf /tmp/downloaded_coverage/coverage/kibana-xpack-tests/kibana-coverage.tar.gz -C /tmp/extracted_coverage + # replace path in json files to have valid html report + pwd=$(pwd) + du -sh /tmp/extracted_coverage/target/kibana-coverage/ + echo replacing path in json files + for i in {1..9}; do + sed -i "s|/dev/shm/workspace/kibana|$pwd|g" /tmp/extracted_coverage/target/kibana-coverage/functional/${i}*.json & + done + wait + # merge oss & x-pack reports + echo merging coverage reports + yarn nyc report --temp-dir /tmp/extracted_coverage/target/kibana-coverage/jest --report-dir target/kibana-coverage/jest-combined --reporter=html --reporter=json-summary + yarn nyc report --temp-dir /tmp/extracted_coverage/target/kibana-coverage/functional --report-dir target/kibana-coverage/functional-combined --reporter=html --reporter=json-summary + echo copy mocha reports + mkdir -p target/kibana-coverage/mocha-combined + cp -r /tmp/extracted_coverage/target/kibana-coverage/mocha target/kibana-coverage/mocha-combined + ''', + "run `yarn kbn bootstrap && merge coverage`" + ) + sh 'tar -czf kibana-jest-coverage.tar.gz target/kibana-coverage/jest-combined/*' + kibanaPipeline.uploadCoverageArtifacts("coverage/jest-combined", 'kibana-jest-coverage.tar.gz') + sh 'tar -czf kibana-functional-coverage.tar.gz target/kibana-coverage/functional-combined/*' + kibanaPipeline.uploadCoverageArtifacts("coverage/functional-combined", 'kibana-functional-coverage.tar.gz') + sh 'tar -czf kibana-mocha-coverage.tar.gz target/kibana-coverage/mocha-combined/*' + kibanaPipeline.uploadCoverageArtifacts("coverage/mocha-combined", 'kibana-mocha-coverage.tar.gz') } } } + kibanaPipeline.sendMail() } diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index f702405aad69e..befb8d259b5b6 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -21,53 +21,47 @@ def workerFailures = [] currentBuild.displayName += trunc(" ${params.GITHUB_OWNER}:${params.branch_specifier}", 24) currentBuild.description = "${params.CI_GROUP}
Agents: ${AGENT_COUNT}
Executions: ${params.NUMBER_EXECUTIONS}" -stage("Kibana Pipeline") { - timeout(time: 180, unit: 'MINUTES') { - timestamps { - ansiColor('xterm') { - def agents = [:] - for(def agentNumber = 1; agentNumber <= AGENT_COUNT; agentNumber++) { - def agentNumberInside = agentNumber - def agentExecutions = floor(EXECUTIONS/AGENT_COUNT) + (agentNumber <= EXECUTIONS%AGENT_COUNT ? 1 : 0) - agents["agent-${agentNumber}"] = { - catchError { - print "Agent ${agentNumberInside} - ${agentExecutions} executions" - - kibanaPipeline.withWorkers('flaky-test-runner', { - if (NEED_BUILD) { - if (!IS_XPACK) { - kibanaPipeline.buildOss() - if (CI_GROUP == '1') { - runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") - } - } else { - kibanaPipeline.buildXpack() - } - } - }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() +kibanaPipeline(timeoutMinutes: 180) { + def agents = [:] + for(def agentNumber = 1; agentNumber <= AGENT_COUNT; agentNumber++) { + def agentNumberInside = agentNumber + def agentExecutions = floor(EXECUTIONS/AGENT_COUNT) + (agentNumber <= EXECUTIONS%AGENT_COUNT ? 1 : 0) + agents["agent-${agentNumber}"] = { + catchErrors { + print "Agent ${agentNumberInside} - ${agentExecutions} executions" + + workers.functional('flaky-test-runner', { + if (NEED_BUILD) { + if (!IS_XPACK) { + kibanaPipeline.buildOss() + if (CI_GROUP == '1') { + runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") + } + } else { + kibanaPipeline.buildXpack() } } - } + }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() + } + } + } - parallel(agents) + parallel(agents) - currentBuild.description += ", Failures: ${workerFailures.size()}" + currentBuild.description += ", Failures: ${workerFailures.size()}" - if (workerFailures.size() > 0) { - print "There were ${workerFailures.size()} test suite failures." - print "The executions that failed were:" - print workerFailures.join("\n") - print "Please check 'Test Result' and 'Pipeline Steps' pages for more info" - } - } - } + if (workerFailures.size() > 0) { + print "There were ${workerFailures.size()} test suite failures." + print "The executions that failed were:" + print workerFailures.join("\n") + print "Please check 'Test Result' and 'Pipeline Steps' pages for more info" } } def getWorkerFromParams(isXpack, job, ciGroup) { if (!isXpack) { if (job == 'serverMocha') { - return kibanaPipeline.getPostBuildWorker('serverMocha', { + return kibanaPipeline.functionalTestProcess('serverMocha', { kibanaPipeline.bash( """ source src/dev/ci_setup/setup_env.sh @@ -77,20 +71,20 @@ def getWorkerFromParams(isXpack, job, ciGroup) { ) }) } else if (job == 'firefoxSmoke') { - return kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') }) + return kibanaPipeline.functionalTestProcess('firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh') } else if(job == 'visualRegression') { - return kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }) + return kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh') } else { - return kibanaPipeline.getOssCiGroupWorker(ciGroup) + return kibanaPipeline.ossCiGroupProcess(ciGroup) } } if (job == 'firefoxSmoke') { - return kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld('./test/scripts/jenkins_xpack_firefox_smoke.sh', 'Execute xpack-firefoxSmoke') }) + return kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh') } else if(job == 'visualRegression') { - return kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld('./test/scripts/jenkins_xpack_visual_regression.sh', 'Execute xpack-visualRegression') }) + return kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh') } else { - return kibanaPipeline.getXpackCiGroupWorker(ciGroup) + return kibanaPipeline.xpackCiGroupProcess(ciGroup) } } @@ -105,10 +99,9 @@ def getWorkerMap(agentNumber, numberOfExecutions, worker, workerFailures, maxWor for(def j = 0; j < workerExecutions; j++) { print "Execute agent-${agentNumber} worker-${workerNumber}: ${j}" withEnv([ - "JOB=agent-${agentNumber}-worker-${workerNumber}-${j}", "REMOVE_KIBANA_INSTALL_DIR=1", ]) { - catchError { + catchErrors { try { worker(workerNumber) } catch (ex) { diff --git a/.ci/es-snapshots/Jenkinsfile_build_es b/.ci/es-snapshots/Jenkinsfile_build_es index ad0ad54275e12..a00bcb3bbc946 100644 --- a/.ci/es-snapshots/Jenkinsfile_build_es +++ b/.ci/es-snapshots/Jenkinsfile_build_es @@ -26,7 +26,7 @@ timeout(time: 120, unit: 'MINUTES') { timestamps { ansiColor('xterm') { node('linux && immutable') { - catchError { + catchErrors { def VERSION def SNAPSHOT_ID def DESTINATION diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es index 30d52a56547bd..ce472a404c053 100644 --- a/.ci/es-snapshots/Jenkinsfile_verify_es +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -19,50 +19,45 @@ currentBuild.description = "ES: ${SNAPSHOT_VERSION}
Kibana: ${params.branch def SNAPSHOT_MANIFEST = "https://storage.googleapis.com/kibana-ci-es-snapshots-daily/${SNAPSHOT_VERSION}/archives/${SNAPSHOT_ID}/manifest.json" -timeout(time: 120, unit: 'MINUTES') { - timestamps { - ansiColor('xterm') { - catchError { - withEnv(["ES_SNAPSHOT_MANIFEST=${SNAPSHOT_MANIFEST}"]) { - parallel([ - // TODO we just need to run integration tests from intake? - 'kibana-intake-agent': kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': kibanaPipeline.intakeWorker('x-pack-intake', './test/scripts/jenkins_xpack.sh'), - 'kibana-oss-agent': kibanaPipeline.withWorkers('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - 'oss-ciGroup1': kibanaPipeline.getOssCiGroupWorker(1), - 'oss-ciGroup2': kibanaPipeline.getOssCiGroupWorker(2), - 'oss-ciGroup3': kibanaPipeline.getOssCiGroupWorker(3), - 'oss-ciGroup4': kibanaPipeline.getOssCiGroupWorker(4), - 'oss-ciGroup5': kibanaPipeline.getOssCiGroupWorker(5), - 'oss-ciGroup6': kibanaPipeline.getOssCiGroupWorker(6), - 'oss-ciGroup7': kibanaPipeline.getOssCiGroupWorker(7), - 'oss-ciGroup8': kibanaPipeline.getOssCiGroupWorker(8), - 'oss-ciGroup9': kibanaPipeline.getOssCiGroupWorker(9), - 'oss-ciGroup10': kibanaPipeline.getOssCiGroupWorker(10), - 'oss-ciGroup11': kibanaPipeline.getOssCiGroupWorker(11), - 'oss-ciGroup12': kibanaPipeline.getOssCiGroupWorker(12), - ]), - 'kibana-xpack-agent': kibanaPipeline.withWorkers('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - 'xpack-ciGroup1': kibanaPipeline.getXpackCiGroupWorker(1), - 'xpack-ciGroup2': kibanaPipeline.getXpackCiGroupWorker(2), - 'xpack-ciGroup3': kibanaPipeline.getXpackCiGroupWorker(3), - 'xpack-ciGroup4': kibanaPipeline.getXpackCiGroupWorker(4), - 'xpack-ciGroup5': kibanaPipeline.getXpackCiGroupWorker(5), - 'xpack-ciGroup6': kibanaPipeline.getXpackCiGroupWorker(6), - 'xpack-ciGroup7': kibanaPipeline.getXpackCiGroupWorker(7), - 'xpack-ciGroup8': kibanaPipeline.getXpackCiGroupWorker(8), - 'xpack-ciGroup9': kibanaPipeline.getXpackCiGroupWorker(9), - 'xpack-ciGroup10': kibanaPipeline.getXpackCiGroupWorker(10), - ]), - ]) - } - - promoteSnapshot(SNAPSHOT_VERSION, SNAPSHOT_ID) - } - - kibanaPipeline.sendMail() +kibanaPipeline(timeoutMinutes: 120) { + catchErrors { + withEnv(["ES_SNAPSHOT_MANIFEST=${SNAPSHOT_MANIFEST}"]) { + parallel([ + 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), + 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), + 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), + 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), + 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), + 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), + 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), + 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), + 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), + 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), + 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), + 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), + 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), + 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), + ]), + 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ + 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), + 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), + 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), + 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), + 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), + 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), + 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), + 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), + 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), + 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), + ]), + ]) } + + promoteSnapshot(SNAPSHOT_VERSION, SNAPSHOT_ID) } + + kibanaPipeline.sendMail() } def promoteSnapshot(snapshotVersion, snapshotId) { diff --git a/Jenkinsfile b/Jenkinsfile index 1b4350d5b91e9..85502369b07be 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,71 +3,49 @@ library 'kibana-pipeline-library' kibanaLibrary.load() -stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a little bit - timeout(time: 135, unit: 'MINUTES') { - timestamps { - ansiColor('xterm') { - githubPr.withDefaultPrComments { - catchError { - retryable.enable() - parallel([ - 'kibana-intake-agent': kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': kibanaPipeline.intakeWorker('x-pack-intake', './test/scripts/jenkins_xpack.sh'), - 'kibana-oss-agent': kibanaPipeline.withWorkers('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - // 'oss-firefoxSmoke': kibanaPipeline.getPostBuildWorker('firefoxSmoke', { - // retryable('kibana-firefoxSmoke') { - // runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') - // } - // }), - 'oss-ciGroup1': kibanaPipeline.getOssCiGroupWorker(1), - 'oss-ciGroup2': kibanaPipeline.getOssCiGroupWorker(2), - 'oss-ciGroup3': kibanaPipeline.getOssCiGroupWorker(3), - 'oss-ciGroup4': kibanaPipeline.getOssCiGroupWorker(4), - 'oss-ciGroup5': kibanaPipeline.getOssCiGroupWorker(5), - 'oss-ciGroup6': kibanaPipeline.getOssCiGroupWorker(6), - 'oss-ciGroup7': kibanaPipeline.getOssCiGroupWorker(7), - 'oss-ciGroup8': kibanaPipeline.getOssCiGroupWorker(8), - 'oss-ciGroup9': kibanaPipeline.getOssCiGroupWorker(9), - 'oss-ciGroup10': kibanaPipeline.getOssCiGroupWorker(10), - 'oss-ciGroup11': kibanaPipeline.getOssCiGroupWorker(11), - 'oss-ciGroup12': kibanaPipeline.getOssCiGroupWorker(12), - 'oss-accessibility': kibanaPipeline.getPostBuildWorker('accessibility', { - retryable('kibana-accessibility') { - runbld('./test/scripts/jenkins_accessibility.sh', 'Execute kibana-accessibility') - } - }), - // 'oss-visualRegression': kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }), - ]), - 'kibana-xpack-agent': kibanaPipeline.withWorkers('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - // 'xpack-firefoxSmoke': kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { - // retryable('xpack-firefoxSmoke') { - // runbld('./test/scripts/jenkins_xpack_firefox_smoke.sh', 'Execute xpack-firefoxSmoke') - // } - // }), - 'xpack-ciGroup1': kibanaPipeline.getXpackCiGroupWorker(1), - 'xpack-ciGroup2': kibanaPipeline.getXpackCiGroupWorker(2), - 'xpack-ciGroup3': kibanaPipeline.getXpackCiGroupWorker(3), - 'xpack-ciGroup4': kibanaPipeline.getXpackCiGroupWorker(4), - 'xpack-ciGroup5': kibanaPipeline.getXpackCiGroupWorker(5), - 'xpack-ciGroup6': kibanaPipeline.getXpackCiGroupWorker(6), - 'xpack-ciGroup7': kibanaPipeline.getXpackCiGroupWorker(7), - 'xpack-ciGroup8': kibanaPipeline.getXpackCiGroupWorker(8), - 'xpack-ciGroup9': kibanaPipeline.getXpackCiGroupWorker(9), - 'xpack-ciGroup10': kibanaPipeline.getXpackCiGroupWorker(10), - 'xpack-accessibility': kibanaPipeline.getPostBuildWorker('xpack-accessibility', { - retryable('xpack-accessibility') { - runbld('./test/scripts/jenkins_xpack_accessibility.sh', 'Execute xpack-accessibility') - } - }), - // 'xpack-visualRegression': kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld('./test/scripts/jenkins_xpack_visual_regression.sh', 'Execute xpack-visualRegression') }), - ]), - ]) - } - } - - retryable.printFlakyFailures() - kibanaPipeline.sendMail() - } +kibanaPipeline(timeoutMinutes: 135) { + githubPr.withDefaultPrComments { + catchError { + retryable.enable() + parallel([ + 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), + 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), + 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + // 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), + 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), + 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), + 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), + 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), + 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), + 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), + 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), + 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), + 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), + 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), + 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), + 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), + 'oss-accessibility': kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh'), + // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'), + ]), + 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ + // 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), + 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), + 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), + 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), + 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), + 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), + 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), + 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), + 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), + 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), + 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), + 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), + // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), + ]), + ]) } } + + retryable.printFlakyFailures() + kibanaPipeline.sendMail() } diff --git a/packages/kbn-test/src/junit_report_path.ts b/packages/kbn-test/src/junit_report_path.ts index 11eaf3d2b14a5..d46c9455dcff0 100644 --- a/packages/kbn-test/src/junit_report_path.ts +++ b/packages/kbn-test/src/junit_report_path.ts @@ -20,7 +20,9 @@ import { resolve } from 'path'; const job = process.env.JOB ? `job-${process.env.JOB}-` : ''; -const num = process.env.CI_WORKER_NUMBER ? `worker-${process.env.CI_WORKER_NUMBER}-` : ''; +const num = process.env.CI_PARALLEL_PROCESS_NUMBER + ? `worker-${process.env.CI_PARALLEL_PROCESS_NUMBER}-` + : ''; export function makeJunitReportPath(rootDirectory: string, reportName: string) { return resolve( diff --git a/test/scripts/jenkins_test_setup_oss.sh b/test/scripts/jenkins_test_setup_oss.sh index 9e68272053221..7bbb867526384 100644 --- a/test/scripts/jenkins_test_setup_oss.sh +++ b/test/scripts/jenkins_test_setup_oss.sh @@ -4,7 +4,7 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]] ; then installDir="$(realpath $PARENT_DIR/kibana/build/oss/kibana-*-SNAPSHOT-linux-x86_64)" - destDir=${installDir}-${CI_WORKER_NUMBER} + destDir=${installDir}-${CI_PARALLEL_PROCESS_NUMBER} cp -R "$installDir" "$destDir" export KIBANA_INSTALL_DIR="$destDir" diff --git a/test/scripts/jenkins_test_setup_xpack.sh b/test/scripts/jenkins_test_setup_xpack.sh index 76fc7cfe6c876..a72e9749ebbd5 100644 --- a/test/scripts/jenkins_test_setup_xpack.sh +++ b/test/scripts/jenkins_test_setup_xpack.sh @@ -4,7 +4,7 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]]; then installDir="$PARENT_DIR/install/kibana" - destDir="${installDir}-${CI_WORKER_NUMBER}" + destDir="${installDir}-${CI_PARALLEL_PROCESS_NUMBER}" cp -R "$installDir" "$destDir" export KIBANA_INSTALL_DIR="$destDir" diff --git a/vars/agentInfo.groovy b/vars/agentInfo.groovy index b53ed23f81ff0..166a86c169261 100644 --- a/vars/agentInfo.groovy +++ b/vars/agentInfo.groovy @@ -1,5 +1,5 @@ def print() { - try { + catchError(catchInterruptions: false, buildResult: null) { def startTime = sh(script: "date -d '-3 minutes' -Iseconds | sed s/+/%2B/", returnStdout: true).trim() def endTime = sh(script: "date -d '+1 hour 30 minutes' -Iseconds | sed s/+/%2B/", returnStdout: true).trim() @@ -34,8 +34,6 @@ def print() { echo 'SSH Command:' echo "ssh -F ssh_config \$(hostname --ip-address)" """, label: "Worker/Agent/Node debug links" - } catch(ex) { - print ex.toString() } } diff --git a/vars/catchErrors.groovy b/vars/catchErrors.groovy new file mode 100644 index 0000000000000..460a90b8ec0c0 --- /dev/null +++ b/vars/catchErrors.groovy @@ -0,0 +1,8 @@ +// Basically, this is a shortcut for catchError(catchInterruptions: false) {} +// By default, catchError will swallow aborts/timeouts, which we almost never want +def call(Map params = [:], Closure closure) { + params.catchInterruptions = false + return catchError(params, closure) +} + +return this diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy index 91a4a76894d94..7759edbbf5bfc 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -14,8 +14,8 @@ So, there is only ever one build status comment on a PR at any given time, the most recent one. */ def withDefaultPrComments(closure) { - catchError { - catchError { + catchErrors { + catchErrors { closure() } diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index dd2e626d1c860..2b9b0eba38f46 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -1,92 +1,36 @@ -def withWorkers(machineName, preWorkerClosure = {}, workerClosures = [:]) { - return { - jobRunner('tests-xl', true) { - withGcsArtifactUpload(machineName, { - withPostBuildReporting { - doSetup() - preWorkerClosure() - - def nextWorker = 1 - def worker = { workerClosure -> - def workerNumber = nextWorker - nextWorker++ - - return { - // This delay helps smooth out CPU load caused by ES/Kibana instances starting up at the same time - def delay = (workerNumber-1)*20 - sleep(delay) - - workerClosure(workerNumber) - } - } - - def workers = [:] - workerClosures.each { workerName, workerClosure -> - workers[workerName] = worker(workerClosure) - } - - parallel(workers) - } - }) - } - } -} - -def withWorker(machineName, label, Closure closure) { - return { - jobRunner(label, false) { - withGcsArtifactUpload(machineName) { - withPostBuildReporting { - doSetup() - closure() - } - } - } - } -} - -def intakeWorker(jobName, String script) { - return withWorker(jobName, 'linux && immutable') { - withEnv([ - "JOB=${jobName}", - ]) { - runbld(script, "Execute ${jobName}") - } - } -} - def withPostBuildReporting(Closure closure) { try { closure() } finally { - catchError { + catchErrors { runErrorReporter() } - catchError { + catchErrors { runbld.junit() } - catchError { + catchErrors { publishJunit() } } } -def getPostBuildWorker(name, closure) { - return { workerNumber -> - def kibanaPort = "61${workerNumber}1" - def esPort = "61${workerNumber}2" - def esTransportPort = "61${workerNumber}3" +def functionalTestProcess(String name, Closure closure) { + return { processNumber -> + def kibanaPort = "61${processNumber}1" + def esPort = "61${processNumber}2" + def esTransportPort = "61${processNumber}3" withEnv([ - "CI_WORKER_NUMBER=${workerNumber}", + "CI_PARALLEL_PROCESS_NUMBER=${processNumber}", "TEST_KIBANA_HOST=localhost", "TEST_KIBANA_PORT=${kibanaPort}", "TEST_KIBANA_URL=http://elastic:changeme@localhost:${kibanaPort}", "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}", "TEST_ES_TRANSPORT_PORT=${esTransportPort}", "IS_PIPELINE_JOB=1", + "JOB=${name}", "KBN_NP_PLUGINS_BUILT=true", ]) { closure() @@ -94,8 +38,16 @@ def getPostBuildWorker(name, closure) { } } -def getOssCiGroupWorker(ciGroup) { - return getPostBuildWorker("ciGroup" + ciGroup, { +def functionalTestProcess(String name, String script) { + return functionalTestProcess(name) { + retryable(name) { + runbld(script, "Execute ${name}") + } + } +} + +def ossCiGroupProcess(ciGroup) { + return functionalTestProcess("ciGroup" + ciGroup) { withEnv([ "CI_GROUP=${ciGroup}", "JOB=kibana-ciGroup${ciGroup}", @@ -104,11 +56,11 @@ def getOssCiGroupWorker(ciGroup) { runbld("./test/scripts/jenkins_ci_group.sh", "Execute kibana-ciGroup${ciGroup}") } } - }) + } } -def getXpackCiGroupWorker(ciGroup) { - return getPostBuildWorker("xpack-ciGroup" + ciGroup, { +def xpackCiGroupProcess(ciGroup) { + return functionalTestProcess("xpack-ciGroup" + ciGroup) { withEnv([ "CI_GROUP=${ciGroup}", "JOB=xpack-kibana-ciGroup${ciGroup}", @@ -117,56 +69,6 @@ def getXpackCiGroupWorker(ciGroup) { runbld("./test/scripts/jenkins_xpack_ci_group.sh", "Execute xpack-kibana-ciGroup${ciGroup}") } } - }) -} - -def jobRunner(label, useRamDisk, closure) { - node(label) { - agentInfo.print() - - if (useRamDisk) { - // Move to a temporary workspace, so that we can symlink the real workspace into /dev/shm - def originalWorkspace = env.WORKSPACE - ws('/tmp/workspace') { - sh( - script: """ - mkdir -p /dev/shm/workspace - mkdir -p '${originalWorkspace}' # create all of the directories leading up to the workspace, if they don't exist - rm --preserve-root -rf '${originalWorkspace}' # then remove just the workspace, just in case there's stuff in it - ln -s /dev/shm/workspace '${originalWorkspace}' - """, - label: "Move workspace to RAM - /dev/shm/workspace" - ) - } - } - - def scmVars - - // Try to clone from Github up to 8 times, waiting 15 secs between attempts - retryWithDelay(8, 15) { - scmVars = checkout scm - } - - withEnv([ - "CI=true", - "HOME=${env.JENKINS_HOME}", - "PR_SOURCE_BRANCH=${env.ghprbSourceBranch ?: ''}", - "PR_TARGET_BRANCH=${env.ghprbTargetBranch ?: ''}", - "PR_AUTHOR=${env.ghprbPullAuthorLogin ?: ''}", - "TEST_BROWSER_HEADLESS=1", - "GIT_BRANCH=${scmVars.GIT_BRANCH}", - ]) { - withCredentials([ - string(credentialsId: 'vault-addr', variable: 'VAULT_ADDR'), - string(credentialsId: 'vault-role-id', variable: 'VAULT_ROLE_ID'), - string(credentialsId: 'vault-secret-id', variable: 'VAULT_SECRET_ID'), - ]) { - // scm is configured to check out to the ./kibana directory - dir('kibana') { - closure() - } - } - } } } @@ -210,7 +112,7 @@ def withGcsArtifactUpload(workerName, closure) { try { closure() } finally { - catchError { + catchErrors { ARTIFACT_PATTERNS.each { pattern -> uploadGcsArtifact(uploadPrefix, pattern) } @@ -243,7 +145,7 @@ def sendMail() { } def sendInfraMail() { - catchError { + catchErrors { step([ $class: 'Mailer', notifyEveryUnstableBuild: true, @@ -254,7 +156,7 @@ def sendInfraMail() { } def sendKibanaMail() { - catchError { + catchErrors { def buildStatus = buildUtils.getBuildStatus() if(params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED') { emailext( @@ -299,4 +201,18 @@ def runErrorReporter() { ) } +def call(Map params = [:], Closure closure) { + def config = [timeoutMinutes: 135] + params + + stage("Kibana Pipeline") { + timeout(time: config.timeoutMinutes, unit: 'MINUTES') { + timestamps { + ansiColor('xterm') { + closure() + } + } + } + } +} + return this diff --git a/vars/retryWithDelay.groovy b/vars/retryWithDelay.groovy index 70d6f86a63ab2..83fd94c6f2b1e 100644 --- a/vars/retryWithDelay.groovy +++ b/vars/retryWithDelay.groovy @@ -2,7 +2,9 @@ def call(retryTimes, delaySecs, closure) { retry(retryTimes) { try { closure() - } catch (ex) { + } catch (org.jenkinsci.plugins.workflow.steps.FlowInterruptedException ex) { + throw ex // Immediately re-throw build abort exceptions, don't sleep first + } catch (Exception ex) { sleep delaySecs throw ex } diff --git a/vars/retryable.groovy b/vars/retryable.groovy index cc34024958aed..ed84a00ece49d 100644 --- a/vars/retryable.groovy +++ b/vars/retryable.groovy @@ -27,7 +27,7 @@ def getFlakyFailures() { } def printFlakyFailures() { - catchError { + catchErrors { def failures = getFlakyFailures() if (failures && failures.size() > 0) { diff --git a/vars/workers.groovy b/vars/workers.groovy new file mode 100644 index 0000000000000..c5638f2624fe5 --- /dev/null +++ b/vars/workers.groovy @@ -0,0 +1,147 @@ +// "Workers" in this file will spin up an instance, do some setup etc depending on the configuration, and then execute some work that you define +// e.g. workers.base(name: 'my-worker') { sh "echo 'ready to execute some kibana scripts'" } + +/* + The base worker that all of the others use. Will clone the scm (assumed to be kibana), and run kibana bootstrap processes by default. + + Parameters: + label - gobld/agent label to use, e.g. 'linux && immutable' + ramDisk - Should the workspace be mounted in memory? Default: true + bootstrapped - If true, download kibana dependencies, run kbn bootstrap, etc. Default: true + name - Name of the worker for display purposes, filenames, etc. + scm - Jenkins scm configuration for checking out code. Use `null` to disable checkout. Default: inherited from job +*/ +def base(Map params, Closure closure) { + def config = [label: '', ramDisk: true, bootstrapped: true, name: 'unnamed-worker', scm: scm] + params + if (!config.label) { + error "You must specify an agent label, such as 'tests-xl' or 'linux && immutable', when using workers.base()" + } + + node(config.label) { + agentInfo.print() + + if (config.ramDisk) { + // Move to a temporary workspace, so that we can symlink the real workspace into /dev/shm + def originalWorkspace = env.WORKSPACE + ws('/tmp/workspace') { + sh( + script: """ + mkdir -p /dev/shm/workspace + mkdir -p '${originalWorkspace}' # create all of the directories leading up to the workspace, if they don't exist + rm --preserve-root -rf '${originalWorkspace}' # then remove just the workspace, just in case there's stuff in it + ln -s /dev/shm/workspace '${originalWorkspace}' + """, + label: "Move workspace to RAM - /dev/shm/workspace" + ) + } + } + + def scmVars = [:] + + if (config.scm) { + // Try to clone from Github up to 8 times, waiting 15 secs between attempts + retryWithDelay(8, 15) { + scmVars = checkout scm + } + } + + withEnv([ + "CI=true", + "HOME=${env.JENKINS_HOME}", + "PR_SOURCE_BRANCH=${env.ghprbSourceBranch ?: ''}", + "PR_TARGET_BRANCH=${env.ghprbTargetBranch ?: ''}", + "PR_AUTHOR=${env.ghprbPullAuthorLogin ?: ''}", + "TEST_BROWSER_HEADLESS=1", + "GIT_BRANCH=${scmVars.GIT_BRANCH ?: ''}", + ]) { + withCredentials([ + string(credentialsId: 'vault-addr', variable: 'VAULT_ADDR'), + string(credentialsId: 'vault-role-id', variable: 'VAULT_ROLE_ID'), + string(credentialsId: 'vault-secret-id', variable: 'VAULT_SECRET_ID'), + ]) { + // scm is configured to check out to the ./kibana directory + dir('kibana') { + if (config.bootstrapped) { + kibanaPipeline.doSetup() + } + + closure() + } + } + } + } +} + +// Worker for ci processes. Extends the base worker and adds GCS artifact upload, error reporting, junit processing +def ci(Map params, Closure closure) { + def config = [ramDisk: true, bootstrapped: true] + params + + return base(config) { + kibanaPipeline.withGcsArtifactUpload(config.name) { + kibanaPipeline.withPostBuildReporting { + closure() + } + } + } +} + +// Worker for running the current intake jobs. Just runs a single script after bootstrap. +def intake(jobName, String script) { + return { + ci(name: jobName, label: 'linux && immutable', ramDisk: false) { + withEnv(["JOB=${jobName}"]) { + runbld(script, "Execute ${jobName}") + } + } + } +} + +// Worker for running functional tests. Runs a setup process (e.g. the kibana build) then executes a map of closures in parallel (e.g. one for each ciGroup) +def functional(name, Closure setup, Map processes) { + return { + parallelProcesses(name: name, setup: setup, processes: processes, delayBetweenProcesses: 20, label: 'tests-xl') + } +} + +/* + Creates a ci worker that can run a setup process, followed by a group of processes in parallel. + + Parameters: + name: Name of the worker for display purposes, filenames, etc. + setup: Closure to execute after the agent is bootstrapped, before starting the parallel work + processes: Map of closures that will execute in parallel after setup. Each closure is passed a unique number. + delayBetweenProcesses: Number of seconds to wait between starting the parallel processes. Useful to spread the load of heavy init processes, e.g. Elasticsearch starting up. Default: 0 + label: gobld/agent label to use, e.g. 'linux && immutable'. Default: 'tests-xl', a 32 CPU machine used for running many functional test suites in parallel +*/ +def parallelProcesses(Map params) { + def config = [name: 'parallel-worker', setup: {}, processes: [:], delayBetweenProcesses: 0, label: 'tests-xl'] + params + + ci(label: config.label, name: config.name) { + config.setup() + + def nextProcessNumber = 1 + def process = { processName, processClosure -> + def processNumber = nextProcessNumber + nextProcessNumber++ + + return { + if (config.delayBetweenProcesses && config.delayBetweenProcesses > 0) { + // This delay helps smooth out CPU load caused by ES/Kibana instances starting up at the same time + def delay = (processNumber-1)*config.delayBetweenProcesses + sleep(delay) + } + + processClosure(processNumber) + } + } + + def processes = [:] + config.processes.each { processName, processClosure -> + processes[processName] = process(processName, processClosure) + } + + parallel(processes) + } +} + +return this From be0a4c4e229ca3c8bd619cb95965a1049ada6b11 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 2 Mar 2020 13:20:18 -0700 Subject: [PATCH 20/26] Downgrade "setting up plugin" log to debug (#58776) --- .../integration_tests/generate_plugin.test.js | 1 + src/core/server/plugins/plugin.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js index 771bf43c4020a..d7d4dc14519c3 100644 --- a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js +++ b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js @@ -102,6 +102,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug 'start', '--optimize.enabled=false', '--logging.json=false', + '--logging.verbose=true', '--migrations.skip=true', ], cwd: generatedPath, diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index d6c774f6fc41c..b372874264eb5 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -95,7 +95,7 @@ export class PluginWrapper< public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { this.instance = this.createPluginInstance(); - this.log.info('Setting up plugin'); + this.log.debug('Setting up plugin'); return this.instance.setup(setupContext, plugins); } @@ -112,6 +112,8 @@ export class PluginWrapper< throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } + this.log.debug('Starting plugin'); + const startContract = await this.instance.start(startContext, plugins); this.startDependencies$.next([startContext, plugins]); return startContract; From b7c8e3a25229c4fcc6bf9a2ff9364860927a77ac Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Mon, 2 Mar 2020 20:56:07 +0000 Subject: [PATCH 21/26] Dashboard a11y tests (#58122) * adding comprehensive dashboard tests * fixing delete and adding dima changes * Fixing some of the a11y test failures * Fixing i18n issue * Extracting exit fullscreen logic in a separate function * Fixing typo * Upgrading axe * Fixing failing jest tests * Removing main tag as it was causing a test to fail * Adding focusable=false to a range control as well * Update test/accessibility/apps/dashboard.ts Co-Authored-By: Michail Yasonik * Fixing linting error * Update src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx Co-Authored-By: Michail Yasonik * Add comments Co-authored-by: Bhavya RM Co-authored-by: Michail Yasonik Co-authored-by: Elastic Machine --- package.json | 2 +- .../__snapshots__/list_control.test.tsx.snap | 1 + .../public/components/vis/list_control.tsx | 14 ++ .../__snapshots__/clone_modal.test.js.snap | 1 + .../np_ready/top_nav/clone_modal.tsx | 3 + .../tool_bar_pager_buttons.test.tsx.snap | 2 + .../pager/tool_bar_pager_buttons.tsx | 13 ++ .../validated_range/validated_dual_range.js | 1 + .../query_string_input/language_switcher.tsx | 2 +- test/accessibility/apps/dashboard.ts | 131 ++++++++++++++++-- .../functional/page_objects/dashboard_page.ts | 7 + yarn.lock | 8 +- 12 files changed, 171 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 0f04a2fba3b65..5db93e5ab5ab9 100644 --- a/package.json +++ b/package.json @@ -384,7 +384,7 @@ "@typescript-eslint/parser": "^2.15.0", "angular-mocks": "^1.7.9", "archiver": "^3.1.1", - "axe-core": "^3.3.2", + "axe-core": "^3.4.1", "babel-eslint": "^10.0.3", "babel-jest": "^24.9.0", "babel-plugin-dynamic-import-node": "^2.3.0", diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap index 99482a4be2d7b..59ae99260cecd 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap @@ -25,6 +25,7 @@ exports[`renders ListControl 1`] = ` compressed={false} data-test-subj="listControlSelect0" fullWidth={false} + inputRef={[Function]} isClearable={true} isLoading={false} onChange={[Function]} diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx b/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx index d62adfdce56b4..d01cef15ea41b 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx @@ -58,8 +58,17 @@ class ListControlUi extends PureComponent { + if (this.textInput) { + this.textInput.setAttribute('focusable', 'false'); // remove when #59039 is fixed + } this.isMounted = true; }; @@ -67,6 +76,10 @@ class ListControlUi extends PureComponent { + this.textInput = ref; + }; + handleOnChange = (selectedOptions: any[]) => { const selectedValues = selectedOptions.map(({ value }) => { return value; @@ -143,6 +156,7 @@ class ListControlUi extends PureComponent ); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap index f5a00e5435ed6..771d53b73d960 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap @@ -28,6 +28,7 @@ exports[`renders DashboardCloneModal 1`] = ` { @@ -41,6 +48,12 @@ export function ToolBarPagerButtons(props: Props) { onClick={() => props.onPageNext()} disabled={!props.hasNextPage} data-test-subj="btnNextPage" + aria-label={i18n.translate( + 'kbn.ddiscover.docTable.pager.toolbarPagerButtons.nextButtonAriaLabel', + { + defaultMessage: 'Next page in table', + } + )} > diff --git a/src/legacy/ui/public/validated_range/validated_dual_range.js b/src/legacy/ui/public/validated_range/validated_dual_range.js index 8689397a78333..3b0efba11afcc 100644 --- a/src/legacy/ui/public/validated_range/validated_dual_range.js +++ b/src/legacy/ui/public/validated_range/validated_dual_range.js @@ -92,6 +92,7 @@ export class ValidatedDualRange extends Component { fullWidth={fullWidth} value={this.state.value} onChange={this._onChange} + focusable={false} // remove when #59039 is fixed {...rest} /> diff --git a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx index d86a8a970a8e7..63f6997ce2fc3 100644 --- a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx +++ b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx @@ -68,7 +68,7 @@ export function QueryLanguageSwitcher(props: Props) { return ( { const dashboardName = 'Dashboard Listing A11y'; + const clonedDashboardName = 'Dashboard Listing A11y Copy'; + before(async () => { - await esArchiver.loadIfNeeded('logstash_functional'); - await kibanaServer.uiSettings.update({ - defaultIndex: 'logstash-*', - }); + await PageObjects.common.navigateToUrl('home', 'tutorial_directory/sampleData'); + await PageObjects.home.addSampleDataSet('flights'); + }); + + after(async () => { await PageObjects.common.navigateToApp('dashboard'); + await listingTable.searchForItemWithName(dashboardName); + await listingTable.checkListingSelectAllCheckbox(); + await listingTable.clickDeleteSelected(); + await PageObjects.common.clickConfirmOnModal(); }); it('dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); await a11y.testAppSnapshot(); }); it('create dashboard button', async () => { - await PageObjects.dashboard.clickCreateDashboardPrompt(); + await PageObjects.dashboard.clickNewDashboard(); await a11y.testAppSnapshot(); }); @@ -49,9 +58,115 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); + it('Open Edit mode', async () => { + await PageObjects.dashboard.switchToEditMode(); + await a11y.testAppSnapshot(); + }); + + it('Open add panel', async () => { + await dashboardAddPanel.clickOpenAddPanel(); + await a11y.testAppSnapshot(); + }); + + it('add a visualization', async () => { + await testSubjects.click('savedObjectTitle[Flights]-Delay-Buckets'); + await a11y.testAppSnapshot(); + }); + + it('add a saved search', async () => { + await dashboardAddPanel.addSavedSearch('[Flights] Flight Log'); + await a11y.testAppSnapshot(); + }); + + it('save the dashboard', async () => { + await PageObjects.dashboard.saveDashboard(dashboardName); + await a11y.testAppSnapshot(); + }); + + it('Open Edit mode', async () => { + await PageObjects.dashboard.switchToEditMode(); + await a11y.testAppSnapshot(); + }); + + it('open options menu', async () => { + await PageObjects.dashboard.openOptions(); + await a11y.testAppSnapshot(); + }); + + it('Should be able to hide panel titles', async () => { + await testSubjects.click('dashboardPanelTitlesCheckbox'); + await a11y.testAppSnapshot(); + }); + + it('Should be able display panels without margins', async () => { + await testSubjects.click('dashboardMarginsCheckbox'); + await a11y.testAppSnapshot(); + }); + + it('Open add panel', async () => { + await dashboardAddPanel.clickOpenAddPanel(); + await a11y.testAppSnapshot(); + }); + + it('Add one more saved object to cancel it', async () => { + await testSubjects.click('savedObjectTitle[Flights]-Average-Ticket-Price'); + await a11y.testAppSnapshot(); + }); + + it('Close add panel', async () => { + await dashboardAddPanel.closeAddPanel(); + await a11y.testAppSnapshot(); + }); + + it('Exit out of edit mode', async () => { + await PageObjects.dashboard.clickCancelOutOfEditMode(); + await a11y.testAppSnapshot(); + }); + + it('Discard changes', async () => { + await PageObjects.common.clickConfirmOnModal(); + await PageObjects.dashboard.getIsInViewMode(); + await a11y.testAppSnapshot(); + }); + + it('Test full screen', async () => { + await PageObjects.dashboard.clickFullScreenMode(); + await a11y.testAppSnapshot(); + }); + + it('Exit out of full screen mode', async () => { + await PageObjects.dashboard.exitFullScreenMode(); + await a11y.testAppSnapshot(); + }); + + it('Make a clone of the dashboard', async () => { + await PageObjects.dashboard.clickClone(); + await a11y.testAppSnapshot(); + }); + + it('Confirm clone with *copy* appended', async () => { + await PageObjects.dashboard.confirmClone(); + await a11y.testAppSnapshot(); + }); + it('Dashboard listing table', async () => { await PageObjects.dashboard.gotoDashboardLandingPage(); await a11y.testAppSnapshot(); }); + + it('Delete a11y clone dashboard', async () => { + await listingTable.searchForItemWithName(clonedDashboardName); + await listingTable.checkListingSelectAllCheckbox(); + await listingTable.clickDeleteSelected(); + await a11y.testAppSnapshot(); + await PageObjects.common.clickConfirmOnModal(); + await listingTable.searchForItemWithName(''); + }); + + // Blocked by https://github.com/elastic/kibana/issues/38980 + it.skip('Open flight dashboard', async () => { + await testSubjects.click('dashboardListingTitleLink-[Flights]-Global-Flight-Dashboard'); + await a11y.testAppSnapshot(); + }); }); } diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 3fd7ce27e27d4..0f01097cf50dc 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -65,6 +65,13 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide await this.waitForRenderComplete(); } + public async exitFullScreenMode() { + log.debug(`exitFullScreenMode`); + const logoButton = await this.getExitFullScreenLogoButton(); + await logoButton.moveMouseTo(); + await this.clickExitFullScreenTextButton(); + } + public async fullScreenModeMenuItemExists() { return await testSubjects.exists('dashboardFullScreenMode'); } diff --git a/yarn.lock b/yarn.lock index 7f38495c20f4a..338d516a796e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7121,10 +7121,10 @@ aws4@^1.6.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" integrity sha1-g+9cqGCysy5KDe7e6MdxudtXRx4= -axe-core@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.3.2.tgz#7baf3c55a5cf1621534a2c38735f5a1bf2f7e1a8" - integrity sha512-lRdxsRt7yNhqpcXQk1ao1BL73OZDzmFCWOG0mC4tGR/r14ohH2payjHwCMQjHGbBKm924eDlmG7utAGHiX/A6g== +axe-core@^3.4.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.1.tgz#d8d5aaef73f003e8b766ea28bb078343f3622201" + integrity sha512-mwpDgPwWB+5kMHyLjlxh4w25ClJfqSxi+c6LQ4ix349TdCUctMwJNPTkhPD1qP9SYIjFgjeVpVZWCvK9oBGwCg== axios@^0.18.0, axios@^0.18.1: version "0.18.1" From bb6fd0bf4ff8e0a501747aef4b399e997cc25e6b Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 2 Mar 2020 15:15:44 -0700 Subject: [PATCH 22/26] [kbn/optimizer] fix ui/* url rewrites in dist (#58627) * [kbn/optimizer] fix ui/* url rewrites in dist * add tests to verify styles are built correctly and ui-rewrites are happening * clarify change to dirs creation * create tested & shared parsePath helper * update renovate config * split implementation of parsePath for dir and file paths * switch to valid css property Co-authored-by: Elastic Machine --- package.json | 1 + .../mock_repo/plugins/bar/public/index.ts | 1 + .../plugins/bar/public/legacy/styles.scss | 4 + .../mock_repo/src/legacy/ui/public/icon.svg | 1 + .../ui/public/styles/_styling_constants.scss | 1 + .../basic_optimization.test.ts.snap | 527 +----------------- .../basic_optimization.test.ts | 56 +- .../__snapshots__/parse_path.test.ts.snap | 156 ++++++ .../src/worker/parse_path.test.ts | 20 +- .../kbn-optimizer/src/worker/parse_path.ts | 43 ++ .../kbn-optimizer/src/worker/run_compilers.ts | 16 +- .../src/worker/webpack.config.ts | 7 +- renovate.json5 | 8 + yarn.lock | 5 + 14 files changed, 299 insertions(+), 547 deletions(-) create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/icon.svg create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_styling_constants.scss create mode 100644 packages/kbn-optimizer/src/worker/__snapshots__/parse_path.test.ts.snap rename typings/normalize_path/index.d.ts => packages/kbn-optimizer/src/worker/parse_path.test.ts (57%) create mode 100644 packages/kbn-optimizer/src/worker/parse_path.ts diff --git a/package.json b/package.json index 5db93e5ab5ab9..e727d87a83c53 100644 --- a/package.json +++ b/package.json @@ -349,6 +349,7 @@ "@types/mustache": "^0.8.31", "@types/node": "^10.12.27", "@types/node-forge": "^0.9.0", + "@types/normalize-path": "^3.0.0", "@types/numeral": "^0.0.26", "@types/opn": "^5.1.0", "@types/pegjs": "^0.10.1", diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts index 66fa55479f3b9..817c4796562e8 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts @@ -17,6 +17,7 @@ * under the License. */ +import './legacy/styles.scss'; import { fooLibFn } from '../../foo/public/index'; export * from './lib'; export { fooLibFn }; diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss new file mode 100644 index 0000000000000..e71a2d485a2f8 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss @@ -0,0 +1,4 @@ +body { + width: $globalStyleConstant; + background-image: url("ui/icon.svg"); +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/icon.svg b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/icon.svg new file mode 100644 index 0000000000000..ae7d5b958bbad --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_styling_constants.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_styling_constants.scss new file mode 100644 index 0000000000000..83995ca65211b --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_styling_constants.scss @@ -0,0 +1 @@ +$globalStyleConstant: 10; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 706f79978beee..1a974d3e81092 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -5,553 +5,56 @@ OptimizerConfig { "bundles": Array [ Bundle { "cache": BundleCache { - "path": /plugins/bar/target/public/.kbn-optimizer-cache, + "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public/.kbn-optimizer-cache, "state": undefined, }, - "contextDir": /plugins/bar, + "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar, "entry": "./public/index", "id": "bar", - "outputDir": /plugins/bar/target/public, - "sourceRoot": , + "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public, + "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, "type": "plugin", }, Bundle { "cache": BundleCache { - "path": /plugins/foo/target/public/.kbn-optimizer-cache, + "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public/.kbn-optimizer-cache, "state": undefined, }, - "contextDir": /plugins/foo, + "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo, "entry": "./public/index", "id": "foo", - "outputDir": /plugins/foo/target/public, - "sourceRoot": , + "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public, + "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, "type": "plugin", }, ], "cache": true, - "dist": false, + "dist": true, "inspectWorkers": false, "maxWorkerCount": 1, "plugins": Array [ Object { - "directory": /plugins/bar, + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar, "id": "bar", "isUiPlugin": true, }, Object { - "directory": /plugins/baz, + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/baz, "id": "baz", "isUiPlugin": false, }, Object { - "directory": /plugins/foo, + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo, "id": "foo", "isUiPlugin": true, }, ], "profileWebpack": false, - "repoRoot": , + "repoRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, "watch": false, } `; -exports[`builds expected bundles, saves bundle counts to metadata: bar bundle 1`] = ` -"var __kbnBundles__ = typeof __kbnBundles__ === \\"object\\" ? __kbnBundles__ : {}; __kbnBundles__[\\"plugin/bar\\"] = -/******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) { -/******/ return installedModules[moduleId].exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ i: moduleId, -/******/ l: false, -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Flag the module as loaded -/******/ module.l = true; -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; -/******/ -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; -/******/ -/******/ // define getter function for harmony exports -/******/ __webpack_require__.d = function(exports, name, getter) { -/******/ if(!__webpack_require__.o(exports, name)) { -/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); -/******/ } -/******/ }; -/******/ -/******/ // define __esModule on exports -/******/ __webpack_require__.r = function(exports) { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ -/******/ // create a fake namespace object -/******/ // mode & 1: value is a module id, require it -/******/ // mode & 2: merge all properties of value into the ns -/******/ // mode & 4: return value when already ns object -/******/ // mode & 8|1: behave like require -/******/ __webpack_require__.t = function(value, mode) { -/******/ if(mode & 1) value = __webpack_require__(value); -/******/ if(mode & 8) return value; -/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; -/******/ var ns = Object.create(null); -/******/ __webpack_require__.r(ns); -/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); -/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); -/******/ return ns; -/******/ }; -/******/ -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { -/******/ var getter = module && module.__esModule ? -/******/ function getDefault() { return module['default']; } : -/******/ function getModuleExports() { return module; }; -/******/ __webpack_require__.d(getter, 'a', getter); -/******/ return getter; -/******/ }; -/******/ -/******/ // Object.prototype.hasOwnProperty.call -/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; -/******/ -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = \\"__REPLACE_WITH_PUBLIC_PATH__\\"; -/******/ -/******/ -/******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = \\"./public/index.ts\\"); -/******/ }) -/************************************************************************/ -/******/ ({ +exports[`builds expected bundles, saves bundle counts to metadata: bar bundle 1`] = `"var __kbnBundles__=typeof __kbnBundles__===\\"object\\"?__kbnBundles__:{};__kbnBundles__[\\"plugin/bar\\"]=function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!==\\"undefined\\"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:\\"Module\\"})}Object.defineProperty(exports,\\"__esModule\\",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value===\\"object\\"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,\\"default\\",{enumerable:true,value:value});if(mode&2&&typeof value!=\\"string\\")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module[\\"default\\"]}:function getModuleExports(){return module};__webpack_require__.d(getter,\\"a\\",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p=\\"__REPLACE_WITH_PUBLIC_PATH__\\";return __webpack_require__(__webpack_require__.s=4)}([function(module,exports,__webpack_require__){\\"use strict\\";var isOldIE=function isOldIE(){var memo;return function memorize(){if(typeof memo===\\"undefined\\"){memo=Boolean(window&&document&&document.all&&!window.atob)}return memo}}();var getTarget=function getTarget(){var memo={};return function memorize(target){if(typeof memo[target]===\\"undefined\\"){var styleTarget=document.querySelector(target);if(window.HTMLIFrameElement&&styleTarget instanceof window.HTMLIFrameElement){try{styleTarget=styleTarget.contentDocument.head}catch(e){styleTarget=null}}memo[target]=styleTarget}return memo[target]}}();var stylesInDom=[];function getIndexByIdentifier(identifier){var result=-1;for(var i=0;i { await del(TMP_DIR); @@ -51,20 +51,25 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { repoRoot: MOCK_REPO_DIR, pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], maxWorkerCount: 1, + dist: true, }); expect(config).toMatchSnapshot('OptimizerConfig'); - const msgs = await runOptimizer(config) - .pipe( - tap(state => { - if (state.event?.type === 'worker stdio') { - // eslint-disable-next-line no-console - console.log('worker', state.event.stream, state.event.chunk.toString('utf8')); + const log = new ToolingLog({ + level: 'error', + writeTo: { + write(chunk) { + if (chunk.endsWith('\n')) { + chunk = chunk.slice(0, -1); } - }), - toArray() - ) + // eslint-disable-next-line no-console + console.error(chunk); + }, + }, + }); + const msgs = await runOptimizer(config) + .pipe(logOptimizerState(log, config), toArray()) .toPromise(); const assert = (statement: string, truth: boolean, altStates?: OptimizerUpdate[]) => { @@ -133,23 +138,31 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { expect(foo.cache.getModuleCount()).toBe(3); expect(foo.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ - /plugins/foo/public/ext.ts, - /plugins/foo/public/index.ts, - /plugins/foo/public/lib.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, ] `); const bar = config.bundles.find(b => b.id === 'bar')!; expect(bar).toBeTruthy(); bar.cache.refresh(); - expect(bar.cache.getModuleCount()).toBe(5); + expect(bar.cache.getModuleCount()).toBe( + // code + styles + style/css-loader runtime + 14 + ); + expect(bar.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ - /plugins/foo/public/ext.ts, - /plugins/foo/public/index.ts, - /plugins/foo/public/lib.ts, - /plugins/bar/public/index.ts, - /plugins/bar/public/lib.ts, + /node_modules/css-loader/package.json, + /node_modules/style-loader/package.json, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/legacy/styles.scss, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/lib.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/icon.svg, ] `); }); @@ -159,6 +172,7 @@ it('uses cache on second run and exist cleanly', async () => { repoRoot: MOCK_REPO_DIR, pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], maxWorkerCount: 1, + dist: true, }); const msgs = await runOptimizer(config) diff --git a/packages/kbn-optimizer/src/worker/__snapshots__/parse_path.test.ts.snap b/packages/kbn-optimizer/src/worker/__snapshots__/parse_path.test.ts.snap new file mode 100644 index 0000000000000..2973ac116d6bd --- /dev/null +++ b/packages/kbn-optimizer/src/worker/__snapshots__/parse_path.test.ts.snap @@ -0,0 +1,156 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`parseDirPath() parses / 1`] = ` +Object { + "dirs": Array [], + "filename": undefined, + "root": "/", +} +`; + +exports[`parseDirPath() parses /foo 1`] = ` +Object { + "dirs": Array [ + "foo", + ], + "filename": undefined, + "root": "/", +} +`; + +exports[`parseDirPath() parses /foo/bar/baz 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + "baz", + ], + "filename": undefined, + "root": "/", +} +`; + +exports[`parseDirPath() parses /foo/bar/baz/ 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + "baz", + ], + "filename": undefined, + "root": "/", +} +`; + +exports[`parseDirPath() parses c:\\ 1`] = ` +Object { + "dirs": Array [], + "filename": undefined, + "root": "c:", +} +`; + +exports[`parseDirPath() parses c:\\foo 1`] = ` +Object { + "dirs": Array [ + "foo", + ], + "filename": undefined, + "root": "c:", +} +`; + +exports[`parseDirPath() parses c:\\foo\\bar\\baz 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + "baz", + ], + "filename": undefined, + "root": "c:", +} +`; + +exports[`parseDirPath() parses c:\\foo\\bar\\baz\\ 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + "baz", + ], + "filename": undefined, + "root": "c:", +} +`; + +exports[`parseFilePath() parses /foo 1`] = ` +Object { + "dirs": Array [], + "filename": "foo", + "root": "/", +} +`; + +exports[`parseFilePath() parses /foo/bar/baz 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz", + "root": "/", +} +`; + +exports[`parseFilePath() parses /foo/bar/baz.json 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz.json", + "root": "/", +} +`; + +exports[`parseFilePath() parses c:/foo/bar/baz.json 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz.json", + "root": "c:", +} +`; + +exports[`parseFilePath() parses c:\\foo 1`] = ` +Object { + "dirs": Array [], + "filename": "foo", + "root": "c:", +} +`; + +exports[`parseFilePath() parses c:\\foo\\bar\\baz 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz", + "root": "c:", +} +`; + +exports[`parseFilePath() parses c:\\foo\\bar\\baz.json 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz.json", + "root": "c:", +} +`; diff --git a/typings/normalize_path/index.d.ts b/packages/kbn-optimizer/src/worker/parse_path.test.ts similarity index 57% rename from typings/normalize_path/index.d.ts rename to packages/kbn-optimizer/src/worker/parse_path.test.ts index 31e064ca63d90..72197e8c8fb07 100644 --- a/typings/normalize_path/index.d.ts +++ b/packages/kbn-optimizer/src/worker/parse_path.test.ts @@ -17,8 +17,20 @@ * under the License. */ -declare function NormalizePath(path: string, stripTrailing?: boolean): string; +import { parseFilePath, parseDirPath } from './parse_path'; -declare module 'normalize-path' { - export = NormalizePath; -} +const DIRS = ['/', '/foo/bar/baz/', 'c:\\', 'c:\\foo\\bar\\baz\\']; +const AMBIGUOUS = ['/foo', '/foo/bar/baz', 'c:\\foo', 'c:\\foo\\bar\\baz']; +const FILES = ['/foo/bar/baz.json', 'c:/foo/bar/baz.json', 'c:\\foo\\bar\\baz.json']; + +describe('parseFilePath()', () => { + it.each([...FILES, ...AMBIGUOUS])('parses %s', path => { + expect(parseFilePath(path)).toMatchSnapshot(); + }); +}); + +describe('parseDirPath()', () => { + it.each([...DIRS, ...AMBIGUOUS])('parses %s', path => { + expect(parseDirPath(path)).toMatchSnapshot(); + }); +}); diff --git a/packages/kbn-optimizer/src/worker/parse_path.ts b/packages/kbn-optimizer/src/worker/parse_path.ts new file mode 100644 index 0000000000000..88152df55b84f --- /dev/null +++ b/packages/kbn-optimizer/src/worker/parse_path.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import normalizePath from 'normalize-path'; + +/** + * Parse an absolute path, supporting normalized paths from webpack, + * into a list of directories and root + */ +export function parseDirPath(path: string) { + const filePath = parseFilePath(path); + return { + ...filePath, + dirs: [...filePath.dirs, ...(filePath.filename ? [filePath.filename] : [])], + filename: undefined, + }; +} + +export function parseFilePath(path: string) { + const normalized = normalizePath(path); + const [root, ...others] = normalized.split('/'); + return { + root: root === '' ? '/' : root, + dirs: others.slice(0, -1), + filename: others[others.length - 1] || undefined, + }; +} diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts index 7dcce8a0fae8d..7a8097fd2b2c7 100644 --- a/packages/kbn-optimizer/src/worker/run_compilers.ts +++ b/packages/kbn-optimizer/src/worker/run_compilers.ts @@ -27,9 +27,10 @@ import webpack, { Stats } from 'webpack'; import * as Rx from 'rxjs'; import { mergeMap, map, mapTo, takeUntil } from 'rxjs/operators'; -import { CompilerMsgs, CompilerMsg, maybeMap, Bundle, WorkerConfig } from '../common'; +import { CompilerMsgs, CompilerMsg, maybeMap, Bundle, WorkerConfig, ascending } from '../common'; import { getWebpackConfig } from './webpack.config'; import { isFailureStats, failedStatsToErrorMessage } from './webpack_helpers'; +import { parseFilePath } from './parse_path'; import { isExternalModule, isNormalModule, @@ -108,20 +109,19 @@ const observeCompiler = ( for (const module of normalModules) { const path = getModulePath(module); + const parsedPath = parseFilePath(path); - const parsedPath = Path.parse(path); - const dirSegments = parsedPath.dir.split(Path.sep); - if (!dirSegments.includes('node_modules')) { + if (!parsedPath.dirs.includes('node_modules')) { referencedFiles.add(path); continue; } - const nmIndex = dirSegments.lastIndexOf('node_modules'); - const isScoped = dirSegments[nmIndex + 1].startsWith('@'); + const nmIndex = parsedPath.dirs.lastIndexOf('node_modules'); + const isScoped = parsedPath.dirs[nmIndex + 1].startsWith('@'); referencedFiles.add( Path.join( parsedPath.root, - ...dirSegments.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), + ...parsedPath.dirs.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), 'package.json' ) ); @@ -146,7 +146,7 @@ const observeCompiler = ( optimizerCacheKey: workerConfig.optimizerCacheKey, cacheKey: bundle.createCacheKey(files, mtimes), moduleCount: normalModules.length, - files, + files: files.sort(ascending(f => f)), }); return compilerMsgs.compilerSuccess({ diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 3c6ae78bc4d91..5d8ef7626f630 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -30,6 +30,7 @@ import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import * as SharedDeps from '@kbn/ui-shared-deps'; import { Bundle, WorkerConfig } from '../common'; +import { parseDirPath } from './parse_path'; const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); @@ -135,7 +136,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { } // manually force ui/* urls in legacy styles to resolve to ui/legacy/public - if (uri.startsWith('ui/') && base.split(Path.sep).includes('legacy')) { + if (uri.startsWith('ui/') && parseDirPath(base).dirs.includes('legacy')) { return Path.resolve( worker.repoRoot, 'src/legacy/ui/public', @@ -150,7 +151,9 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { { loader: 'sass-loader', options: { - sourceMap: !worker.dist, + // must always be enabled as long as we're using the `resolve-url-loader` to + // rewrite `ui/*` urls. They're dropped by subsequent loaders though + sourceMap: true, prependData(loaderContext: webpack.loader.LoaderContext) { return `@import ${stringifyRequest( loaderContext, diff --git a/renovate.json5 b/renovate.json5 index 58a64a5d0f967..ca2cd2e6bcd93 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -665,6 +665,14 @@ '@types/nodemailer', ], }, + { + groupSlug: 'normalize-path', + groupName: 'normalize-path related packages', + packageNames: [ + 'normalize-path', + '@types/normalize-path', + ], + }, { groupSlug: 'numeral', groupName: 'numeral related packages', diff --git a/yarn.lock b/yarn.lock index 338d516a796e1..e4d5dcce5bca0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4864,6 +4864,11 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/normalize-path@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/normalize-path/-/normalize-path-3.0.0.tgz#bb5c46cab77b93350b4cf8d7ff1153f47189ae31" + integrity sha512-Nd8y/5t/7CRakPYiyPzr/IAfYusy1FkcZYFEAcoMZkwpJv2n4Wm+olW+e7xBdHEXhOnWdG9ddbar0gqZWS4x5Q== + "@types/numeral@^0.0.25": version "0.0.25" resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-0.0.25.tgz#b6f55062827a4787fe4ab151cf3412a468e65271" From 48a33abdeed147932401cc3a24c36669189f67f3 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 2 Mar 2020 15:51:33 -0700 Subject: [PATCH 23/26] Remove appBasePath from docs + add mock for AppMountParameters (#58775) --- ...lugin-public.appmountparameters.history.md | 1 - ...in-public.appmountparameters.onappleave.md | 4 +-- src/core/CONVENTIONS.md | 4 +-- src/core/TESTING.md | 14 ++++++----- src/core/public/application/types.ts | 5 ++-- src/core/public/mocks.ts | 25 ++++++++++++++++++- 6 files changed, 38 insertions(+), 15 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.history.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.history.md index 9a3fa1a1bb48a..f22e70b0e7bee 100644 --- a/docs/development/core/public/kibana-plugin-public.appmountparameters.history.md +++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.history.md @@ -44,7 +44,6 @@ import { MyPluginDepsStart } from './plugin'; export renderApp = ({ element, history }: AppMountParameters) => { ReactDOM.render( - // pass `appBasePath` to `basename` , diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.onappleave.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.onappleave.md index 283ae34f14c54..6c5b89ffda05b 100644 --- a/docs/development/core/public/kibana-plugin-public.appmountparameters.onappleave.md +++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.onappleave.md @@ -26,7 +26,7 @@ import { BrowserRouter, Route } from 'react-router-dom'; import { CoreStart, AppMountParams } from 'src/core/public'; import { MyPluginDepsStart } from './plugin'; -export renderApp = ({ appBasePath, element, onAppLeave }: AppMountParams) => { +export renderApp = ({ element, history, onAppLeave }: AppMountParams) => { const { renderApp, hasUnsavedChanges } = await import('./application'); onAppLeave(actions => { if(hasUnsavedChanges()) { @@ -34,7 +34,7 @@ export renderApp = ({ appBasePath, element, onAppLeave }: AppMountParams) => { } return actions.default(); }); - return renderApp(params); + return renderApp({ element, history }); } ``` diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index 2769079757bc3..0f592d108c561 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -148,8 +148,8 @@ import { MyAppRoot } from './components/app.ts'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. */ -export const renderApp = (core: CoreStart, deps: MyPluginDepsStart, { element, appBasePath }: AppMountParams) => { - ReactDOM.render(, element); +export const renderApp = (core: CoreStart, deps: MyPluginDepsStart, { element, history }: AppMountParams) => { + ReactDOM.render(, element); return () => ReactDOM.unmountComponentAtNode(element); } ``` diff --git a/src/core/TESTING.md b/src/core/TESTING.md index 9abc2bb77d7d1..cb38dac0e20ce 100644 --- a/src/core/TESTING.md +++ b/src/core/TESTING.md @@ -453,7 +453,7 @@ describe('Plugin', () => { const [coreStartMock, startDepsMock] = await coreSetup.getStartServices(); const unmountMock = jest.fn(); renderAppMock.mockReturnValue(unmountMock); - const params = { element: document.createElement('div'), appBasePath: '/fake/base/path' }; + const params = coreMock.createAppMountParamters('/fake/base/path'); new Plugin(coreMock.createPluginInitializerContext()).setup(coreSetup); // Grab registered mount function @@ -478,7 +478,7 @@ import ReactDOM from 'react-dom'; import { AppMountParams, CoreStart } from 'src/core/public'; import { AppRoot } from './components/app_root'; -export const renderApp = ({ element, appBasePath }: AppMountParams, core: CoreStart, plugins: MyPluginDepsStart) => { +export const renderApp = ({ element, history }: AppMountParams, core: CoreStart, plugins: MyPluginDepsStart) => { // Hide the chrome while this app is mounted for a full screen experience core.chrome.setIsVisible(false); @@ -491,7 +491,7 @@ export const renderApp = ({ element, appBasePath }: AppMountParams, core: CoreSt // Render app ReactDOM.render( - , + , element ); @@ -512,12 +512,14 @@ In testing `renderApp` you should be verifying that: ```typescript /** public/application.test.ts */ +import { createMemoryHistory } from 'history'; +import { ScopedHistory } from 'src/core/public'; import { coreMock } from 'src/core/public/mocks'; import { renderApp } from './application'; describe('renderApp', () => { it('mounts and unmounts UI', () => { - const params = { element: document.createElement('div'), appBasePath: '/fake/base/path' }; + const params = coreMock.createAppMountParamters('/fake/base/path'); const core = coreMock.createStart(); // Verify some expected DOM element is rendered into the element @@ -529,7 +531,7 @@ describe('renderApp', () => { }); it('unsubscribes from uiSettings', () => { - const params = { element: document.createElement('div'), appBasePath: '/fake/base/path' }; + const params = coreMock.createAppMountParamters('/fake/base/path'); const core = coreMock.createStart(); // Create a fake Subject you can use to monitor observers const settings$ = new Subject(); @@ -544,7 +546,7 @@ describe('renderApp', () => { }); it('resets chrome visibility', () => { - const params = { element: document.createElement('div'), appBasePath: '/fake/base/path' }; + const params = coreMock.createAppMountParamters('/fake/base/path'); const core = coreMock.createStart(); // Verify stateful Core API was called on mount diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index facb818c60ff9..318afb652999e 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -347,7 +347,6 @@ export interface AppMountParameters { * * export renderApp = ({ element, history }: AppMountParameters) => { * ReactDOM.render( - * // pass `appBasePath` to `basename` * * * , @@ -429,7 +428,7 @@ export interface AppMountParameters { * import { CoreStart, AppMountParams } from 'src/core/public'; * import { MyPluginDepsStart } from './plugin'; * - * export renderApp = ({ appBasePath, element, onAppLeave }: AppMountParams) => { + * export renderApp = ({ element, history, onAppLeave }: AppMountParams) => { * const { renderApp, hasUnsavedChanges } = await import('./application'); * onAppLeave(actions => { * if(hasUnsavedChanges()) { @@ -437,7 +436,7 @@ export interface AppMountParameters { * } * return actions.default(); * }); - * return renderApp(params); + * return renderApp({ element, history }); * } * ``` */ diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 8ea672890ca29..c860e9de8334e 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -16,9 +16,15 @@ * specific language governing permissions and limitations * under the License. */ +import { createMemoryHistory } from 'history'; + +// Only import types from '.' to avoid triggering default Jest mocks. +import { CoreContext, PluginInitializerContext, AppMountParameters } from '.'; +// Import values from their individual modules instead. +import { ScopedHistory } from './application'; + import { applicationServiceMock } from './application/application_service.mock'; import { chromeServiceMock } from './chrome/chrome_service.mock'; -import { CoreContext, PluginInitializerContext } from '.'; import { docLinksServiceMock } from './doc_links/doc_links_service.mock'; import { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock'; import { httpServiceMock } from './http/http_service.mock'; @@ -139,10 +145,27 @@ function createStorageMock() { return storageMock; } +function createAppMountParametersMock(appBasePath = '') { + // Assemble an in-memory history mock using the provided basePath + const rawHistory = createMemoryHistory(); + rawHistory.push(appBasePath); + const history = new ScopedHistory(rawHistory, appBasePath); + + const params: jest.Mocked = { + appBasePath, + element: document.createElement('div'), + history, + onAppLeave: jest.fn(), + }; + + return params; +} + export const coreMock = { createCoreContext, createSetup: createCoreSetupMock, createStart: createCoreStartMock, createPluginInitializerContext: pluginInitializerContextMock, createStorage: createStorageMock, + createAppMountParamters: createAppMountParametersMock, }; From 90b3678dffa20ff7e0ab806861bfa64d46d86cb9 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 2 Mar 2020 16:04:29 -0700 Subject: [PATCH 24/26] [SIEM] [Case] Comments to case view (#58315) --- .../edit_data_provider/translations.ts | 2 +- .../components/formatted_date/index.tsx | 29 +++ .../editable_title.test.tsx.snap | 2 +- .../components/header_page/editable_title.tsx | 64 ++++-- .../components/header_page/translations.ts | 4 +- .../components/markdown_editor/constants.ts | 7 + .../components/markdown_editor/form.tsx | 58 ++++++ .../components/markdown_editor/index.tsx | 121 +++++++++++ .../markdown_editor/translations.ts | 18 ++ .../components/navigation/index.test.tsx | 4 +- .../siem/public/containers/case/api.ts | 38 +++- .../siem/public/containers/case/constants.ts | 2 +- .../siem/public/containers/case/types.ts | 28 ++- .../public/containers/case/use_get_case.tsx | 4 +- .../public/containers/case/use_post_case.tsx | 3 +- .../containers/case/use_post_comment.tsx | 97 +++++++++ .../containers/case/use_update_case.tsx | 68 +++--- .../containers/case/use_update_comment.tsx | 92 ++++++++ .../case/components/add_comment/index.tsx | 73 +++++++ .../case/components/add_comment/schema.tsx | 20 ++ .../components/all_cases/__mock__/index.tsx | 10 + .../case/components/all_cases/columns.tsx | 2 +- .../components/case_view/__mock__/index.tsx | 29 ++- .../case/components/case_view/index.test.tsx | 41 +++- .../pages/case/components/case_view/index.tsx | 196 ++++++------------ .../case/components/case_view/translations.ts | 12 ++ .../pages/case/components/create/index.tsx | 77 ++++--- .../pages/case/components/create/schema.tsx | 6 +- .../description_md_editor/index.tsx | 111 ---------- .../pages/case/components/tag_list/index.tsx | 68 +++--- .../pages/case/components/tag_list/schema.tsx | 2 +- .../components/user_action_tree/index.tsx | 139 ++++++++++--- .../user_action_tree/user_action_avatar.tsx | 18 ++ .../user_action_tree/user_action_item.tsx | 60 ++++++ .../user_action_tree/user_action_markdown.tsx | 89 ++++++++ .../user_action_tree/user_action_title.tsx | 70 +++++++ .../pages/case/components/user_list/index.tsx | 4 +- .../siem/public/pages/case/translations.ts | 38 +++- .../rules/components/add_item_form/index.tsx | 2 +- .../components/description_step/index.tsx | 2 +- .../rules/components/mitre/index.tsx | 2 +- .../rules/components/pick_timeline/index.tsx | 2 +- .../rules/components/query_bar/index.tsx | 2 +- .../components/schedule_item_form/index.tsx | 2 +- .../components/step_about_rule/index.tsx | 2 +- .../components/step_about_rule/schema.tsx | 2 +- .../components/step_define_rule/index.tsx | 2 +- .../components/step_define_rule/schema.tsx | 2 +- .../components/step_schedule_rule/index.tsx | 2 +- .../components/step_schedule_rule/schema.tsx | 2 +- .../detection_engine/rules/create/index.tsx | 2 +- .../detection_engine/rules/edit/index.tsx | 2 +- .../pages/detection_engine/rules/helpers.tsx | 2 +- .../pages/detection_engine/rules/types.ts | 2 +- .../siem/public/pages/home/translations.ts | 2 +- .../siem/public/{pages => }/shared_imports.ts | 8 +- .../api/__tests__/update_comment.test.ts | 19 ++ .../plugins/case/server/routes/api/schema.ts | 5 + .../case/server/routes/api/update_comment.ts | 36 +++- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 61 files changed, 1355 insertions(+), 455 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/markdown_editor/constants.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/markdown_editor/form.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/markdown_editor/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/markdown_editor/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/schema.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_avatar.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx rename x-pack/legacy/plugins/siem/public/{pages => }/shared_imports.ts (52%) diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/translations.ts b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/translations.ts index dadd349096a53..53d2ffa197327 100644 --- a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/translations.ts @@ -18,7 +18,7 @@ export const FIELD = i18n.translate('xpack.siem.editDataProvider.fieldLabel', { defaultMessage: 'Field', }); -export const FIELD_PLACEHOLDER = i18n.translate('xpack.siem.editDataProvider.fieldPlaceholder', { +export const FIELD_PLACEHOLDER = i18n.translate('xpack.siem.editDataProvider.placeholder', { defaultMessage: 'Select a field', }); diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx index f74ee995c965b..d100f89182014 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx @@ -125,3 +125,32 @@ export const FormattedRelativePreferenceDate = ({ value }: { value?: string | nu ); }; + +/** + * Renders a preceding label according to under/over one hour + */ + +export const FormattedRelativePreferenceLabel = ({ + value, + preferenceLabel, + relativeLabel, +}: { + value?: string | number | null; + preferenceLabel?: string | null; + relativeLabel?: string | null; +}) => { + if (value == null) { + return null; + } + const maybeDate = getMaybeDate(value); + if (!maybeDate.isValid()) { + return null; + } + return moment(maybeDate.toDate()) + .add(1, 'hours') + .isBefore(new Date()) ? ( + <>{preferenceLabel} + ) : ( + <>{relativeLabel} + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap index 30c70d7f5a2a6..24b1756aade2e 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap @@ -15,7 +15,7 @@ exports[`EditableTitle it renders 1`] = ` - css` margin-left: ${theme.eui.euiSize}; `} `; -StyledEuiButtonIcon.displayName = 'StyledEuiButtonIcon'; +const MySpinner = styled(EuiLoadingSpinner)` + ${({ theme }) => css` + margin-left: ${theme.eui.euiSize}; + `} +`; interface Props { isLoading: boolean; @@ -36,24 +41,30 @@ interface Props { const EditableTitleComponent: React.FC = ({ onSubmit, isLoading, title }) => { const [editMode, setEditMode] = useState(false); - const [changedTitle, onTitleChange] = useState(title); + const [changedTitle, onTitleChange] = useState(typeof title === 'string' ? title : ''); const onCancel = useCallback(() => setEditMode(false), []); const onClickEditIcon = useCallback(() => setEditMode(true), []); - const onClickSubmit = useCallback( - (newTitle: string): void => { - onSubmit(newTitle); - setEditMode(false); + const onClickSubmit = useCallback((): void => { + if (changedTitle !== title) { + onSubmit(changedTitle); + } + setEditMode(false); + }, [changedTitle, title]); + + const handleOnChange = useCallback( + (e: ChangeEvent) => { + onTitleChange(e.target.value); }, - [changedTitle] + [onTitleChange] ); return editMode ? ( onTitleChange(e.target.value)} + onChange={handleOnChange} value={`${changedTitle}`} data-test-subj="editable-title-input-field" /> @@ -61,17 +72,23 @@ const EditableTitleComponent: React.FC = ({ onSubmit, isLoading, title }) onClickSubmit(changedTitle as string)} + color="secondary" data-test-subj="editable-title-submit-btn" + fill + iconType="save" + onClick={onClickSubmit} + size="s" > - {i18n.SUBMIT} + {i18n.SAVE} - + {i18n.CANCEL} @@ -84,12 +101,15 @@ const EditableTitleComponent: React.FC = ({ onSubmit, isLoading, title }) </EuiFlexItem> <EuiFlexItem grow={false}> - <StyledEuiButtonIcon - aria-label={i18n.EDIT_TITLE_ARIA(title as string)} - iconType="pencil" - onClick={onClickEditIcon} - data-test-subj="editable-title-edit-icon" - /> + {isLoading && <MySpinner />} + {!isLoading && ( + <MyEuiButtonIcon + aria-label={i18n.EDIT_TITLE_ARIA(title as string)} + iconType="pencil" + onClick={onClickEditIcon} + data-test-subj="editable-title-edit-icon" + /> + )} </EuiFlexItem> </EuiFlexGroup> ); diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/translations.ts b/x-pack/legacy/plugins/siem/public/components/header_page/translations.ts index 2bc2ac492b0b1..764f1e5ac3731 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/header_page/translations.ts @@ -6,8 +6,8 @@ import { i18n } from '@kbn/i18n'; -export const SUBMIT = i18n.translate('xpack.siem.header.editableTitle.submit', { - defaultMessage: 'Submit', +export const SAVE = i18n.translate('xpack.siem.header.editableTitle.save', { + defaultMessage: 'Save', }); export const CANCEL = i18n.translate('xpack.siem.header.editableTitle.cancel', { diff --git a/x-pack/legacy/plugins/siem/public/components/markdown_editor/constants.ts b/x-pack/legacy/plugins/siem/public/components/markdown_editor/constants.ts new file mode 100644 index 0000000000000..dc57de5252b3e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/markdown_editor/constants.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export const MARKDOWN_HELP_LINK = 'https://www.markdownguide.org/cheat-sheet/'; diff --git a/x-pack/legacy/plugins/siem/public/components/markdown_editor/form.tsx b/x-pack/legacy/plugins/siem/public/components/markdown_editor/form.tsx new file mode 100644 index 0000000000000..3c5287a6fac24 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/markdown_editor/form.tsx @@ -0,0 +1,58 @@ +/* + * 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 { EuiFormRow } from '@elastic/eui'; +import React, { useCallback } from 'react'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../../shared_imports'; +import { MarkdownEditor } from '.'; + +interface IMarkdownEditorForm { + dataTestSubj: string; + field: FieldHook; + idAria: string; + isDisabled: boolean; + placeholder?: string; + footerContentRight?: React.ReactNode; +} +export const MarkdownEditorForm = ({ + dataTestSubj, + field, + idAria, + isDisabled = false, + placeholder, + footerContentRight, +}: IMarkdownEditorForm) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const handleContentChange = useCallback( + (newContent: string) => { + field.setValue(newContent); + }, + [field] + ); + + return ( + <EuiFormRow + label={field.label} + labelAppend={field.labelAppend} + helpText={field.helpText} + error={errorMessage} + isInvalid={isInvalid} + fullWidth + data-test-subj={dataTestSubj} + describedByIds={idAria ? [idAria] : undefined} + > + <MarkdownEditor + initialContent={field.value as string} + isDisabled={isDisabled} + footerContentRight={footerContentRight} + onChange={handleContentChange} + placeholder={placeholder} + /> + </EuiFormRow> + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/markdown_editor/index.tsx b/x-pack/legacy/plugins/siem/public/components/markdown_editor/index.tsx new file mode 100644 index 0000000000000..8572b447cced8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/markdown_editor/index.tsx @@ -0,0 +1,121 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiPanel, + EuiTabbedContent, + EuiTextArea, +} from '@elastic/eui'; +import React, { useEffect, useMemo, useState } from 'react'; +import styled, { css } from 'styled-components'; + +import { Markdown } from '../markdown'; +import * as i18n from './translations'; +import { MARKDOWN_HELP_LINK } from './constants'; + +const TextArea = styled(EuiTextArea)` + width: 100%; +`; + +const Container = styled(EuiPanel)` + ${({ theme }) => css` + padding: 0; + background: ${theme.eui.euiColorLightestShade}; + position: relative; + .euiTab { + padding: 10px; + } + .euiFormRow__labelWrapper { + position: absolute; + top: -${theme.eui.euiSizeL}; + } + .euiFormErrorText { + padding: 0 ${theme.eui.euiSizeM}; + } + `} +`; + +const Tabs = styled(EuiTabbedContent)` + width: 100%; +`; + +const Footer = styled(EuiFlexGroup)` + ${({ theme }) => css` + height: 41px; + padding: 0 ${theme.eui.euiSizeM}; + .euiLink { + font-size: ${theme.eui.euiSizeM}; + } + `} +`; + +const MarkdownContainer = styled(EuiPanel)` + min-height: 150px; + overflow: auto; +`; + +/** An input for entering a new case description */ +export const MarkdownEditor = React.memo<{ + placeholder?: string; + footerContentRight?: React.ReactNode; + initialContent: string; + isDisabled?: boolean; + onChange: (description: string) => void; +}>(({ placeholder, footerContentRight, initialContent, isDisabled = false, onChange }) => { + const [content, setContent] = useState(initialContent); + useEffect(() => { + onChange(content); + }, [content]); + const tabs = useMemo( + () => [ + { + id: 'comment', + name: i18n.MARKDOWN, + content: ( + <TextArea + onChange={e => { + setContent(e.target.value); + }} + aria-label={`markdown-editor-comment`} + fullWidth={true} + disabled={isDisabled} + placeholder={placeholder ?? ''} + spellCheck={false} + value={content} + /> + ), + }, + { + id: 'preview', + name: i18n.PREVIEW, + content: ( + <MarkdownContainer data-test-subj="markdown-container" paddingSize="s"> + <Markdown raw={content} /> + </MarkdownContainer> + ), + }, + ], + [content, isDisabled, placeholder] + ); + return ( + <Container> + <Tabs data-test-subj={`markdown-tabs`} size="s" tabs={tabs} initialSelectedTab={tabs[0]} /> + <Footer alignItems="center" gutterSize="none" justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiLink href={MARKDOWN_HELP_LINK} external target="_blank"> + {i18n.MARKDOWN_SYNTAX_HELP} + </EuiLink> + </EuiFlexItem> + {footerContentRight && <EuiFlexItem grow={false}>{footerContentRight}</EuiFlexItem>} + </Footer> + </Container> + ); +}); + +MarkdownEditor.displayName = 'MarkdownEditor'; diff --git a/x-pack/legacy/plugins/siem/public/components/markdown_editor/translations.ts b/x-pack/legacy/plugins/siem/public/components/markdown_editor/translations.ts new file mode 100644 index 0000000000000..642c524c48be0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/markdown_editor/translations.ts @@ -0,0 +1,18 @@ +/* + * 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 MARKDOWN_SYNTAX_HELP = i18n.translate('xpack.siem.markdownEditor.markdownInputHelp', { + defaultMessage: 'Markdown syntax help', +}); + +export const MARKDOWN = i18n.translate('xpack.siem.markdownEditor.markdown', { + defaultMessage: 'Markdown', +}); +export const PREVIEW = i18n.translate('xpack.siem.markdownEditor.preview', { + defaultMessage: 'Preview', +}); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx index e1b3951a2317d..a821d310344d8 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx @@ -70,7 +70,7 @@ describe('SIEM Navigation', () => { disabled: false, href: '#/link-to/case', id: 'case', - name: 'Case', + name: 'Cases', urlKey: 'case', }, detections: { @@ -163,7 +163,7 @@ describe('SIEM Navigation', () => { disabled: false, href: '#/link-to/case', id: 'case', - name: 'Case', + name: 'Cases', urlKey: 'case', }, detections: { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index bff3bfd62a85c..f1d87ca58b44b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -5,12 +5,22 @@ */ import { KibanaServices } from '../../lib/kibana'; -import { FetchCasesProps, Case, NewCase, SortFieldCase, AllCases, CaseSnake } from './types'; +import { + AllCases, + Case, + CaseSnake, + Comment, + CommentSnake, + FetchCasesProps, + NewCase, + NewComment, + SortFieldCase, +} from './types'; import { throwIfNotOk } from '../../hooks/api/api'; import { CASES_URL } from './constants'; import { convertToCamelCase, convertAllCasesToCamel } from './utils'; -export const getCase = async (caseId: string, includeComments: boolean): Promise<Case> => { +export const getCase = async (caseId: string, includeComments: boolean = true): Promise<Case> => { const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, { method: 'GET', asResponse: true, @@ -72,3 +82,27 @@ export const updateCaseProperty = async ( await throwIfNotOk(response.response); return convertToCamelCase<Partial<CaseSnake>, Partial<Case>>(response.body!); }; + +export const createComment = async (newComment: NewComment, caseId: string): Promise<Comment> => { + const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}/comment`, { + method: 'POST', + asResponse: true, + body: JSON.stringify(newComment), + }); + await throwIfNotOk(response.response); + return convertToCamelCase<CommentSnake, Comment>(response.body!); +}; + +export const updateComment = async ( + commentId: string, + commentUpdate: string, + version: string +): Promise<Partial<Comment>> => { + const response = await KibanaServices.get().http.fetch(`${CASES_URL}/comment/${commentId}`, { + method: 'PATCH', + asResponse: true, + body: JSON.stringify({ comment: commentUpdate, version }), + }); + await throwIfNotOk(response.response); + return convertToCamelCase<Partial<CommentSnake>, Partial<Comment>>(response.body!); +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts index c8d668527ae32..031ba1c128a24 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts @@ -11,6 +11,6 @@ export const FETCH_FAILURE = 'FETCH_FAILURE'; export const FETCH_INIT = 'FETCH_INIT'; export const FETCH_SUCCESS = 'FETCH_SUCCESS'; export const POST_NEW_CASE = 'POST_NEW_CASE'; -export const UPDATE_CASE_PROPERTY = 'UPDATE_CASE_PROPERTY'; +export const POST_NEW_COMMENT = 'POST_NEW_COMMENT'; export const UPDATE_FILTER_OPTIONS = 'UPDATE_FILTER_OPTIONS'; export const UPDATE_QUERY_PARAMS = 'UPDATE_QUERY_PARAMS'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index 1aea0b0f50a89..75ed6f7c2366d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -14,8 +14,31 @@ export interface NewCase extends FormData { title: string; } +export interface NewComment extends FormData { + comment: string; +} + +export interface CommentSnake { + comment_id: string; + created_at: string; + created_by: ElasticUserSnake; + comment: string; + updated_at: string; + version: string; +} + +export interface Comment { + commentId: string; + createdAt: string; + createdBy: ElasticUser; + comment: string; + updatedAt: string; + version: string; +} + export interface CaseSnake { case_id: string; + comments: CommentSnake[]; created_at: string; created_by: ElasticUserSnake; description: string; @@ -23,11 +46,12 @@ export interface CaseSnake { tags: string[]; title: string; updated_at: string; - version?: string; + version: string; } export interface Case { caseId: string; + comments: Comment[]; createdAt: string; createdBy: ElasticUser; description: string; @@ -35,7 +59,7 @@ export interface Case { tags: string[]; title: string; updatedAt: string; - version?: string; + version: string; } export interface QueryParams { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx index bf76b69ef22d6..ce71c26078db9 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -52,6 +52,7 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { const initialData: Case = { caseId: '', createdAt: '', + comments: [], createdBy: { username: '', }, @@ -60,6 +61,7 @@ const initialData: Case = { tags: [], title: '', updatedAt: '', + version: '', }; export const useGetCase = (caseId: string): [CaseState] => { @@ -75,7 +77,7 @@ export const useGetCase = (caseId: string): [CaseState] => { const fetchData = async () => { dispatch({ type: FETCH_INIT }); try { - const response = await getCase(caseId, false); + const response = await getCase(caseId); if (!didCancel) { dispatch({ type: FETCH_SUCCESS, payload: response }); } diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx index 5cf99701977d2..0fcc8a3a1abec 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx @@ -80,8 +80,7 @@ export const usePostCase = (): [NewCaseState, Dispatch<SetStateAction<NewCase>>] const postCase = async () => { dispatch({ type: FETCH_INIT }); try { - const dataWithoutIsNew = state.data; - delete dataWithoutIsNew.isNew; + const { isNew, ...dataWithoutIsNew } = state.data; const response = await createCase(dataWithoutIsNew); dispatch({ type: FETCH_SUCCESS, payload: response }); } catch (error) { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx new file mode 100644 index 0000000000000..d8abda25af286 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx @@ -0,0 +1,97 @@ +/* + * 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 { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; +import { useStateToaster } from '../../components/toasters'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, POST_NEW_COMMENT } from './constants'; +import { Comment, NewComment } from './types'; +import { createComment } from './api'; +import { getTypedPayload } from './utils'; + +interface NewCommentState { + data: NewComment; + newComment?: Comment; + isLoading: boolean; + isError: boolean; + caseId: string; +} +interface Action { + type: string; + payload?: NewComment | Comment; +} + +const dataFetchReducer = (state: NewCommentState, action: Action): NewCommentState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case POST_NEW_COMMENT: + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload<NewComment>(action.payload), + }; + case FETCH_SUCCESS: + return { + ...state, + isLoading: false, + isError: false, + newComment: getTypedPayload<Comment>(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; +const initialData: NewComment = { + comment: '', +}; + +export const usePostComment = ( + caseId: string +): [NewCommentState, Dispatch<SetStateAction<NewComment>>] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + caseId, + data: initialData, + }); + const [formData, setFormData] = useState(initialData); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + dispatch({ type: POST_NEW_COMMENT, payload: formData }); + }, [formData]); + + useEffect(() => { + const postComment = async () => { + dispatch({ type: FETCH_INIT }); + try { + const { isNew, ...dataWithoutIsNew } = state.data; + const response = await createComment(dataWithoutIsNew, state.caseId); + dispatch({ type: FETCH_SUCCESS, payload: response }); + } catch (error) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + }; + if (state.data.isNew) { + postComment(); + } + }, [state.data.isNew]); + return [state, setFormData]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index 62e3d87b528c0..ebbb1e14dc237 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useReducer } from 'react'; +import { useReducer } from 'react'; import { useStateToaster } from '../../components/toasters'; import { errorToToaster } from '../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, UPDATE_CASE_PROPERTY } from './constants'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import { Case } from './types'; import { updateCaseProperty } from './api'; import { getTypedPayload } from './utils'; @@ -19,7 +19,7 @@ interface NewCaseState { data: Case; isLoading: boolean; isError: boolean; - updateKey?: UpdateKey | null; + updateKey: UpdateKey | null; } interface UpdateByKey { @@ -29,7 +29,7 @@ interface UpdateByKey { interface Action { type: string; - payload?: Partial<Case> | UpdateByKey; + payload?: Partial<Case> | UpdateKey; } const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { @@ -39,20 +39,9 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => ...state, isLoading: true, isError: false, - updateKey: null, - }; - case UPDATE_CASE_PROPERTY: - const { updateKey, updateValue } = getTypedPayload<UpdateByKey>(action.payload); - return { - ...state, - isLoading: false, - isError: false, - data: { - ...state.data, - [updateKey]: updateValue, - }, - updateKey, + updateKey: getTypedPayload<UpdateKey>(action.payload), }; + case FETCH_SUCCESS: return { ...state, @@ -62,12 +51,14 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => ...state.data, ...getTypedPayload<Case>(action.payload), }, + updateKey: null, }; case FETCH_FAILURE: return { ...state, isLoading: false, isError: true, + updateKey: null, }; default: throw new Error(); @@ -77,40 +68,29 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => export const useUpdateCase = ( caseId: string, initialData: Case -): [{ data: Case }, (updates: UpdateByKey) => void] => { +): [NewCaseState, (updates: UpdateByKey) => void] => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, + updateKey: null, }); const [, dispatchToaster] = useStateToaster(); - const dispatchUpdateCaseProperty = ({ updateKey, updateValue }: UpdateByKey) => { - dispatch({ - type: UPDATE_CASE_PROPERTY, - payload: { updateKey, updateValue }, - }); - }; - - useEffect(() => { - const updateData = async (updateKey: keyof Case) => { - dispatch({ type: FETCH_INIT }); - try { - const response = await updateCaseProperty( - caseId, - { [updateKey]: state.data[updateKey] }, - state.data.version ?? '' // saved object versions are typed as string | undefined, hope that's not true - ); - dispatch({ type: FETCH_SUCCESS, payload: response }); - } catch (error) { - errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); - dispatch({ type: FETCH_FAILURE }); - } - }; - if (state.updateKey) { - updateData(state.updateKey); + const dispatchUpdateCaseProperty = async ({ updateKey, updateValue }: UpdateByKey) => { + dispatch({ type: FETCH_INIT, payload: updateKey }); + try { + const response = await updateCaseProperty( + caseId, + { [updateKey]: updateValue }, + state.data.version ?? '' // saved object versions are typed as string | undefined, hope that's not true + ); + dispatch({ type: FETCH_SUCCESS, payload: response }); + } catch (error) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); } - }, [state.updateKey]); + }; - return [{ data: state.data }, dispatchUpdateCaseProperty]; + return [state, dispatchUpdateCaseProperty]; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx new file mode 100644 index 0000000000000..bc8369117433a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx @@ -0,0 +1,92 @@ +/* + * 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 { useReducer, useRef } from 'react'; +import { useStateToaster } from '../../components/toasters'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; +import { Comment } from './types'; +import { updateComment } from './api'; +import { getTypedPayload } from './utils'; + +interface CommetUpdateState { + data: Comment[]; + isLoadingIds: string[]; + isError: boolean; +} + +interface CommentUpdate { + update: Partial<Comment>; + commentId: string; +} + +interface Action { + type: string; + payload?: CommentUpdate | string; +} + +const dataFetchReducer = (state: CommetUpdateState, action: Action): CommetUpdateState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoadingIds: [...state.isLoadingIds, getTypedPayload<string>(action.payload)], + isError: false, + }; + + case FETCH_SUCCESS: + const updatePayload = getTypedPayload<CommentUpdate>(action.payload); + const foundIndex = state.data.findIndex( + comment => comment.commentId === updatePayload.commentId + ); + state.data[foundIndex] = { ...state.data[foundIndex], ...updatePayload.update }; + return { + ...state, + isLoadingIds: state.isLoadingIds.filter(id => updatePayload.commentId !== id), + isError: false, + data: [...state.data], + }; + case FETCH_FAILURE: + return { + ...state, + isLoadingIds: state.isLoadingIds.filter( + id => getTypedPayload<string>(action.payload) !== id + ), + isError: true, + }; + default: + throw new Error(); + } +}; + +export const useUpdateComment = ( + comments: Comment[] +): [CommetUpdateState, (commentId: string, commentUpdate: string) => void] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoadingIds: [], + isError: false, + data: comments, + }); + const dispatchUpdateComment = useRef<(commentId: string, commentUpdate: string) => void>(); + const [, dispatchToaster] = useStateToaster(); + + dispatchUpdateComment.current = async (commentId: string, commentUpdate: string) => { + dispatch({ type: FETCH_INIT, payload: commentId }); + try { + const currentComment = state.data.find(comment => comment.commentId === commentId) ?? { + version: '', + }; + const response = await updateComment(commentId, commentUpdate, currentComment.version); + dispatch({ type: FETCH_SUCCESS, payload: { update: response, commentId } }); + } catch (error) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE, payload: commentId }); + } + }; + + return [state, dispatchUpdateComment.current]; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx new file mode 100644 index 0000000000000..c8e0dafcf5742 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx @@ -0,0 +1,73 @@ +/* + * 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, { useCallback } from 'react'; +import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; +import styled from 'styled-components'; +import { Form, useForm, UseField } from '../../../../shared_imports'; +import { NewComment } from '../../../../containers/case/types'; +import { usePostComment } from '../../../../containers/case/use_post_comment'; +import { schema } from './schema'; +import * as i18n from '../../translations'; +import { MarkdownEditorForm } from '../../../../components/markdown_editor/form'; + +const MySpinner = styled(EuiLoadingSpinner)` + position: absolute; + top: 50%; + left: 50%; +`; + +export const AddComment = React.memo<{ + caseId: string; +}>(({ caseId }) => { + const [{ data, isLoading, newComment }, setFormData] = usePostComment(caseId); + const { form } = useForm({ + defaultValue: data, + options: { stripEmptyFields: false }, + schema, + }); + + const onSubmit = useCallback(async () => { + const { isValid, data: newData } = await form.submit(); + if (isValid && newData.comment) { + setFormData({ ...newData, isNew: true } as NewComment); + } else if (isValid && data.comment) { + setFormData({ ...data, ...newData, isNew: true } as NewComment); + } + }, [form, data]); + + return ( + <> + {isLoading && <MySpinner size="xl" />} + <Form form={form}> + <UseField + path="comment" + component={MarkdownEditorForm} + componentProps={{ + idAria: 'caseComment', + isDisabled: isLoading, + dataTestSubj: 'caseComment', + placeholder: i18n.ADD_COMMENT_HELP_TEXT, + footerContentRight: ( + <EuiButton + iconType="plusInCircle" + isDisabled={isLoading} + isLoading={isLoading} + onClick={onSubmit} + size="s" + > + {i18n.ADD_COMMENT} + </EuiButton> + ), + }} + /> + </Form> + {newComment && + 'TO DO new comment got added but we didnt update the UI yet. Refresh the page to see your comment ;)'} + </> + ); +}); + +AddComment.displayName = 'AddComment'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/schema.tsx new file mode 100644 index 0000000000000..5f30f59149d99 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/schema.tsx @@ -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 { FIELD_TYPES, fieldValidators, FormSchema } from '../../../../shared_imports'; +import * as i18n from '../../translations'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + comment: { + type: FIELD_TYPES.TEXTAREA, + validations: [ + { + validator: emptyField(i18n.COMMENT_REQUIRED), + }, + ], + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx index 98a67304fcf1f..0169493773b74 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx @@ -14,51 +14,61 @@ export const useGetCasesMockState: UseGetCasesState = { caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:23.627Z', createdBy: { username: 'elastic' }, + comments: [], description: 'Security banana Issue', state: 'open', tags: ['defacement'], title: 'Another horrible breach', updatedAt: '2020-02-13T19:44:23.627Z', + version: 'WzQ3LDFd', }, { caseId: '362a5c10-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:13.328Z', createdBy: { username: 'elastic' }, + comments: [], description: 'Security banana Issue', state: 'open', tags: ['phishing'], title: 'Bad email', updatedAt: '2020-02-13T19:44:13.328Z', + version: 'WzQ3LDFd', }, { caseId: '34f8b9e0-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:11.328Z', createdBy: { username: 'elastic' }, + comments: [], description: 'Security banana Issue', state: 'open', tags: ['phishing'], title: 'Bad email', updatedAt: '2020-02-13T19:44:11.328Z', + version: 'WzQ3LDFd', }, { caseId: '31890e90-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:05.563Z', createdBy: { username: 'elastic' }, + comments: [], description: 'Security banana Issue', state: 'closed', tags: ['phishing'], title: 'Uh oh', updatedAt: '2020-02-18T21:32:24.056Z', + version: 'WzQ3LDFd', }, { caseId: '2f5b3210-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:01.901Z', createdBy: { username: 'elastic' }, + comments: [], description: 'Security banana Issue', state: 'open', tags: ['phishing'], title: 'Uh oh', updatedAt: '2020-02-13T19:44:01.901Z', + version: 'WzQ3LDFd', }, ], page: 1, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index 4c47bf605051d..9c276d1b24da1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -19,7 +19,7 @@ const renderStringField = (field: string, dataTestSubj: string) => export const getCasesColumns = (): CasesColumns[] => [ { - name: i18n.CASE_TITLE, + name: i18n.NAME, render: (theCase: Case) => { if (theCase.caseId != null && theCase.title != null) { return <CaseDetailsLink detailName={theCase.caseId}>{theCase.title}</CaseDetailsLink>; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx index 7480c4fc4bb2a..89d321c6d106a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx @@ -11,6 +11,19 @@ export const caseProps: CaseProps = { caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', initialData: { caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + comments: [ + { + comment: 'Solve this fast!', + commentId: 'a357c6a0-5435-11ea-b427-fb51a1fcb7b8', + createdAt: '2020-02-20T23:06:33.798Z', + createdBy: { + fullName: 'Steph Milovic', + username: 'smilovic', + }, + updatedAt: '2020-02-20T23:06:33.798Z', + version: 'WzQ3LDFd', + }, + ], createdAt: '2020-02-13T19:44:23.627Z', createdBy: { fullName: null, username: 'elastic' }, description: 'Security banana Issue', @@ -18,12 +31,25 @@ export const caseProps: CaseProps = { tags: ['defacement'], title: 'Another horrible breach!!', updatedAt: '2020-02-19T15:02:57.995Z', + version: 'WzQ3LDFd', }, - isLoading: false, }; export const data: Case = { caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + comments: [ + { + comment: 'Solve this fast!', + commentId: 'a357c6a0-5435-11ea-b427-fb51a1fcb7b8', + createdAt: '2020-02-20T23:06:33.798Z', + createdBy: { + fullName: 'Steph Milovic', + username: 'smilovic', + }, + updatedAt: '2020-02-20T23:06:33.798Z', + version: 'WzQ3LDFd', + }, + ], createdAt: '2020-02-13T19:44:23.627Z', createdBy: { username: 'elastic', fullName: null }, description: 'Security banana Issue', @@ -31,4 +57,5 @@ export const data: Case = { tags: ['defacement'], title: 'Another horrible breach!!', updatedAt: '2020-02-19T15:02:57.995Z', + version: 'WzQ3LDFd', }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx index a9e694bad705d..1539b3de5a0c1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx @@ -16,7 +16,12 @@ describe('CaseView ', () => { beforeEach(() => { jest.resetAllMocks(); - jest.spyOn(apiHook, 'useUpdateCase').mockReturnValue([{ data }, dispatchUpdateCaseProperty]); + jest + .spyOn(apiHook, 'useUpdateCase') + .mockReturnValue([ + { data, isLoading: false, isError: false, updateKey: null }, + dispatchUpdateCaseProperty, + ]); }); it('should render CaseComponent', () => { @@ -79,4 +84,38 @@ describe('CaseView ', () => { updateValue: 'closed', }); }); + + it('should render comments', () => { + const wrapper = mount( + <TestProviders> + <CaseComponent {...caseProps} /> + </TestProviders> + ); + expect( + wrapper + .find( + `div[data-test-subj="user-action-${data.comments[0].commentId}-avatar"] [data-test-subj="user-action-avatar"]` + ) + .first() + .prop('name') + ).toEqual(data.comments[0].createdBy.fullName); + + expect( + wrapper + .find( + `div[data-test-subj="user-action-${data.comments[0].commentId}"] [data-test-subj="user-action-title"] strong` + ) + .first() + .text() + ).toEqual(data.comments[0].createdBy.username); + + expect( + wrapper + .find( + `div[data-test-subj="user-action-${data.comments[0].commentId}"] [data-test-subj="markdown"]` + ) + .first() + .prop('source') + ).toEqual(data.comments[0].comment); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index df3e30a698b56..605f9e8fa1713 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback } from 'react'; import { EuiBadge, - EuiButton, - EuiButtonEmpty, EuiButtonToggle, EuiDescriptionList, EuiDescriptionListDescription, @@ -20,13 +18,11 @@ import { import styled, { css } from 'styled-components'; import * as i18n from './translations'; -import { DescriptionMarkdown } from '../description_md_editor'; import { Case } from '../../../../containers/case/types'; import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; import { getCaseUrl } from '../../../../components/link_to'; import { HeaderPage } from '../../../../components/header_page'; import { EditableTitle } from '../../../../components/header_page/editable_title'; -import { Markdown } from '../../../../components/markdown'; import { PropertyActions } from '../property_actions'; import { TagList } from '../tag_list'; import { useGetCase } from '../../../../containers/case/use_get_case'; @@ -34,6 +30,7 @@ import { UserActionTree } from '../user_action_tree'; import { UserList } from '../user_list'; import { useUpdateCase } from '../../../../containers/case/use_update_case'; import { WrapperPage } from '../../../../components/wrapper_page'; +import { getTypedPayload } from '../../../../containers/case/utils'; import { WhitePageWrapper } from '../wrappers'; interface Props { @@ -53,95 +50,71 @@ const MyWrapper = styled(WrapperPage)` padding-bottom: 0; `; +const MyEuiFlexGroup = styled(EuiFlexGroup)` + height: 100%; +`; + export interface CaseProps { caseId: string; initialData: Case; - isLoading: boolean; } -export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData, isLoading }) => { - const [{ data }, dispatchUpdateCaseProperty] = useUpdateCase(caseId, initialData); - const [isEditDescription, setIsEditDescription] = useState(false); - const [isEditTags, setIsEditTags] = useState(false); - const [isCaseOpen, setIsCaseOpen] = useState(data.state === 'open'); - const [description, setDescription] = useState(data.description); - const [title, setTitle] = useState(data.title); - const [tags, setTags] = useState(data.tags); +export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData }) => { + const [{ data, isLoading, updateKey }, dispatchUpdateCaseProperty] = useUpdateCase( + caseId, + initialData + ); const onUpdateField = useCallback( - async (updateKey: keyof Case, updateValue: string | string[]) => { - switch (updateKey) { + (newUpdateKey: keyof Case, updateValue: Case[keyof Case]) => { + switch (newUpdateKey) { case 'title': - if (updateValue.length > 0) { + const titleUpdate = getTypedPayload<string>(updateValue); + if (titleUpdate.length > 0) { dispatchUpdateCaseProperty({ updateKey: 'title', - updateValue, + updateValue: titleUpdate, }); } break; case 'description': - if (updateValue.length > 0) { + const descriptionUpdate = getTypedPayload<string>(updateValue); + if (descriptionUpdate.length > 0) { dispatchUpdateCaseProperty({ updateKey: 'description', - updateValue, + updateValue: descriptionUpdate, }); - setIsEditDescription(false); } break; case 'tags': - setTags(updateValue as string[]); - if (updateValue.length > 0) { + const tagsUpdate = getTypedPayload<string[]>(updateValue); + dispatchUpdateCaseProperty({ + updateKey: 'tags', + updateValue: tagsUpdate, + }); + break; + case 'state': + const stateUpdate = getTypedPayload<string>(updateValue); + if (data.state !== updateValue) { dispatchUpdateCaseProperty({ - updateKey: 'tags', - updateValue, + updateKey: 'state', + updateValue: stateUpdate, }); - setIsEditTags(false); } - break; default: return null; } }, - [dispatchUpdateCaseProperty, title] + [dispatchUpdateCaseProperty, data.state] ); - const onSetIsCaseOpen = useCallback(() => setIsCaseOpen(!isCaseOpen), [ - isCaseOpen, - setIsCaseOpen, - ]); - - useEffect(() => { - const caseState = isCaseOpen ? 'open' : 'closed'; - if (data.state !== caseState) { - dispatchUpdateCaseProperty({ - updateKey: 'state', - updateValue: caseState, - }); - } - }, [isCaseOpen]); - // TO DO refactor each of these const's into their own components const propertyActions = [ - { - iconType: 'documentEdit', - label: 'Edit description', - onClick: () => setIsEditDescription(true), - }, - { - iconType: 'securitySignalResolved', - label: 'Close case', - onClick: () => null, - }, { iconType: 'trash', label: 'Delete case', onClick: () => null, }, - { - iconType: 'importAction', - label: 'Push as ServiceNow incident', - onClick: () => null, - }, { iconType: 'popout', label: 'View ServiceNow incident', @@ -153,66 +126,13 @@ export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData, isLoa onClick: () => null, }, ]; - const userActions = [ - { - avatarName: data.createdBy.username, - title: ( - <EuiFlexGroup alignItems="baseline" gutterSize="none" justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> - <p> - <strong>{`${data.createdBy.username}`}</strong> - {` ${i18n.ADDED_DESCRIPTION} `}{' '} - <FormattedRelativePreferenceDate value={data.createdAt} /> - {/* STEPH FIX come back and add label `on` */} - </p> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <PropertyActions propertyActions={propertyActions} /> - </EuiFlexItem> - </EuiFlexGroup> - ), - children: isEditDescription ? ( - <> - <DescriptionMarkdown - descriptionInputHeight={200} - initialDescription={data.description} - isLoading={isLoading} - onChange={updatedDescription => setDescription(updatedDescription)} - /> - <EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}> - <EuiFlexItem grow={false}> - <EuiButton - fill - isDisabled={isLoading} - isLoading={isLoading} - onClick={() => onUpdateField('description', description)} - > - {i18n.SUBMIT} - </EuiButton> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButtonEmpty onClick={() => setIsEditDescription(false)}> - {i18n.CANCEL} - </EuiButtonEmpty> - </EuiFlexItem> - </EuiFlexGroup> - </> - ) : ( - <Markdown raw={data.description} data-test-subj="case-view-description" /> - ), - }, - ]; - - const onSubmit = useCallback( - newTitle => { - onUpdateField('title', newTitle); - setTitle(newTitle); - }, - [title] + const onSubmit = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]); + const toggleStateCase = useCallback( + e => onUpdateField('state', e.target.checked ? 'open' : 'closed'), + [onUpdateField] ); - - const titleNode = <EditableTitle isLoading={isLoading} title={title} onSubmit={onSubmit} />; + const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]); return ( <> @@ -223,8 +143,14 @@ export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData, isLoa text: i18n.BACK_TO_ALL, }} data-test-subj="case-view-title" - titleNode={titleNode} - title={title} + titleNode={ + <EditableTitle + isLoading={isLoading && updateKey === 'title'} + title={data.title} + onSubmit={onSubmit} + /> + } + title={data.title} > <EuiFlexGroup gutterSize="l" justifyContent="flexEnd"> <EuiFlexItem grow={false}> @@ -234,7 +160,7 @@ export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData, isLoa <EuiDescriptionListTitle>{i18n.STATUS}</EuiDescriptionListTitle> <EuiDescriptionListDescription> <EuiBadge - color={isCaseOpen ? 'secondary' : 'danger'} + color={data.state === 'open' ? 'secondary' : 'danger'} data-test-subj="case-view-state" > {data.state} @@ -258,10 +184,11 @@ export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData, isLoa <EuiFlexItem> <EuiButtonToggle data-test-subj="toggle-case-state" - label={isCaseOpen ? 'Close case' : 'Reopen case'} - iconType={isCaseOpen ? 'checkInCircleFilled' : 'magnet'} - onChange={onSetIsCaseOpen} - isSelected={isCaseOpen} + iconType={data.state === 'open' ? 'checkInCircleFilled' : 'magnet'} + isLoading={isLoading && updateKey === 'state'} + isSelected={data.state === 'open'} + label={data.state === 'open' ? 'Close case' : 'Reopen case'} + onChange={toggleStateCase} /> </EuiFlexItem> <EuiFlexItem grow={false}> @@ -276,7 +203,11 @@ export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData, isLoa <MyWrapper> <EuiFlexGroup> <EuiFlexItem grow={6}> - <UserActionTree userActions={userActions} /> + <UserActionTree + data={data} + isLoadingDescription={isLoading && updateKey === 'description'} + onUpdateField={onUpdateField} + /> </EuiFlexItem> <EuiFlexItem grow={2}> <UserList @@ -286,14 +217,9 @@ export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData, isLoa /> <TagList data-test-subj="case-view-tag-list" - tags={tags} - iconAction={{ - 'aria-label': title, - iconType: 'pencil', - onSubmit: newTags => onUpdateField('tags', newTags), - onClick: isEdit => setIsEditTags(isEdit), - }} - isEditTags={isEditTags} + tags={data.tags} + onSubmit={onSubmitTags} + isLoading={isLoading && updateKey === 'tags'} /> </EuiFlexItem> </EuiFlexGroup> @@ -310,15 +236,15 @@ export const CaseView = React.memo(({ caseId }: Props) => { } if (isLoading) { return ( - <EuiFlexGroup justifyContent="center" alignItems="center"> + <MyEuiFlexGroup justifyContent="center" alignItems="center"> <EuiFlexItem grow={false}> <EuiLoadingSpinner size="xl" /> </EuiFlexItem> - </EuiFlexGroup> + </MyEuiFlexGroup> ); } - return <CaseComponent caseId={caseId} initialData={data} isLoading={isLoading} />; + return <CaseComponent caseId={caseId} initialData={data} />; }); CaseView.displayName = 'CaseView'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts index f45c52533d2e7..82b5e771e2151 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts @@ -32,6 +32,18 @@ export const EDITED_DESCRIPTION = i18n.translate( } ); +export const EDIT_DESCRIPTION = i18n.translate('xpack.siem.case.caseView.edit.description', { + defaultMessage: 'Edit description', +}); + +export const EDIT_COMMENT = i18n.translate('xpack.siem.case.caseView.edit.comment', { + defaultMessage: 'Edit comment', +}); + +export const ON = i18n.translate('xpack.siem.case.caseView.actionLabel.on', { + defaultMessage: 'on', +}); + export const ADDED_COMMENT = i18n.translate('xpack.siem.case.caseView.actionLabel.addComment', { defaultMessage: 'added comment', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx index 7d79e287b22e7..65d7256fd6e20 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -3,38 +3,48 @@ * 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, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import { EuiButton, + EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, - EuiHorizontalRule, EuiLoadingSpinner, EuiPanel, } from '@elastic/eui'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { Redirect } from 'react-router-dom'; -import { Field, Form, getUseField, useForm } from '../../../shared_imports'; +import { Field, Form, getUseField, useForm, UseField } from '../../../../shared_imports'; import { NewCase } from '../../../../containers/case/types'; import { usePostCase } from '../../../../containers/case/use_post_case'; import { schema } from './schema'; import * as i18n from '../../translations'; import { SiemPageName } from '../../../home/types'; -import { DescriptionMarkdown } from '../description_md_editor'; +import { MarkdownEditorForm } from '../../../../components/markdown_editor/form'; export const CommonUseField = getUseField({ component: Field }); -const TagContainer = styled.div` - margin-top: 16px; +const ContainerBig = styled.div` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSizeXL}; + `} +`; + +const Container = styled.div` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSize}; + `} `; const MySpinner = styled(EuiLoadingSpinner)` position: absolute; top: 50%; left: 50%; + z-index: 99; `; export const Create = React.memo(() => { const [{ data, isLoading, newCase }, setFormData] = usePostCase(); + const [isCancel, setIsCancel] = useState(false); const { form } = useForm({ defaultValue: data, options: { stripEmptyFields: false }, @@ -43,14 +53,19 @@ export const Create = React.memo(() => { const onSubmit = useCallback(async () => { const { isValid, data: newData } = await form.submit(); - if (isValid) { + if (isValid && newData.description) { setFormData({ ...newData, isNew: true } as NewCase); + } else if (isValid && data.description) { + setFormData({ ...data, ...newData, isNew: true } as NewCase); } - }, [form]); + }, [form, data]); if (newCase && newCase.caseId) { return <Redirect to={`/${SiemPageName.case}/${newCase.caseId}`} />; } + if (isCancel) { + return <Redirect to={`/${SiemPageName.case}`} />; + } return ( <EuiPanel> {isLoading && <MySpinner size="xl" />} @@ -62,18 +77,11 @@ export const Create = React.memo(() => { 'data-test-subj': 'caseTitle', euiFieldProps: { fullWidth: false, + disabled: isLoading, }, - isDisabled: isLoading, }} /> - <DescriptionMarkdown - descriptionInputHeight={200} - formHook={true} - initialDescription={data.description} - isLoading={isLoading} - onChange={description => setFormData({ ...data, description })} - /> - <TagContainer> + <Container> <CommonUseField path="tags" componentProps={{ @@ -82,14 +90,24 @@ export const Create = React.memo(() => { euiFieldProps: { fullWidth: true, placeholder: '', + isDisabled: isLoading, }, + }} + /> + </Container> + <ContainerBig> + <UseField + path="description" + component={MarkdownEditorForm} + componentProps={{ + idAria: 'caseDescription', + dataTestSubj: 'caseDescription', isDisabled: isLoading, }} /> - </TagContainer> + </ContainerBig> </Form> - <> - <EuiHorizontalRule margin="m" /> + <Container> <EuiFlexGroup alignItems="center" justifyContent="flexEnd" @@ -97,12 +115,23 @@ export const Create = React.memo(() => { responsive={false} > <EuiFlexItem grow={false}> - <EuiButton fill isDisabled={isLoading} isLoading={isLoading} onClick={onSubmit}> - {i18n.SUBMIT} + <EuiButtonEmpty size="s" onClick={() => setIsCancel(true)} iconType="cross"> + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + fill + iconType="plusInCircle" + isDisabled={isLoading} + isLoading={isLoading} + onClick={onSubmit} + > + {i18n.CREATE_CASE} </EuiButton> </EuiFlexItem> </EuiFlexGroup> - </> + </Container> </EuiPanel> ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx index 1b5df72a6671c..c81a31f0d4f3f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../shared_imports'; +import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../../shared_imports'; import { OptionalFieldLabel } from './optional_field_label'; import * as i18n from '../../translations'; @@ -13,7 +13,7 @@ const { emptyField } = fieldValidators; export const schema: FormSchema = { title: { type: FIELD_TYPES.TEXT, - label: i18n.CASE_TITLE, + label: i18n.NAME, validations: [ { validator: emptyField(i18n.TITLE_REQUIRED), @@ -21,7 +21,7 @@ export const schema: FormSchema = { ], }, description: { - type: FIELD_TYPES.TEXTAREA, + label: i18n.DESCRIPTION, validations: [ { validator: emptyField(i18n.DESCRIPTION_REQUIRED), diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx deleted file mode 100644 index 44062a5a1d589..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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 { EuiFlexItem, EuiPanel, EuiTabbedContent, EuiTextArea } from '@elastic/eui'; -import React, { useState } from 'react'; -import styled from 'styled-components'; - -import { Markdown } from '../../../../components/markdown'; -import * as i18n from '../../translations'; -import { MarkdownHint } from '../../../../components/markdown/markdown_hint'; -import { CommonUseField } from '../create'; - -const TextArea = styled(EuiTextArea)<{ height: number }>` - min-height: ${({ height }) => `${height}px`}; - width: 100%; -`; - -TextArea.displayName = 'TextArea'; - -const DescriptionContainer = styled.div` - margin-top: 15px; - margin-bottom: 15px; -`; - -const DescriptionMarkdownTabs = styled(EuiTabbedContent)` - width: 100%; -`; - -DescriptionMarkdownTabs.displayName = 'DescriptionMarkdownTabs'; - -const MarkdownContainer = styled(EuiPanel)<{ height: number }>` - height: ${({ height }) => height}px; - overflow: auto; -`; - -MarkdownContainer.displayName = 'MarkdownContainer'; - -/** An input for entering a new case description */ -export const DescriptionMarkdown = React.memo<{ - descriptionInputHeight: number; - initialDescription: string; - isLoading: boolean; - formHook?: boolean; - onChange: (description: string) => void; -}>(({ initialDescription, isLoading, descriptionInputHeight, onChange, formHook = false }) => { - const [description, setDescription] = useState(initialDescription); - const tabs = [ - { - id: 'description', - name: i18n.DESCRIPTION, - content: formHook ? ( - <CommonUseField - path="description" - onChange={e => { - setDescription(e as string); - onChange(e as string); - }} - componentProps={{ - idAria: 'caseDescription', - 'data-test-subj': 'caseDescription', - isDisabled: isLoading, - spellcheck: false, - }} - /> - ) : ( - <TextArea - onChange={e => { - setDescription(e.target.value); - onChange(e.target.value); - }} - fullWidth={true} - height={descriptionInputHeight} - aria-label={i18n.DESCRIPTION} - disabled={isLoading} - spellCheck={false} - value={description} - /> - ), - }, - { - id: 'preview', - name: i18n.PREVIEW, - content: ( - <MarkdownContainer - data-test-subj="markdown-container" - height={descriptionInputHeight} - paddingSize="s" - > - <Markdown raw={description} /> - </MarkdownContainer> - ), - }, - ]; - return ( - <DescriptionContainer> - <DescriptionMarkdownTabs - data-test-subj="new-description-tabs" - tabs={tabs} - initialSelectedTab={tabs[0]} - /> - <EuiFlexItem grow={true}> - <MarkdownHint show={description.trim().length > 0} /> - </EuiFlexItem> - </DescriptionContainer> - ); -}); - -DescriptionMarkdown.displayName = 'DescriptionMarkdown'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx index 6634672cb6a77..3513d4de12aa1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import { EuiText, EuiHorizontalRule, @@ -14,24 +14,18 @@ import { EuiButton, EuiButtonEmpty, EuiButtonIcon, + EuiLoadingSpinner, } from '@elastic/eui'; import styled, { css } from 'styled-components'; import * as i18n from '../../translations'; -import { Form, useForm } from '../../../shared_imports'; +import { Form, useForm } from '../../../../shared_imports'; import { schema } from './schema'; import { CommonUseField } from '../create'; -interface IconAction { - 'aria-label': string; - iconType: string; - onClick: (b: boolean) => void; - onSubmit: (a: string[]) => void; -} - interface TagListProps { + isLoading: boolean; + onSubmit: (a: string[]) => void; tags: string[]; - iconAction?: IconAction; - isEditTags?: boolean; } const MyFlexGroup = styled(EuiFlexGroup)` @@ -43,37 +37,35 @@ const MyFlexGroup = styled(EuiFlexGroup)` `} `; -export const TagList = React.memo(({ tags, isEditTags, iconAction }: TagListProps) => { +export const TagList = React.memo(({ isLoading, onSubmit, tags }: TagListProps) => { const { form } = useForm({ defaultValue: { tags }, options: { stripEmptyFields: false }, schema, }); + const [isEditTags, setIsEditTags] = useState(false); - const onSubmit = useCallback(async () => { + const onSubmitTags = useCallback(async () => { const { isValid, data: newData } = await form.submit(); - if (isValid && iconAction) { - iconAction.onSubmit(newData.tags); - iconAction.onClick(false); + if (isValid && newData.tags) { + onSubmit(newData.tags); + setIsEditTags(false); } - }, [form]); + }, [form, onSubmit]); - const onActionClick = useCallback( - (cb: (b: boolean) => void, onClickBool: boolean) => cb(onClickBool), - [iconAction] - ); return ( <EuiText> <EuiFlexGroup alignItems="center" gutterSize="xs" justifyContent="spaceBetween"> <EuiFlexItem grow={false}> <h4>{i18n.TAGS}</h4> </EuiFlexItem> - {iconAction && ( + {isLoading && <EuiLoadingSpinner />} + {!isLoading && ( <EuiFlexItem grow={false}> <EuiButtonIcon - aria-label={iconAction['aria-label']} - iconType={iconAction.iconType} - onClick={() => onActionClick(iconAction.onClick, true)} + aria-label={'tags'} + iconType={'pencil'} + onClick={setIsEditTags.bind(null, true)} /> </EuiFlexItem> )} @@ -88,7 +80,7 @@ export const TagList = React.memo(({ tags, isEditTags, iconAction }: TagListProp <EuiBadge color="hollow">{tag}</EuiBadge> </EuiFlexItem> ))} - {isEditTags && iconAction && ( + {isEditTags && ( <EuiFlexGroup direction="column"> <EuiFlexItem> <Form form={form}> @@ -106,14 +98,22 @@ export const TagList = React.memo(({ tags, isEditTags, iconAction }: TagListProp </Form> </EuiFlexItem> <EuiFlexItem> - <EuiButton fill onClick={onSubmit}> - {i18n.SUBMIT} - </EuiButton> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButtonEmpty onClick={() => onActionClick(iconAction.onClick, false)}> - {i18n.CANCEL} - </EuiButtonEmpty> + <EuiFlexGroup gutterSize="s" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiButton color="secondary" fill iconType="save" onClick={onSubmitTags} size="s"> + {i18n.SAVE} + </EuiButton> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + iconType="cross" + onClick={setIsEditTags.bind(null, false)} + size="s" + > + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> </EuiFlexItem> </EuiFlexGroup> )} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/schema.tsx index dfc9c61cd5f0c..26a89408069fb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/schema.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { FormSchema } from '../../../shared_imports'; +import { FormSchema } from '../../../../shared_imports'; import { schema as createSchema } from '../create/schema'; export const schema: FormSchema = { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx index 8df98a4cef0e8..6599151f9d4fd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx @@ -4,18 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { ReactNode } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiAvatar, EuiPanel, EuiText } from '@elastic/eui'; +import React, { ReactNode, useCallback, useMemo, useState } from 'react'; +import { EuiFlexGroup } from '@elastic/eui'; import styled, { css } from 'styled-components'; +import * as i18n from '../case_view/translations'; + +import { Case } from '../../../../containers/case/types'; +import { useUpdateComment } from '../../../../containers/case/use_update_comment'; +import { UserActionItem } from './user_action_item'; +import { UserActionMarkdown } from './user_action_markdown'; +import { AddComment } from '../add_comment'; export interface UserActionItem { avatarName: string; children?: ReactNode; - title: ReactNode; + skipPanel?: boolean; + title?: ReactNode; } export interface UserActionTreeProps { - userActions: UserActionItem[]; + data: Case; + isLoadingDescription: boolean; + onUpdateField: (updateKey: keyof Case, updateValue: string | string[]) => void; } const UserAction = styled(EuiFlexGroup)` @@ -48,35 +58,110 @@ const UserAction = styled(EuiFlexGroup)` border-bottom: ${theme.eui.euiBorderThin}; border-radius: ${theme.eui.euiBorderRadius} ${theme.eui.euiBorderRadius} 0 0; } - .userAction__content { - padding: ${theme.eui.euiSizeM} ${theme.eui.euiSizeL}; - } .euiText--small * { margin-bottom: 0; } `} `; -const renderUserActions = (userActions: UserActionItem[]) => { - return userActions.map(({ avatarName, children, title }, key) => ( - <UserAction key={key} gutterSize={'none'}> - <EuiFlexItem grow={false}> - <EuiAvatar className="userAction__circle" name={avatarName} /> - </EuiFlexItem> - <EuiFlexItem> - <EuiPanel className="userAction__panel" paddingSize="none"> - <EuiText size="s" className="userAction__title"> - {title} - </EuiText> - {children && <div className="userAction__content">{children}</div>} - </EuiPanel> - </EuiFlexItem> - </UserAction> - )); -}; +const DescriptionId = 'description'; +const NewId = 'newComent'; + +export const UserActionTree = React.memo( + ({ data, onUpdateField, isLoadingDescription }: UserActionTreeProps) => { + const [{ data: comments, isLoadingIds }, dispatchUpdateComment] = useUpdateComment( + data.comments + ); + + const [manageMarkdownEditIds, setManangeMardownEditIds] = useState<string[]>([]); + + const handleManageMarkdownEditId = useCallback( + (id: string) => { + if (!manageMarkdownEditIds.includes(id)) { + setManangeMardownEditIds([...manageMarkdownEditIds, id]); + } else { + setManangeMardownEditIds(manageMarkdownEditIds.filter(myId => id !== myId)); + } + }, + [manageMarkdownEditIds] + ); + + const handleSaveComment = useCallback( + (id: string, content: string) => { + handleManageMarkdownEditId(id); + dispatchUpdateComment(id, content); + }, + [handleManageMarkdownEditId, dispatchUpdateComment] + ); + + const MarkdownDescription = useMemo( + () => ( + <UserActionMarkdown + id={DescriptionId} + content={data.description} + isEditable={manageMarkdownEditIds.includes(DescriptionId)} + onSaveContent={(content: string) => { + handleManageMarkdownEditId(DescriptionId); + onUpdateField(DescriptionId, content); + }} + onChangeEditable={handleManageMarkdownEditId} + /> + ), + [data.description, handleManageMarkdownEditId, manageMarkdownEditIds, onUpdateField] + ); + + const MarkdownNewComment = useMemo(() => <AddComment caseId={data.caseId} />, [data.caseId]); -export const UserActionTree = React.memo(({ userActions }: UserActionTreeProps) => ( - <div>{renderUserActions(userActions)}</div> -)); + return ( + <UserAction data-test-subj="user-action-description" gutterSize={'none'}> + <UserActionItem + createdAt={data.createdAt} + id={DescriptionId} + isEditable={manageMarkdownEditIds.includes(DescriptionId)} + isLoading={isLoadingDescription} + labelAction={i18n.EDIT_DESCRIPTION} + labelTitle={i18n.ADDED_DESCRIPTION} + fullName={data.createdBy.fullName ?? data.createdBy.username} + markdown={MarkdownDescription} + onEdit={handleManageMarkdownEditId.bind(null, DescriptionId)} + userName={data.createdBy.username} + /> + {comments.map(comment => ( + <UserActionItem + key={comment.commentId} + createdAt={comment.createdAt} + id={comment.commentId} + isEditable={manageMarkdownEditIds.includes(comment.commentId)} + isLoading={isLoadingIds.includes(comment.commentId)} + labelAction={i18n.EDIT_COMMENT} + labelTitle={i18n.ADDED_COMMENT} + fullName={comment.createdBy.fullName ?? comment.createdBy.username} + markdown={ + <UserActionMarkdown + id={comment.commentId} + content={comment.comment} + isEditable={manageMarkdownEditIds.includes(comment.commentId)} + onChangeEditable={handleManageMarkdownEditId} + onSaveContent={handleSaveComment.bind(null, comment.commentId)} + /> + } + onEdit={handleManageMarkdownEditId.bind(null, comment.commentId)} + userName={comment.createdBy.username} + /> + ))} + <UserActionItem + createdAt={new Date().toISOString()} + id={NewId} + isEditable={true} + isLoading={isLoadingIds.includes(NewId)} + fullName="to be determined" + markdown={MarkdownNewComment} + onEdit={handleManageMarkdownEditId.bind(null, NewId)} + userName="to be determined" + /> + </UserAction> + ); + } +); UserActionTree.displayName = 'UserActionTree'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_avatar.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_avatar.tsx new file mode 100644 index 0000000000000..f3276bd50e72c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_avatar.tsx @@ -0,0 +1,18 @@ +/* + * 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 { EuiAvatar } from '@elastic/eui'; +import React from 'react'; + +interface UserActionAvatarProps { + name: string; +} + +export const UserActionAvatar = ({ name }: UserActionAvatarProps) => { + return ( + <EuiAvatar data-test-subj={`user-action-avatar`} className="userAction__circle" name={name} /> + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx new file mode 100644 index 0000000000000..816e500827590 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx @@ -0,0 +1,60 @@ +/* + * 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 { EuiFlexItem, EuiPanel } from '@elastic/eui'; +import React from 'react'; + +import { UserActionAvatar } from './user_action_avatar'; +import { UserActionTitle } from './user_action_title'; + +interface UserActionItemProps { + createdAt: string; + id: string; + isEditable: boolean; + isLoading: boolean; + labelAction?: string; + labelTitle?: string; + fullName: string; + markdown: React.ReactNode; + onEdit: (id: string) => void; + userName: string; +} + +export const UserActionItem = ({ + createdAt, + id, + isEditable, + isLoading, + labelAction, + labelTitle, + fullName, + markdown, + onEdit, + userName, +}: UserActionItemProps) => ( + <> + <EuiFlexItem data-test-subj={`user-action-${id}-avatar`} grow={false}> + <UserActionAvatar name={fullName ?? userName} /> + </EuiFlexItem> + <EuiFlexItem data-test-subj={`user-action-${id}`}> + {isEditable && markdown} + {!isEditable && ( + <EuiPanel className="userAction__panel" paddingSize="none"> + <UserActionTitle + createdAt={createdAt} + id={id} + isLoading={isLoading} + labelAction={labelAction ?? ''} + labelTitle={labelTitle ?? ''} + userName={userName} + onEdit={onEdit} + /> + {markdown} + </EuiPanel> + )} + </EuiFlexItem> + </> +); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx new file mode 100644 index 0000000000000..6a50bf24e9d7e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx @@ -0,0 +1,89 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; +import styled, { css } from 'styled-components'; + +import { MarkdownEditor } from '../../../../components/markdown_editor'; +import * as i18n from '../case_view/translations'; +import { Markdown } from '../../../../components/markdown'; + +const ContentWrapper = styled.div` + ${({ theme }) => css` + padding: ${theme.eui.euiSizeM} ${theme.eui.euiSizeL}; + `} +`; + +interface UserActionMarkdownProps { + content: string; + id: string; + isEditable: boolean; + onChangeEditable: (id: string) => void; + onSaveContent: (content: string) => void; +} + +export const UserActionMarkdown = ({ + id, + content, + isEditable, + onChangeEditable, + onSaveContent, +}: UserActionMarkdownProps) => { + const [myContent, setMyContent] = useState(content); + + const handleCancelAction = useCallback(() => { + onChangeEditable(id); + }, [id, onChangeEditable]); + + const handleSaveAction = useCallback(() => { + if (myContent !== content) { + onSaveContent(content); + } + onChangeEditable(id); + }, [content, id, myContent, onChangeEditable, onSaveContent]); + + const handleOnChange = useCallback(() => { + if (myContent !== content) { + setMyContent(content); + } + }, [content, myContent]); + + const renderButtons = useCallback( + ({ cancelAction, saveAction }) => { + return ( + <EuiFlexGroup gutterSize="s" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty size="s" onClick={cancelAction} iconType="cross"> + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton color="secondary" fill iconType="save" onClick={saveAction} size="s"> + {i18n.SAVE} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + ); + }, + [handleCancelAction, handleSaveAction] + ); + + return isEditable ? ( + <MarkdownEditor + footerContentRight={renderButtons({ + cancelAction: handleCancelAction, + saveAction: handleSaveAction, + })} + initialContent={content} + onChange={handleOnChange} + /> + ) : ( + <ContentWrapper> + <Markdown raw={content} data-test-subj="case-view-description" /> + </ContentWrapper> + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx new file mode 100644 index 0000000000000..6ad60fb9f963e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx @@ -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 { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; + +import { + FormattedRelativePreferenceDate, + FormattedRelativePreferenceLabel, +} from '../../../../components/formatted_date'; +import * as i18n from '../case_view/translations'; +import { PropertyActions } from '../property_actions'; + +const MySpinner = styled(EuiLoadingSpinner)` + .euiLoadingSpinner { + margin-top: 1px; // yes it matters! + } +`; + +interface UserActionTitleProps { + createdAt: string; + id: string; + isLoading: boolean; + labelAction: string; + labelTitle: string; + userName: string; + onEdit: (id: string) => void; +} + +export const UserActionTitle = ({ + createdAt, + id, + isLoading, + labelAction, + labelTitle, + userName, + onEdit, +}: UserActionTitleProps) => { + const propertyActions = useMemo(() => { + return [ + { + iconType: 'documentEdit', + label: labelAction, + onClick: () => onEdit(id), + }, + ]; + }, [id, onEdit]); + return ( + <EuiText size="s" className="userAction__title" data-test-subj={`user-action-title`}> + <EuiFlexGroup alignItems="baseline" gutterSize="none" justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <p> + <strong>{userName}</strong> + {` ${labelTitle} `} + <FormattedRelativePreferenceLabel value={createdAt} preferenceLabel={`${i18n.ON} `} /> + <FormattedRelativePreferenceDate value={createdAt} /> + </p> + </EuiFlexItem> + <EuiFlexItem grow={false}> + {isLoading && <MySpinner />} + {!isLoading && <PropertyActions propertyActions={propertyActions} />} + </EuiFlexItem> + </EuiFlexGroup> + </EuiText> + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx index 33e0a9541c5b4..abb49122dc142 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx @@ -32,12 +32,12 @@ const MyFlexGroup = styled(EuiFlexGroup)` `; const renderUsers = (users: ElasticUser[]) => { - return users.map(({ username }, key) => ( + return users.map(({ fullName, username }, key) => ( <MyFlexGroup key={key} justifyContent="spaceBetween"> <EuiFlexItem grow={false}> <EuiFlexGroup gutterSize="xs"> <EuiFlexItem> - <MyAvatar name={username} /> + <MyAvatar name={fullName ? fullName : username} /> </EuiFlexItem> <EuiFlexItem> <p> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 265af0bde547f..5f0509586fc81 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -14,8 +14,8 @@ export const CANCEL = i18n.translate('xpack.siem.case.caseView.cancel', { defaultMessage: 'Cancel', }); -export const CASE_TITLE = i18n.translate('xpack.siem.case.caseView.caseTitle', { - defaultMessage: 'Case Title', +export const NAME = i18n.translate('xpack.siem.case.caseView.name', { + defaultMessage: 'Name', }); export const CREATED_AT = i18n.translate('xpack.siem.case.caseView.createdAt', { @@ -45,6 +45,13 @@ export const DESCRIPTION_REQUIRED = i18n.translate( } ); +export const COMMENT_REQUIRED = i18n.translate( + 'xpack.siem.case.caseView.commentFieldRequiredError', + { + defaultMessage: 'A comment is required.', + } +); + export const EDIT = i18n.translate('xpack.siem.case.caseView.edit', { defaultMessage: 'Edit', }); @@ -58,15 +65,11 @@ export const LAST_UPDATED = i18n.translate('xpack.siem.case.caseView.updatedAt', }); export const PAGE_SUBTITLE = i18n.translate('xpack.siem.case.caseView.pageSubtitle', { - defaultMessage: 'Case Workflow Management within the Elastic SIEM', + defaultMessage: 'Cases within the Elastic SIEM', }); export const PAGE_TITLE = i18n.translate('xpack.siem.case.pageTitle', { - defaultMessage: 'Case Workflows', -}); - -export const PREVIEW = i18n.translate('xpack.siem.case.caseView.preview', { - defaultMessage: 'Preview', + defaultMessage: 'Cases', }); export const STATE = i18n.translate('xpack.siem.case.caseView.state', { @@ -77,6 +80,10 @@ export const SUBMIT = i18n.translate('xpack.siem.case.caseView.submit', { defaultMessage: 'Submit', }); +export const CREATE_CASE = i18n.translate('xpack.siem.case.caseView.createCase', { + defaultMessage: 'Create case', +}); + export const TAGS = i18n.translate('xpack.siem.case.caseView.tags', { defaultMessage: 'Tags', }); @@ -104,3 +111,18 @@ export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate( export const CONFIGURE_CASES_BUTTON = i18n.translate('xpack.siem.case.configureCasesButton', { defaultMessage: 'Configure cases', }); + +export const ADD_COMMENT = i18n.translate('xpack.siem.case.caseView.comment.addComment', { + defaultMessage: 'Add comment', +}); + +export const ADD_COMMENT_HELP_TEXT = i18n.translate( + 'xpack.siem.case.caseView.comment.addCommentHelpText', + { + defaultMessage: 'Add a new comment...', + } +); + +export const SAVE = i18n.translate('xpack.siem.case.caseView.description.save', { + defaultMessage: 'Save', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx index cc5e9b38eb2f8..abbaa6d6192ee 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx @@ -18,7 +18,7 @@ import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 're import styled from 'styled-components'; import * as RuleI18n from '../../translations'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; interface AddItemProps { addText: string; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx index 1cc7bba5558db..f921c29c06ab0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx @@ -19,7 +19,7 @@ import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/searc import { useKibana } from '../../../../../lib/kibana'; import { IMitreEnterpriseAttack } from '../../types'; import { FieldValueTimeline } from '../pick_timeline'; -import { FormSchema } from '../../../../shared_imports'; +import { FormSchema } from '../../../../../shared_imports'; import { ListItems } from './types'; import { buildQueryBarDescription, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx index b49126c8c0fe0..e87dba251ed6d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx @@ -20,7 +20,7 @@ import styled from 'styled-components'; import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; import * as Rulei18n from '../../translations'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; import { threatDefault } from '../step_about_rule/default_value'; import { IMitreEnterpriseAttack } from '../../types'; import { MyAddItemButton } from '../add_item_form'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx index 56cb02c9ec817..923ec3a7f0066 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx @@ -8,7 +8,7 @@ import { EuiFormRow } from '@elastic/eui'; import React, { useCallback, useEffect, useState } from 'react'; import { SearchTimelineSuperSelect } from '../../../../../components/timeline/search_super_select'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; export interface FieldValueTimeline { id: string | null; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx index fbe854c1ee346..5886a76182eec 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx @@ -29,7 +29,7 @@ import { convertKueryToElasticSearchQuery } from '../../../../../lib/keury'; import { useKibana } from '../../../../../lib/kibana'; import { TimelineModel } from '../../../../../store/timeline/model'; import { useSavedQueryServices } from '../../../../../utils/saved_query_services'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; import * as i18n from './translations'; export interface FieldValueQueryBar { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx index ffb6c4eda3243..1b7d17016f83c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx @@ -16,7 +16,7 @@ import { isEmpty } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; import * as I18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx index 431d793d6e68a..d93c057506ca7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx @@ -30,7 +30,7 @@ import { getUseField, UseField, useForm, -} from '../../../../shared_imports'; +} from '../../../../../shared_imports'; import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data'; import { stepAboutDefaultValue } from './default_value'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx index 27887bcbbe600..42cf1e0d95649 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx @@ -13,7 +13,7 @@ import { FormSchema, ValidationFunc, ERROR_CODE, -} from '../../../../shared_imports'; +} from '../../../../../shared_imports'; import { isMitreAttackInvalid } from '../mitre/helpers'; import { OptionalFieldLabel } from '../optional_field_label'; import { isUrlInvalid } from './helpers'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index 773eb44efb26c..837bc79e968e8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -33,7 +33,7 @@ import { getUseField, UseField, useForm, -} from '../../../../shared_imports'; +} from '../../../../../shared_imports'; import { schema } from './schema'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx index bb178d7197069..e202ff030cd90 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx @@ -17,7 +17,7 @@ import { fieldValidators, FormSchema, ValidationFunc, -} from '../../../../shared_imports'; +} from '../../../../../shared_imports'; import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; const { emptyField } = fieldValidators; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx index 2e2c7e068dd85..e9632966fdfaf 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx @@ -12,7 +12,7 @@ import { setFieldValue } from '../../helpers'; import { RuleStep, RuleStepProps, ScheduleStepRule } from '../../types'; import { StepRuleDescription } from '../description_step'; import { ScheduleItem } from '../schedule_item_form'; -import { Form, UseField, useForm } from '../../../../shared_imports'; +import { Form, UseField, useForm } from '../../../../../shared_imports'; import { StepContentWrapper } from '../step_content_wrapper'; import { schema } from './schema'; import * as I18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx index 9932e4f6ef435..8fbfdf5f25a51 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { OptionalFieldLabel } from '../optional_field_label'; -import { FormSchema } from '../../../../shared_imports'; +import { FormSchema } from '../../../../../shared_imports'; export const schema: FormSchema = { interval: { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index c985045b1897b..d816c7e867057 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -17,7 +17,7 @@ import { displaySuccessToast, useStateToaster } from '../../../../components/toa import { SpyRoute } from '../../../../utils/route/spy_routes'; import { useUserInfo } from '../../components/user_info'; import { AccordionTitle } from '../components/accordion_title'; -import { FormData, FormHook } from '../../../shared_imports'; +import { FormData, FormHook } from '../../../../shared_imports'; import { StepAboutRule } from '../components/step_about_rule'; import { StepDefineRule } from '../components/step_define_rule'; import { StepScheduleRule } from '../components/step_schedule_rule'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index 0fac4641e54a7..5e0e4223e3e27 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -26,7 +26,7 @@ import { displaySuccessToast, useStateToaster } from '../../../../components/toa import { SpyRoute } from '../../../../utils/route/spy_routes'; import { useUserInfo } from '../../components/user_info'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; -import { FormHook, FormData } from '../../../shared_imports'; +import { FormHook, FormData } from '../../../../shared_imports'; import { StepPanel } from '../components/step_panel'; import { StepAboutRule } from '../components/step_about_rule'; import { StepDefineRule } from '../components/step_define_rule'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index 3fab456d856ca..85f3bcbd236e9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -11,7 +11,7 @@ import { useLocation } from 'react-router-dom'; import { Filter } from '../../../../../../../../src/plugins/data/public'; import { Rule } from '../../../containers/detection_engine/rules'; -import { FormData, FormHook, FormSchema } from '../../shared_imports'; +import { FormData, FormHook, FormSchema } from '../../../shared_imports'; import { AboutStepRule, DefineStepRule, IMitreEnterpriseAttack, ScheduleStepRule } from './types'; interface GetStepsData { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index b2650dcc2b77e..34df20de1e461 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -6,7 +6,7 @@ import { Filter } from '../../../../../../../../src/plugins/data/common'; import { FieldValueQueryBar } from './components/query_bar'; -import { FormData, FormHook } from '../../shared_imports'; +import { FormData, FormHook } from '../../../shared_imports'; import { FieldValueTimeline } from './components/pick_timeline'; export interface EuiBasicTableSortTypes { diff --git a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts index 581c81d9f98a0..f2bcaa07b1a25 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts @@ -27,5 +27,5 @@ export const TIMELINES = i18n.translate('xpack.siem.navigation.timelines', { }); export const CASE = i18n.translate('xpack.siem.navigation.case', { - defaultMessage: 'Case', + defaultMessage: 'Cases', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/shared_imports.ts b/x-pack/legacy/plugins/siem/public/shared_imports.ts similarity index 52% rename from x-pack/legacy/plugins/siem/public/pages/shared_imports.ts rename to x-pack/legacy/plugins/siem/public/shared_imports.ts index a41f121b36926..edd7812b3bd16 100644 --- a/x-pack/legacy/plugins/siem/public/pages/shared_imports.ts +++ b/x-pack/legacy/plugins/siem/public/shared_imports.ts @@ -17,7 +17,7 @@ export { UseField, useForm, ValidationFunc, -} from '../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -export { Field } from '../../../../../../src/plugins/es_ui_shared/static/forms/components'; -export { fieldValidators } from '../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; -export { ERROR_CODE } from '../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; +} from '../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +export { Field } from '../../../../../src/plugins/es_ui_shared/static/forms/components'; +export { fieldValidators } from '../../../../../src/plugins/es_ui_shared/static/forms/helpers'; +export { ERROR_CODE } from '../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; diff --git a/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts index 5bfd121691ab4..6b4e3c194eb82 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts @@ -28,6 +28,7 @@ describe('UPDATE comment', () => { }, body: { comment: 'Update my comment', + version: 'WzEsMV0=', }, }); @@ -37,6 +38,24 @@ describe('UPDATE comment', () => { expect(response.status).toEqual(200); expect(response.payload.comment).toEqual('Update my comment'); }); + it(`Fails with 409 if version does not match`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/comment/{id}', + method: 'patch', + params: { + id: 'mock-comment-1', + }, + body: { + comment: 'Update my comment', + version: 'badv=', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCaseComments)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(409); + }); it(`Returns an error if updateComment throws`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases/comment/{id}', diff --git a/x-pack/plugins/case/server/routes/api/schema.ts b/x-pack/plugins/case/server/routes/api/schema.ts index 468abc8e7226f..765f9c722219f 100644 --- a/x-pack/plugins/case/server/routes/api/schema.ts +++ b/x-pack/plugins/case/server/routes/api/schema.ts @@ -15,6 +15,11 @@ export const NewCommentSchema = schema.object({ comment: schema.string(), }); +export const UpdateCommentArguments = schema.object({ + comment: schema.string(), + version: schema.string(), +}); + export const CommentSchema = schema.object({ comment: schema.string(), created_at: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/update_comment.ts b/x-pack/plugins/case/server/routes/api/update_comment.ts index 815f44a14e2e7..9f99253f76629 100644 --- a/x-pack/plugins/case/server/routes/api/update_comment.ts +++ b/x-pack/plugins/case/server/routes/api/update_comment.ts @@ -5,9 +5,12 @@ */ import { schema } from '@kbn/config-schema'; +import { SavedObject } from 'kibana/server'; +import Boom from 'boom'; import { wrapError } from './utils'; -import { NewCommentSchema } from './schema'; +import { UpdateCommentArguments } from './schema'; import { RouteDeps } from '.'; +import { CommentAttributes } from './types'; export function initUpdateCommentApi({ caseService, router }: RouteDeps) { router.patch( @@ -17,20 +20,45 @@ export function initUpdateCommentApi({ caseService, router }: RouteDeps) { params: schema.object({ id: schema.string(), }), - body: NewCommentSchema, + body: UpdateCommentArguments, }, }, async (context, request, response) => { + let theComment: SavedObject<CommentAttributes>; + try { + theComment = await caseService.getComment({ + client: context.core.savedObjects.client, + commentId: request.params.id, + }); + } catch (error) { + return response.customError(wrapError(error)); + } + if (request.body.version !== theComment.version) { + return response.customError( + wrapError( + Boom.conflict( + 'This comment has been updated. Please refresh before saving additional updates.' + ) + ) + ); + } + if (request.body.comment === theComment.attributes.comment) { + return response.customError( + wrapError(Boom.notAcceptable('Comment is identical to current version.')) + ); + } try { const updatedComment = await caseService.updateComment({ client: context.core.savedObjects.client, commentId: request.params.id, updatedAttributes: { - ...request.body, + comment: request.body.comment, updated_at: new Date().toISOString(), }, }); - return response.ok({ body: updatedComment.attributes }); + return response.ok({ + body: { ...updatedComment.attributes, version: updatedComment.version }, + }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 21500c4db9c34..a97cf608abc71 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10986,7 +10986,6 @@ "xpack.siem.editDataProvider.doesNotExistLabel": "存在しません", "xpack.siem.editDataProvider.existsLabel": "存在する", "xpack.siem.editDataProvider.fieldLabel": "フィールド", - "xpack.siem.editDataProvider.fieldPlaceholder": "フィールドを選択", "xpack.siem.editDataProvider.isLabel": "が", "xpack.siem.editDataProvider.isNotLabel": "is not", "xpack.siem.editDataProvider.operatorLabel": "演算子", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c9e7ea1ec80de..e6055680e1240 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10986,7 +10986,6 @@ "xpack.siem.editDataProvider.doesNotExistLabel": "不存在", "xpack.siem.editDataProvider.existsLabel": "存在", "xpack.siem.editDataProvider.fieldLabel": "字段", - "xpack.siem.editDataProvider.fieldPlaceholder": "选择字段", "xpack.siem.editDataProvider.isLabel": "是", "xpack.siem.editDataProvider.isNotLabel": "不是", "xpack.siem.editDataProvider.operatorLabel": "运算符", From 2378d8a0fdd425920d8321aaae729e0dee95013b Mon Sep 17 00:00:00 2001 From: Nathan L Smith <nathan.smith@elastic.co> Date: Mon, 2 Mar 2020 17:36:43 -0600 Subject: [PATCH 25/26] Service map language icons (#58633) Add icons as described in #56235. Also: * Add double-border and ghost "shadow" on nodes * Add framework name capability to popover metrics --- .../app/ServiceMap/Cytoscape.stories.tsx | 188 ++++++++++++++---- .../app/ServiceMap/Popover/Contents.tsx | 6 +- .../ServiceMap/Popover/Popover.stories.tsx | 1 + .../Popover/ServiceMetricFetcher.tsx | 10 +- .../ServiceMap/Popover/ServiceMetricList.tsx | 34 ++-- .../app/ServiceMap/cytoscapeOptions.ts | 10 +- .../app/ServiceMap/get_cytoscape_elements.ts | 3 +- .../public/components/app/ServiceMap/icons.ts | 55 +++-- .../app/ServiceMap/icons/default.svg | 3 + .../app/ServiceMap/icons/dot-net.svg | 127 ++++++++++++ .../components/app/ServiceMap/icons/go.svg | 11 + .../components/app/ServiceMap/icons/java.svg | 7 + .../app/ServiceMap/icons/nodejs.svg | 46 +++++ .../components/app/ServiceMap/icons/php.svg | 18 ++ .../app/ServiceMap/icons/python.svg | 19 ++ .../components/app/ServiceMap/icons/ruby.svg | 125 ++++++++++++ .../components/app/ServiceMap/icons/rumjs.svg | 3 + .../elasticsearch_fieldnames.test.ts.snap | 6 + .../apm/common/elasticsearch_fieldnames.ts | 1 + x-pack/plugins/apm/common/service_map.ts | 1 + .../server/lib/service_map/get_service_map.ts | 14 +- x-pack/plugins/apm/server/lib/services/map.ts | 88 -------- 22 files changed, 599 insertions(+), 177 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/default.svg create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/go.svg create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/java.svg create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/php.svg create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/python.svg create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg delete mode 100644 x-pack/plugins/apm/server/lib/services/map.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx index 731555694bff7..52941391ca364 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx @@ -4,51 +4,52 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiCard, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { storiesOf } from '@storybook/react'; import cytoscape from 'cytoscape'; import React from 'react'; import { Cytoscape } from './Cytoscape'; - -const elements: cytoscape.ElementDefinition[] = [ - { - data: { - id: 'opbeans-python', - label: 'opbeans-python', - agentName: 'python', - type: 'service' - } - }, - { - data: { - id: 'opbeans-node', - label: 'opbeans-node', - agentName: 'nodejs', - type: 'service' - } - }, - { - data: { - id: 'opbeans-ruby', - label: 'opbeans-ruby', - agentName: 'ruby', - type: 'service' - } - }, - { data: { source: 'opbeans-python', target: 'opbeans-node' } }, - { - data: { - bidirectional: true, - source: 'opbeans-python', - target: 'opbeans-ruby' - } - } -]; -const height = 300; -const serviceName = 'opbeans-python'; +import { iconForNode } from './icons'; storiesOf('app/ServiceMap/Cytoscape', module).add( 'example', () => { + const elements: cytoscape.ElementDefinition[] = [ + { + data: { + id: 'opbeans-python', + label: 'opbeans-python', + agentName: 'python', + type: 'service' + } + }, + { + data: { + id: 'opbeans-node', + label: 'opbeans-node', + agentName: 'nodejs', + type: 'service' + } + }, + { + data: { + id: 'opbeans-ruby', + label: 'opbeans-ruby', + agentName: 'ruby', + type: 'service' + } + }, + { data: { source: 'opbeans-python', target: 'opbeans-node' } }, + { + data: { + bidirectional: true, + source: 'opbeans-python', + target: 'opbeans-ruby' + } + } + ]; + const height = 300; + const serviceName = 'opbeans-python'; return ( <Cytoscape elements={elements} @@ -59,6 +60,119 @@ storiesOf('app/ServiceMap/Cytoscape', module).add( }, { info: { + propTables: false, + source: false + } + } +); + +storiesOf('app/ServiceMap/Cytoscape', module).add( + 'node icons', + () => { + const cy = cytoscape(); + const elements = [ + { data: { id: 'default', label: 'default', type: undefined } }, + { data: { id: 'cache', label: 'cache', type: 'cache' } }, + { data: { id: 'database', label: 'database', type: 'database' } }, + { data: { id: 'external', label: 'external', type: 'external' } }, + { data: { id: 'messaging', label: 'messaging', type: 'messaging' } }, + + { + data: { + id: 'dotnet', + label: 'dotnet service', + type: 'service', + agentName: 'dotnet' + } + }, + { + data: { + id: 'go', + label: 'go service', + type: 'service', + agentName: 'go' + } + }, + { + data: { + id: 'java', + label: 'java service', + type: 'service', + agentName: 'java' + } + }, + { + data: { + id: 'js-base', + label: 'js-base service', + type: 'service', + agentName: 'js-base' + } + }, + { + data: { + id: 'nodejs', + label: 'nodejs service', + type: 'service', + agentName: 'nodejs' + } + }, + { + data: { + id: 'php', + label: 'php service', + type: 'service', + agentName: 'php' + } + }, + { + data: { + id: 'python', + label: 'python service', + type: 'service', + agentName: 'python' + } + }, + { + data: { + id: 'ruby', + label: 'ruby service', + type: 'service', + agentName: 'ruby' + } + } + ]; + cy.add(elements); + + return ( + <EuiFlexGroup gutterSize="l" wrap={true}> + {cy.nodes().map(node => ( + <EuiFlexItem key={node.data('id')}> + <EuiCard + description={ + <pre> + agentName: {node.data('agentName') || 'undefined'}, type:{' '} + {node.data('type') || 'undefined'} + </pre> + } + icon={ + <img + alt={node.data('label')} + src={iconForNode(node)} + height={80} + width={80} + /> + } + title={node.data('label')} + /> + </EuiFlexItem> + ))} + </EuiFlexGroup> + ); + }, + { + info: { + propTables: false, source: false } } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index f1c53673c8755..405bd855898b7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -35,6 +35,7 @@ export function Contents({ onFocusClick, selectedNodeServiceName }: ContentsProps) { + const frameworkName = selectedNodeData.frameworkName; return ( <EuiFlexGroup direction="column" @@ -49,7 +50,10 @@ export function Contents({ </EuiFlexItem> <EuiFlexItem> {isService ? ( - <ServiceMetricFetcher serviceName={selectedNodeServiceName} /> + <ServiceMetricFetcher + frameworkName={frameworkName} + serviceName={selectedNodeServiceName} + /> ) : ( <Info {...selectedNodeData} /> )} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx index e5962afd76eb8..23e9e737be9a6 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -16,6 +16,7 @@ storiesOf('app/ServiceMap/Popover/ServiceMetricList', module) avgRequestsPerMinute={164.47222031860858} avgCpuUsage={0.32809666568309237} avgMemoryUsage={0.5504868173242986} + frameworkName="Spring" numInstances={2} isLoading={false} /> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx index b0a5e892b5a7e..697aa6a1b652b 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx @@ -11,10 +11,12 @@ import { useUrlParams } from '../../../../hooks/useUrlParams'; import { ServiceMetricList } from './ServiceMetricList'; interface ServiceMetricFetcherProps { + frameworkName?: string; serviceName: string; } export function ServiceMetricFetcher({ + frameworkName, serviceName }: ServiceMetricFetcherProps) { const { @@ -37,5 +39,11 @@ export function ServiceMetricFetcher({ ); const isLoading = status === 'loading'; - return <ServiceMetricList {...data} isLoading={isLoading} />; + return ( + <ServiceMetricList + {...data} + frameworkName={frameworkName} + isLoading={isLoading} + /> + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx index 3a6b4c5ebcaac..056af68cc8173 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx @@ -30,6 +30,10 @@ function LoadingSpinner() { ); } +const BadgeRow = styled(EuiFlexItem)` + padding-bottom: ${lightTheme.gutterTypes.gutterSmall}; +`; + const ItemRow = styled('tr')` line-height: 2; `; @@ -44,6 +48,7 @@ const ItemDescription = styled('td')` `; interface ServiceMetricListProps extends ServiceNodeMetrics { + frameworkName?: string; isLoading: boolean; } @@ -53,6 +58,7 @@ export function ServiceMetricList({ avgErrorsPerMinute, avgCpuUsage, avgMemoryUsage, + frameworkName, numInstances, isLoading }: ServiceMetricListProps) { @@ -106,23 +112,27 @@ export function ServiceMetricList({ : null } ]; + const showBadgeRow = frameworkName || numInstances > 1; + return isLoading ? ( <LoadingSpinner /> ) : ( <> - {numInstances && numInstances > 1 && ( - <EuiFlexItem> - <div> - <EuiBadge iconType="apps" color="hollow"> - {i18n.translate('xpack.apm.serviceMap.numInstancesMetric', { - values: { numInstances }, - defaultMessage: '{numInstances} instances' - })} - </EuiBadge> - </div> - </EuiFlexItem> + {showBadgeRow && ( + <BadgeRow> + <EuiFlexGroup gutterSize="none"> + {frameworkName && <EuiBadge>{frameworkName}</EuiBadge>} + {numInstances > 1 && ( + <EuiBadge iconType="apps" color="hollow"> + {i18n.translate('xpack.apm.serviceMap.numInstancesMetric', { + values: { numInstances }, + defaultMessage: '{numInstances} instances' + })} + </EuiBadge> + )} + </EuiFlexGroup> + </BadgeRow> )} - <table> <tbody> {listItems.map( diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index af5bd17f71ca4..8411169dbc944 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -42,19 +42,23 @@ const style: cytoscape.Stylesheet[] = [ 'background-image': (el: cytoscape.NodeSingular) => iconForNode(el) ?? defaultIcon, 'background-height': (el: cytoscape.NodeSingular) => - isService(el) ? '85%' : '40%', + isService(el) ? '60%' : '40%', 'background-width': (el: cytoscape.NodeSingular) => - isService(el) ? '85%' : '40%', + isService(el) ? '60%' : '40%', 'border-color': (el: cytoscape.NodeSingular) => el.hasClass('primary') || el.selected() ? theme.euiColorPrimary : theme.euiColorMediumShade, - 'border-width': 1, + 'border-width': 2, color: theme.textColors.default, // theme.euiFontFamily doesn't work here for some reason, so we're just // specifying a subset of the fonts for the label text. 'font-family': 'Inter UI, Segoe UI, Helvetica, Arial, sans-serif', 'font-size': theme.euiFontSizeXS, + ghost: 'yes', + 'ghost-offset-x': 0, + 'ghost-offset-y': 2, + 'ghost-opacity': 0.15, height: nodeHeight, label: 'data(label)', 'min-zoomed-font-size': theme.euiSizeL, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts index 2403ed047cbc0..bc619b1ecdfe5 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts @@ -105,7 +105,8 @@ export function getCytoscapeElements( `/services/${node['service.name']}/service-map`, search ), - agentName: node['agent.name'] || node['agent.name'], + agentName: node['agent.name'], + frameworkName: node['service.framework.name'], type: 'service' }; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts index c637d145639ce..1b57cd52082d8 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts @@ -4,26 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; import cytoscape from 'cytoscape'; import databaseIcon from './icons/database.svg'; import documentsIcon from './icons/documents.svg'; +import dotNetIcon from './icons/dot-net.svg'; import globeIcon from './icons/globe.svg'; +import goIcon from './icons/go.svg'; +import javaIcon from './icons/java.svg'; +import nodeJsIcon from './icons/nodejs.svg'; +import phpIcon from './icons/php.svg'; +import pythonIcon from './icons/python.svg'; +import rubyIcon from './icons/ruby.svg'; +import rumJsIcon from './icons/rumjs.svg'; +import defaultIconImport from './icons/default.svg'; -function getAvatarIcon( - text = '', - backgroundColor = 'transparent', - foregroundColor = 'white' -) { - return ( - 'data:image/svg+xml;utf8,' + - encodeURIComponent(`<svg width="80" height="80" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> - <circle cx="40" cy="40" fill="${backgroundColor}" r="40" stroke-width="0" /> - <text fill="${foregroundColor}" font-family="'Inter UI', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica', 'Arial', sans-serif" font-size="36" text-anchor="middle" x="40" xml:space="preserve" y="52">${text}</text> -</svg> -`) - ); -} +export const defaultIcon = defaultIconImport; // The colors here are taken from the logos of the corresponding technologies const icons: { [key: string]: string } = { @@ -34,18 +29,17 @@ const icons: { [key: string]: string } = { resource: globeIcon }; -const serviceAbbreviations: { [key: string]: string } = { - dotnet: '.N', - go: 'Go', - java: 'Jv', - 'js-base': 'JS', - nodejs: 'No', - python: 'Py', - ruby: 'Rb' +const serviceIcons: { [key: string]: string } = { + dotnet: dotNetIcon, + go: goIcon, + java: javaIcon, + 'js-base': rumJsIcon, + nodejs: nodeJsIcon, + php: phpIcon, + python: pythonIcon, + ruby: rubyIcon }; -export const defaultIcon = getAvatarIcon(); - // IE 11 does not properly load some SVGs, which causes a runtime error and the // map to not work at all. We would prefer to do some kind of feature detection // rather than browser detection, but IE 11 does support SVG, just not well @@ -61,15 +55,12 @@ export function iconForNode(node: cytoscape.NodeSingular) { const type = node.data('type'); if (type === 'service') { - return getAvatarIcon( - serviceAbbreviations[node.data('agentName') as string], - node.selected() || node.hasClass('primary') - ? theme.euiColorPrimary - : theme.euiColorDarkestShade - ); + return serviceIcons[node.data('agentName') as string]; } else if (isIE11) { return defaultIcon; - } else { + } else if (icons[type]) { return icons[type]; + } else { + return defaultIcon; } } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/default.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/default.svg new file mode 100644 index 0000000000000..08bc5331e083b --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/default.svg @@ -0,0 +1,3 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M16.75 6.165a1.5 1.5 0 00-1.5 0l-7.392 4.268a1.5 1.5 0 00-.75 1.3v8.535a1.5 1.5 0 00.75 1.299l7.392 4.268a1.5 1.5 0 001.5 0l7.392-4.268a1.5 1.5 0 00.75-1.299v-8.536a1.5 1.5 0 00-.75-1.299L16.75 6.165zm.75-1.299l7.392 4.268a3 3 0 011.5 2.598v8.536a3 3 0 01-1.5 2.598L17.5 27.134a3 3 0 01-3 0l-7.392-4.268a3 3 0 01-1.5-2.598v-8.536a3 3 0 011.5-2.598L14.5 4.866a3 3 0 013 0z" fill="#98A2B3"/> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg new file mode 100644 index 0000000000000..9f7427f0e1001 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg @@ -0,0 +1,127 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M11.164 13.586c1.145 3.503 1.58 9.753 4.93 9.753.254 0 .512-.025.77-.074-3.045-.71-3.405-6.892-5.264-10.093-.148.135-.293.273-.436.414" fill="url(#paint0_linear)"/> + <path d="M11.6 13.172c1.859 3.201 2.22 9.383 5.265 10.093.239-.044.479-.11.719-.195-2.733-1.339-3.489-7.341-5.6-10.231-.13.108-.258.22-.384.333" fill="url(#paint1_linear)"/> + <path d="M14.278 11.268a4.14 4.14 0 00-.772.074c-.678.128-1.367.42-2.06.862.189.187.37.4.539.635.706-.586 1.407-1.02 2.106-1.28.255-.095.518-.168.786-.218a2.42 2.42 0 00-.6-.073" fill="#14559A"/> + <path d="M19.718 21.836c.29-.233.571-.478.84-.737-1.164-3.487-1.58-9.826-4.954-9.826-.241 0-.485.023-.727.068 3.072.764 3.466 7.45 4.84 10.495" fill="url(#paint2_linear)"/> + <path d="M14.877 11.34a2.425 2.425 0 00-.6-.072l1.327.005a3.95 3.95 0 00-.727.068" fill="#3092C4"/> + <path d="M19.659 22.577a5.018 5.018 0 01-.38-.411 6.977 6.977 0 01-1.695.904 2.609 2.609 0 001.17.269c.63 0 1.129-.075 1.553-.278a2.95 2.95 0 01-.648-.484" fill="#1969BC"/> + <path d="M14.09 11.56c2.753 1.44 2.992 7.959 5.189 10.606.15-.106.295-.216.438-.33-1.374-3.045-1.767-9.731-4.84-10.495-.268.05-.53.124-.786.22" fill="url(#paint3_linear)"/> + <path d="M11.985 12.839c2.11 2.89 2.866 8.892 5.599 10.231a6.975 6.975 0 001.695-.904C17.082 19.519 16.843 13 14.09 11.56c-.699.26-1.4.693-2.106 1.279" fill="url(#paint4_linear)"/> + <path d="M9.814 13.502c-.331.748-.67 1.73-1.078 3.014.813-1.145 1.623-2.131 2.428-2.93a8.789 8.789 0 00-.359-.935c-.345.265-.676.55-.991.85" fill="url(#paint5_linear)"/> + <path d="M11.099 12.435c-.1.07-.197.142-.293.216.128.28.247.594.358.935.143-.14.288-.279.436-.414a5.808 5.808 0 00-.501-.737" fill="#2B74B1"/> + <path d="M11.445 12.204a9.562 9.562 0 00-.347.23c.18.223.346.47.502.738.126-.114.254-.225.385-.333a5.094 5.094 0 00-.54-.635" fill="#125A9E"/> + <path d="M30.218 11.001c-1.556 6.004-4.807 10.825-7.533 12.04h-.005c-.05.023-.1.044-.148.064-.006.004-.012.004-.018.007l-.041.016c-.007.004-.013.005-.02.007-.022.009-.045.015-.067.024-.01.005-.02.007-.028.01a.36.36 0 01-.034.011l-.033.012-.03.01-.057.017c-.009 0-.016.005-.025.007l-.04.01c-.008.005-.016.006-.026.008a.934.934 0 01-.043.01l-.052.012c.125.046.257.07.39.069 2.585 0 5.19-4.632 9.503-12.335h-1.693v.001z" fill="url(#paint6_linear)"/> + <path d="M8.175 11.512c.002 0 .004-.004.005-.004.002 0 .005 0 .006-.004h.003l.042-.016c.004 0 .006 0 .008-.004.004 0 .008-.004.011-.005l.045-.016h.003c.034-.01.066-.023.099-.035.004 0 .009-.004.015-.004l.042-.012c.007-.004.013-.004.02-.007l.042-.012c.007 0 .012-.004.017-.005.047-.013.094-.025.142-.036.006 0 .012-.005.02-.005a.274.274 0 01.04-.008c.007-.004.014-.004.021-.006.014-.004.027-.005.042-.008h.01l.086-.016h.018c.013-.004.026-.004.04-.007.007 0 .015-.004.022-.004.013 0 .025-.004.038-.006.007 0 .013 0 .021-.004.03-.004.061-.005.093-.008a2.87 2.87 0 00-.275-.014c-2.91 0-6.921 5.4-8.728 12.395h.349a101.37 101.37 0 001.572-2.922c1.265-4.954 3.842-8.333 6.131-9.228" fill="url(#paint7_linear)"/> + <path d="M9.814 13.502a12.1 12.1 0 01.991-.851 4.68 4.68 0 00-.236-.457c-.266.323-.51.75-.755 1.308M10.053 11.553c.19.16.36.379.516.641.05-.06.099-.116.15-.168a2.921 2.921 0 00-.666-.473" fill="#0D82CA"/> + <path d="M2.044 20.74c2.654-5.115 3.912-8.352 6.131-9.228-2.288.895-4.866 4.274-6.131 9.227" fill="url(#paint8_linear)"/> + <path d="M10.72 12.026c-.053.053-.102.108-.151.168.087.148.166.3.236.457.097-.074.195-.146.293-.216a3.989 3.989 0 00-.379-.409" fill="#127BCA"/> + <path d="M3.249 23.638c-.017.004-.033.004-.048.006h-.01c-.013 0-.028.004-.04.004h-.007c-.033.004-.066.004-.098.007h-.01c2.686-.075 3.914-1.42 4.524-3.371.463-1.48.843-2.725 1.177-3.77C7.61 18.097 6.48 19.985 5.345 22.1c-.548 1.02-1.382 1.445-2.096 1.537" fill="url(#paint9_linear)"/> + <path d="M3.249 23.637c.714-.09 1.548-.516 2.096-1.536 1.136-2.114 2.267-4.002 3.39-5.586.41-1.284.747-2.266 1.079-3.014-2.241 2.133-4.49 5.679-6.565 10.136" fill="url(#paint10_linear)"/> + <path d="M2.044 20.74c-.475.915-.995 1.89-1.573 2.922h1.013c.128-.985.315-1.96.56-2.922" fill="#05A1E6"/> + <path d="M9.033 11.29c-.008 0-.014.005-.021.005-.013.004-.025.004-.038.005-.008 0-.015 0-.023.004-.014.004-.026.005-.04.007-.006 0-.012 0-.017.004l-.086.015h-.01a.315.315 0 01-.042.009c-.007.004-.014.004-.021.005-.014.004-.028.006-.04.008-.008.005-.014.005-.02.005a4.64 4.64 0 00-.142.037c-.005 0-.01.004-.017.005a2.21 2.21 0 00-.043.012c-.006.004-.013.004-.02.006a1.055 1.055 0 00-.042.013c-.005 0-.01.004-.015.004-.033.012-.065.024-.099.034l-.045.016c-.006.004-.011.005-.018.008-.015.004-.03.01-.042.015-.005.004-.01.005-.014.007-2.22.876-3.477 4.113-6.132 9.228a24.646 24.646 0 00-.56 2.921h.143c.4 0 .513-.004.974-.004h.446c.032-.004.064-.004.097-.006h.007c.013 0 .027-.005.04-.005h.01c.015 0 .032-.003.048-.005 2.075-4.457 4.325-8.003 6.565-10.136.247-.558.49-.984.755-1.307a2.476 2.476 0 00-.516-.642s-.005 0-.005-.004l-.032-.015-.031-.016a.344.344 0 01-.03-.015c-.014-.004-.025-.01-.036-.015l-.029-.012a1.243 1.243 0 01-.058-.025l-.025-.01a.868.868 0 01-.044-.017c-.008-.004-.016-.005-.024-.008l-.06-.02h-.006c-.023-.008-.046-.014-.07-.02-.006-.005-.01-.005-.016-.006l-.06-.016c-.004 0-.01-.004-.013-.004a2.13 2.13 0 00-.148-.033c-.005 0-.01-.004-.015-.004a.86.86 0 00-.064-.01c-.005-.004-.008-.004-.013-.004a1.609 1.609 0 00-.076-.01h-.013c-.02-.004-.04-.004-.058-.006-.032.004-.063.005-.093.008" fill="url(#paint11_linear)"/> + <path d="M24.138 14.325c-.51 1.636-.924 2.985-1.284 4.096 1.401-1.929 2.781-4.354 4.096-7.154-1.534.482-2.351 1.584-2.812 3.058z" fill="url(#paint12_linear)"/> + <path d="M22.898 22.94a4.26 4.26 0 01-.213.102c2.726-1.216 5.977-6.038 7.533-12.04h-.315c-3.576 6.388-4.727 10.664-7.005 11.937z" fill="url(#paint13_linear)"/> + <path d="M21.119 22.403c.593-.724 1.076-1.954 1.735-3.982-.76 1.045-1.526 1.943-2.293 2.675 0 .004 0 .004-.004.007.167.5.35.943.56 1.3" fill="#079AE1"/> + <path d="M21.119 22.403a2.43 2.43 0 01-.812.658 2.295 2.295 0 00.967.274h.009c.02 0 .043 0 .065.004h.222c.008 0 .017 0 .025-.004.016 0 .033 0 .049-.004h.024c.017 0 .035-.004.053-.006h.004c.006 0 .011-.004.017-.004.018-.004.038-.006.056-.009h.017l.064-.01h.01l.128-.027c-.352-.129-.647-.433-.9-.865" fill="#1969BC"/> + <path d="M16.093 23.339c.255 0 .513-.025.772-.075.239-.043.479-.11.719-.193a2.614 2.614 0 001.17.268h-2.66z" fill="#1E5CB3"/> + <path d="M18.754 23.339c.63 0 1.129-.075 1.553-.278a2.298 2.298 0 00.967.274h.009c.02 0 .043 0 .065.004h.077-2.673.002z" fill="#1E5CB3"/> + <path d="M21.426 23.339h.146c.008 0 .017 0 .025-.004.016 0 .032 0 .05-.004h.024c.016 0 .035-.004.052-.006h.005c.005 0 .01-.004.016-.004.018-.004.038-.006.056-.009.006 0 .011 0 .018-.004l.064-.01h.01c.043-.008.085-.016.127-.027.125.046.257.07.39.069l-.982.004-.001-.005z" fill="#1D60B5"/> + <path d="M20.559 21.103v-.004c-.269.258-.55.504-.84.736-.144.114-.29.224-.44.33.123.147.249.285.38.411.201.194.415.358.649.484a2.424 2.424 0 00.812-.658c-.21-.357-.393-.799-.56-1.3" fill="#175FAB"/> + <path d="M28.904 11.001h-1.588c-.085.004-.169.007-.251.013-.039.083-.079.166-.117.25-1.314 2.8-2.695 5.225-4.096 7.153-.659 2.028-1.142 3.259-1.735 3.983.253.432.548.736.899.865.01-.004.018-.004.027-.006h.006c.006 0 .013-.004.02-.004l.044-.011c.009-.004.016-.005.026-.008l.039-.01c.008-.004.016-.005.024-.007l.059-.017a.169.169 0 01.029-.01l.033-.012.033-.01c.01-.005.02-.007.028-.01.023-.01.045-.016.068-.025.007 0 .013-.006.02-.007l.041-.016c.005-.004.011-.005.018-.007.048-.02.098-.04.147-.063h.005a4.21 4.21 0 00.214-.102c2.278-1.273 3.429-5.55 7.005-11.938h-1 .002z" fill="url(#paint14_linear)"/> + <path d="M9.126 11.282c.006 0 .01 0 .016.004a.13.13 0 01.042.005h.014l.075.01a1.473 1.473 0 00.077.015c.005 0 .01.003.016.003.049.01.1.02.147.033.005 0 .01.004.013.004l.06.017c.006 0 .01.004.016.005.024.006.047.012.07.02.001 0 .002 0 .005.004l.061.02c.009.005.016.006.024.008.014.007.03.013.044.018.008 0 .016.006.025.01.019.008.039.015.058.024l.03.012a1.675 1.675 0 00.066.03l.03.016c.254.124.488.29.704.492.078-.082.16-.157.25-.226a2.745 2.745 0 00-1.843-.518" fill="#7DCBEC"/> + <path d="M10.72 12.026c.135.127.262.264.378.409.114-.08.23-.157.347-.231a3.65 3.65 0 00-.477-.405c-.088.07-.172.145-.249.227" fill="#5EC5ED"/> + <path d="M9.126 11.282a2.749 2.749 0 011.842.518c.5-.394 1.103-.532 1.94-.532H8.851c.094 0 .185.006.275.014" fill="url(#paint15_linear)"/> + <path d="M12.908 11.268c-.838 0-1.44.138-1.94.531.17.121.33.257.477.405.694-.442 1.383-.735 2.061-.862.254-.049.512-.073.771-.074h-1.369z" fill="url(#paint16_linear)"/> + <path d="M22.854 18.421c.36-1.11.773-2.46 1.285-4.096.46-1.475 1.277-2.577 2.81-3.058.04-.082.079-.167.118-.25-2.499.15-3.667 1.42-4.255 3.303-1.028 3.288-1.65 5.418-2.25 6.776.765-.732 1.531-1.63 2.292-2.675" fill="url(#paint17_linear)"/> + <path d="M26.52 22.463h-.283v.88h-.115v-.88h-.285v-.105h.683v.105zm1.16.88h-.115v-.661c0-.052.004-.116.01-.191a.68.68 0 01-.031.096l-.335.756h-.057l-.336-.75a.526.526 0 01-.03-.102h-.004c.004.04.005.104.005.192v.66h-.11v-.985h.152l.302.688a.902.902 0 01.045.118h.004l.047-.121.309-.685h.145v.985z" fill="#000"/> + <defs> + <linearGradient id="paint0_linear" x1="14.014" y1="11.188" x2="14.014" y2="26.351" gradientUnits="userSpaceOnUse"> + <stop stop-color="#0994DC"/> + <stop offset=".35" stop-color="#66CEF5"/> + <stop offset=".846" stop-color="#127BCA"/> + <stop offset="1" stop-color="#127BCA"/> + </linearGradient> + <linearGradient id="paint1_linear" x1="14.592" y1="10.804" x2="14.592" y2="26.353" gradientUnits="userSpaceOnUse"> + <stop stop-color="#0E76BC"/> + <stop offset=".36" stop-color="#36AEE8"/> + <stop offset=".846" stop-color="#00ADEF"/> + <stop offset="1" stop-color="#00ADEF"/> + </linearGradient> + <linearGradient id="paint2_linear" x1="17.717" y1="22.99" x2="17.717" y2="10.617" gradientUnits="userSpaceOnUse"> + <stop stop-color="#1C63B7"/> + <stop offset=".5" stop-color="#33BDF2"/> + <stop offset="1" stop-color="#33BDF2" stop-opacity=".42"/> + </linearGradient> + <linearGradient id="paint3_linear" x1="16.904" y1="9.228" x2="16.904" y2="25.373" gradientUnits="userSpaceOnUse"> + <stop stop-color="#166AB8"/> + <stop offset=".4" stop-color="#36AEE8"/> + <stop offset=".846" stop-color="#0798DD"/> + <stop offset="1" stop-color="#0798DD"/> + </linearGradient> + <linearGradient id="paint4_linear" x1="15.632" y1="9.313" x2="15.632" y2="26.48" gradientUnits="userSpaceOnUse"> + <stop stop-color="#124379"/> + <stop offset=".39" stop-color="#1487CB"/> + <stop offset=".78" stop-color="#165197"/> + <stop offset="1" stop-color="#165197"/> + </linearGradient> + <linearGradient id="paint5_linear" x1="8.736" y1="14.583" x2="11.164" y2="14.583" gradientUnits="userSpaceOnUse"> + <stop stop-color="#33BDF2" stop-opacity=".698"/> + <stop offset="1" stop-color="#1DACD8"/> + </linearGradient> + <linearGradient id="paint6_linear" x1="26.965" y1="22.679" x2="26.965" y2="10.627" gradientUnits="userSpaceOnUse"> + <stop stop-color="#136AB4"/> + <stop offset=".6" stop-color="#59CAF5" stop-opacity=".549"/> + <stop offset="1" stop-color="#59CAF5" stop-opacity=".235"/> + </linearGradient> + <linearGradient id="paint7_linear" x1=".123" y1="17.463" x2="9.126" y2="17.463" gradientUnits="userSpaceOnUse"> + <stop stop-color="#05A1E6" stop-opacity=".247"/> + <stop offset="1" stop-color="#05A1E6"/> + </linearGradient> + <linearGradient id="paint8_linear" x1="5.109" y1="22.983" x2="5.109" y2="10.642" gradientUnits="userSpaceOnUse"> + <stop stop-color="#318ED5"/> + <stop offset="1" stop-color="#38A7E4"/> + </linearGradient> + <linearGradient id="paint9_linear" x1="5.886" y1="23.026" x2="5.886" y2="11.03" gradientUnits="userSpaceOnUse"> + <stop stop-color="#05A1E6"/> + <stop offset="1" stop-color="#05A1E6" stop-opacity=".549"/> + </linearGradient> + <linearGradient id="paint10_linear" x1="6.531" y1="23.639" x2="6.531" y2="13.502" gradientUnits="userSpaceOnUse"> + <stop stop-color="#1959A6"/> + <stop offset=".5" stop-color="#05A1E6"/> + <stop offset=".918" stop-color="#7EC5EA"/> + <stop offset="1" stop-color="#7EC5EA"/> + </linearGradient> + <linearGradient id="paint11_linear" x1="6.92" y1="22.991" x2="3.484" y2="12.49" gradientUnits="userSpaceOnUse"> + <stop stop-color="#165096"/> + <stop offset="1" stop-color="#0D82CA"/> + </linearGradient> + <linearGradient id="paint12_linear" x1="24.901" y1="16.775" x2="24.901" y2="11.031" gradientUnits="userSpaceOnUse"> + <stop stop-color="#05A1E6"/> + <stop offset=".874" stop-color="#0495D6"/> + <stop offset="1" stop-color="#0495D6"/> + </linearGradient> + <linearGradient id="paint13_linear" x1="27.601" y1="10.5" x2="22.248" y2="22.386" gradientUnits="userSpaceOnUse"> + <stop stop-color="#38A7E4" stop-opacity=".329"/> + <stop offset=".962" stop-color="#0E88D3"/> + <stop offset="1" stop-color="#0E88D3"/> + </linearGradient> + <linearGradient id="paint14_linear" x1="25.511" y1="10.965" x2="25.511" y2="23.496" gradientUnits="userSpaceOnUse"> + <stop stop-color="#168CD4"/> + <stop offset=".5" stop-color="#1C87CC"/> + <stop offset="1" stop-color="#154B8D"/> + </linearGradient> + <linearGradient id="paint15_linear" x1="9.527" y1="10.707" x2="9.582" y2="12.022" gradientUnits="userSpaceOnUse"> + <stop stop-color="#97D6EE"/> + <stop offset=".703" stop-color="#55C1EA"/> + <stop offset="1" stop-color="#55C1EA"/> + </linearGradient> + <linearGradient id="paint16_linear" x1="11.196" y1="12.145" x2="11.442" y2="10.753" gradientUnits="userSpaceOnUse"> + <stop stop-color="#7ACCEC"/> + <stop offset="1" stop-color="#3FB7ED"/> + </linearGradient> + <linearGradient id="paint17_linear" x1="23.814" y1="11.658" x2="23.814" y2="23.914" gradientUnits="userSpaceOnUse"> + <stop stop-color="#1DA7E7"/> + <stop offset="1" stop-color="#37ABE7" stop-opacity="0"/> + </linearGradient> + </defs> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/go.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/go.svg new file mode 100644 index 0000000000000..fb171e2813fac --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/go.svg @@ -0,0 +1,11 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <g clip-path="url(#clip0)" fill="#00ADD8"> + <path d="M2.414 13.15c-.062 0-.077-.031-.046-.078l.324-.417c.031-.047.109-.078.17-.078h5.525c.062 0 .077.047.046.093l-.263.402c-.03.047-.108.093-.155.093l-5.601-.015zM.077 14.573c-.061 0-.077-.03-.046-.077l.325-.418c.03-.046.108-.077.17-.077h7.056c.062 0 .093.046.078.093l-.124.371c-.016.062-.078.093-.14.093l-7.319.015zm3.745 1.424c-.062 0-.077-.046-.046-.093l.216-.387c.031-.046.093-.093.155-.093h3.095c.062 0 .093.047.093.109l-.031.371c0 .062-.062.108-.109.108l-3.373-.015zm16.062-3.126c-.975.248-1.64.433-2.6.681-.232.062-.247.077-.448-.155-.232-.263-.403-.433-.728-.588-.975-.48-1.918-.34-2.8.232-1.053.681-1.594 1.687-1.579 2.94.016 1.238.867 2.26 2.09 2.43 1.051.14 1.933-.232 2.63-1.021.139-.17.263-.356.418-.573H13.88c-.325 0-.402-.201-.294-.464.201-.48.573-1.285.79-1.687a.418.418 0 01.386-.247h5.633c-.031.417-.031.835-.093 1.253a6.598 6.598 0 01-1.27 3.033c-1.113 1.47-2.568 2.383-4.41 2.63-1.516.201-2.924-.093-4.162-1.02-1.145-.867-1.795-2.013-1.965-3.436-.201-1.687.294-3.203 1.315-4.534 1.1-1.439 2.554-2.352 4.333-2.677 1.455-.263 2.847-.093 4.1.758.82.542 1.409 1.285 1.796 2.182.093.14.03.217-.155.263z"/> + <path d="M25.006 21.428c-1.408-.03-2.693-.433-3.776-1.361-.913-.79-1.485-1.795-1.671-2.987-.279-1.748.201-3.296 1.253-4.673 1.13-1.486 2.492-2.26 4.333-2.584 1.578-.279 3.064-.124 4.41.789 1.223.836 1.98 1.965 2.182 3.45.263 2.09-.34 3.792-1.78 5.246-1.02 1.037-2.274 1.687-3.713 1.981-.418.077-.836.093-1.238.14zm3.683-6.251c-.016-.201-.016-.356-.047-.51-.278-1.533-1.686-2.4-3.157-2.059-1.439.325-2.367 1.238-2.707 2.693-.279 1.207.309 2.429 1.423 2.924.851.371 1.702.325 2.522-.093 1.223-.634 1.888-1.625 1.966-2.955z"/> + </g> + <defs> + <clipPath id="clip0"> + <path fill="#fff" d="M0 9.333h32v12.275H0z"/> + </clipPath> + </defs> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/java.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/java.svg new file mode 100644 index 0000000000000..52a410e2eaa1a --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/java.svg @@ -0,0 +1,7 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M11.729 24.747s-1.243.71.885.951c2.58.29 3.897.248 6.738-.28 0 0 .748.46 1.792.858-6.371 2.685-14.419-.155-9.415-1.53zm-.779-3.503s-1.394 1.014.736 1.231c2.755.28 4.93.303 8.695-.41 0 0 .52.52 1.338.803-7.702 2.215-16.28.174-10.769-1.624z" fill="#3174B9"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M17.512 15.3c1.57 1.778-.411 3.377-.411 3.377s3.986-2.023 2.155-4.557c-1.71-2.362-3.02-3.536 4.077-7.583 0 0-11.141 2.735-5.82 8.763z" fill="#CA3132"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M25.938 27.338s.92.746-1.013 1.323c-3.677 1.095-15.304 1.425-18.534.044-1.16-.497 1.016-1.186 1.701-1.332.714-.151 1.122-.124 1.122-.124-1.291-.894-8.346 1.756-3.583 2.516 12.988 2.071 23.675-.932 20.307-2.427zm-13.611-9.724s-5.914 1.381-2.094 1.884c1.613.212 4.827.163 7.823-.084a61.883 61.883 0 004.905-.634s-.862.363-1.487.782c-6.007 1.554-17.608.83-14.268-.758 2.824-1.343 5.12-1.19 5.12-1.19zm10.61 5.831c6.105-3.12 3.282-6.117 1.311-5.713a4.702 4.702 0 00-.698.184s.18-.276.522-.395c3.898-1.347 6.896 3.974-1.257 6.082 0 0 .094-.084.122-.158z" fill="#3174B9"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M19.256 0s3.38 3.326-3.207 8.44c-5.283 4.103-1.205 6.442-.002 9.115-3.084-2.736-5.347-5.144-3.83-7.386C14.448 6.879 20.62 5.283 19.257 0z" fill="#CA3132"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M12.927 31.9c5.86.368 14.86-.205 15.073-2.932 0 0-.41 1.033-4.843 1.853-5.002.926-11.172.819-14.83.225 0 0 .75.61 4.6.853z" fill="#3174B9"/> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg new file mode 100644 index 0000000000000..d327b1ba65ad2 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg @@ -0,0 +1,46 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <mask id="a" maskUnits="userSpaceOnUse" x="5" y="4" width="22" height="24"> + <path d="M16.101 4.15a1.151 1.151 0 00-1.139 0L5.56 9.579c-.358.201-.559.58-.559.983v10.878c0 .402.223.782.559.983l9.403 5.427a1.152 1.152 0 001.14 0l9.403-5.427c.357-.201.559-.581.559-.983V10.56c0-.402-.224-.782-.559-.983l-9.404-5.427z" fill="#fff"/> + </mask> + <g mask="url(#a)"> + <path d="M16.101 4.15a1.151 1.151 0 00-1.139 0L5.56 9.579c-.358.201-.559.58-.559.983v10.878c0 .402.223.782.559.983l9.403 5.427a1.152 1.152 0 001.14 0l9.403-5.427c.357-.201.559-.581.559-.983V10.56c0-.402-.224-.782-.559-.983l-9.404-5.427z" fill="url(#paint0_linear)"/> + <path d="M25.527 9.577L16.08 4.15a1.55 1.55 0 00-.29-.112L5.2 22.175c.088.107.194.198.313.268l9.448 5.428c.268.157.581.201.871.112L25.773 9.8a1.173 1.173 0 00-.246-.223z" fill="url(#paint1_linear)"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M25.55 22.422c.267-.157.468-.425.558-.715L15.744 4.017c-.268-.045-.559-.023-.804.133L5.559 9.556l10.118 18.45c.147-.024.29-.07.424-.134l9.448-5.45z" fill="url(#paint2_linear)"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M25.55 22.422l-9.426 5.428a1.349 1.349 0 01-.424.134l.178.335 10.453-6.053v-.134l-.268-.447c-.044.313-.245.58-.514.737z" fill="url(#paint3_linear)"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M25.55 22.422l-9.426 5.428a1.349 1.349 0 01-.424.134l.178.335 10.453-6.053v-.134l-.268-.447c-.044.313-.245.58-.514.737z" fill="url(#paint4_linear)"/> + </g> + <defs> + <linearGradient id="paint0_linear" x1="19.363" y1="8.197" x2="9.056" y2="24.392" gradientUnits="userSpaceOnUse"> + <stop stop-color="#41873F"/> + <stop offset=".329" stop-color="#418B3D"/> + <stop offset=".635" stop-color="#419637"/> + <stop offset=".932" stop-color="#3FA92D"/> + <stop offset="1" stop-color="#3FAE2A"/> + </linearGradient> + <linearGradient id="paint1_linear" x1="14.104" y1="17.273" x2="39.918" y2="3.249" gradientUnits="userSpaceOnUse"> + <stop offset=".138" stop-color="#41873F"/> + <stop offset=".403" stop-color="#54A044"/> + <stop offset=".714" stop-color="#66B848"/> + <stop offset=".908" stop-color="#6CC04A"/> + </linearGradient> + <linearGradient id="paint2_linear" x1="4.657" y1="16" x2="26.416" y2="16" gradientUnits="userSpaceOnUse"> + <stop offset=".092" stop-color="#6CC04A"/> + <stop offset=".286" stop-color="#66B848"/> + <stop offset=".597" stop-color="#54A044"/> + <stop offset=".862" stop-color="#41873F"/> + </linearGradient> + <linearGradient id="paint3_linear" x1="4.657" y1="25.02" x2="26.416" y2="25.02" gradientUnits="userSpaceOnUse"> + <stop offset=".092" stop-color="#6CC04A"/> + <stop offset=".286" stop-color="#66B848"/> + <stop offset=".597" stop-color="#54A044"/> + <stop offset=".862" stop-color="#41873F"/> + </linearGradient> + <linearGradient id="paint4_linear" x1="29.586" y1="7.683" x2="24.073" y2="36.568" gradientUnits="userSpaceOnUse"> + <stop stop-color="#41873F"/> + <stop offset=".329" stop-color="#418B3D"/> + <stop offset=".635" stop-color="#419637"/> + <stop offset=".932" stop-color="#3FA92D"/> + <stop offset="1" stop-color="#3FAE2A"/> + </linearGradient> + </defs> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/php.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/php.svg new file mode 100644 index 0000000000000..c8af5dc331269 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/php.svg @@ -0,0 +1,18 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M.45 15.63c0 4.518 6.962 8.18 15.55 8.18 8.588 0 15.55-3.662 15.55-8.18S24.588 7.45 16 7.45C7.412 7.45.45 11.112.45 15.63z" fill="url(#paint0_radial)"/> + <path d="M16 23.203c8.253 0 14.943-3.39 14.943-7.573 0-4.182-6.69-7.573-14.943-7.573-8.252 0-14.943 3.39-14.943 7.573 0 4.182 6.69 7.573 14.943 7.573z" fill="#777BB3"/> + <path d="M8.898 16.569c.679 0 1.186-.125 1.506-.372.317-.244.536-.667.651-1.257.107-.552.066-.937-.121-1.145-.192-.212-.606-.32-1.232-.32H8.618l-.601 3.094h.881zm-3.546 3.81a.168.168 0 01-.166-.2l1.593-8.198a.169.169 0 01.166-.137h3.434c1.079 0 1.882.293 2.387.871.508.581.664 1.394.466 2.414-.08.416-.22.802-.413 1.148a3.841 3.841 0 01-.76.952c-.372.349-.793.601-1.25.75-.45.146-1.029.22-1.719.22H7.7l-.397 2.044a.169.169 0 01-.166.136H5.352z" fill="#000"/> + <path d="M8.757 13.644h.946c.754 0 1.017.166 1.106.264.148.164.175.51.08 1-.106.548-.304.937-.588 1.155-.29.224-.762.337-1.403.337h-.676l.535-2.756zm1.622-1.969H6.945c-.162 0-.3.115-.331.274L5.02 20.146a.338.338 0 00.332.402h1.785c.162 0 .3-.115.332-.273l.37-1.907H9.09c.708 0 1.304-.077 1.77-.229.481-.156.923-.42 1.314-.786.324-.298.59-.632.792-.993.202-.362.347-.765.432-1.198.209-1.075.039-1.935-.505-2.558-.538-.616-1.384-.928-2.514-.928zm-2.567 5.062h1.086c.72 0 1.257-.135 1.609-.406.353-.272.59-.725.714-1.36.118-.608.064-1.038-.162-1.289-.226-.25-.678-.375-1.356-.375H8.479l-.667 3.43zm2.567-4.724c1.033 0 1.786.271 2.26.813.474.543.616 1.3.427 2.271-.078.4-.209.766-.394 1.098a3.663 3.663 0 01-.727.91 3.154 3.154 0 01-1.189.713c-.436.142-.99.213-1.666.213H7.56l-.423 2.18H5.352l1.593-8.198h3.434" fill="#fff"/> + <path d="M17.326 18.2a.168.168 0 01-.165-.201l.705-3.627c.067-.345.05-.593-.047-.697-.06-.064-.238-.172-.765-.172h-1.277l-.886 4.56a.169.169 0 01-.166.137h-1.771a.169.169 0 01-.166-.201L14.38 9.8a.169.169 0 01.166-.136h1.771a.169.169 0 01.166.2l-.384 1.98h1.373c1.047 0 1.756.184 2.17.563.42.387.552 1.007.39 1.84l-.741 3.815a.169.169 0 01-.166.137h-1.8z" fill="#000"/> + <path d="M16.318 9.496h-1.771c-.162 0-.3.115-.332.273l-1.593 8.198a.337.337 0 00.331.401h1.772c.162 0 .3-.114.331-.273l.86-4.423h1.138c.526 0 .637.113.641.117.032.035.074.194.005.55l-.705 3.628a.337.337 0 00.331.401h1.8c.162 0 .3-.114.331-.273l.742-3.814c.174-.896.025-1.568-.443-1.997-.445-.41-1.192-.609-2.283-.609h-1.169l.346-1.777a.337.337 0 00-.332-.402zm0 .338l-.423 2.179h1.578c.993 0 1.678.173 2.055.52.377.346.49.907.34 1.683l-.742 3.815h-1.8l.705-3.627c.08-.413.05-.694-.088-.844-.14-.15-.436-.225-.89-.225h-1.415l-.913 4.696h-1.772l1.594-8.197h1.771z" fill="#fff"/> + <path d="M22.836 16.569c.679 0 1.185-.125 1.506-.372.317-.244.536-.667.65-1.257.108-.552.067-.937-.12-1.145-.192-.212-.606-.32-1.232-.32h-1.084l-.601 3.094h.881zm-3.546 3.81a.168.168 0 01-.166-.2l1.593-8.198a.169.169 0 01.166-.137h3.434c1.079 0 1.882.293 2.387.871.508.581.664 1.393.466 2.414-.081.416-.22.802-.413 1.148-.194.346-.45.667-.76.952-.372.349-.793.601-1.25.75-.45.146-1.029.22-1.719.22h-1.39l-.397 2.044a.169.169 0 01-.166.136H19.29z" fill="#000"/> + <path d="M22.695 13.644h.945c.755 0 1.017.166 1.107.264.147.164.175.51.08 1-.107.548-.304.937-.588 1.155-.29.224-.762.337-1.403.337h-.676l.535-2.756zm1.622-1.969h-3.434c-.162 0-.3.115-.331.274l-1.594 8.197a.338.338 0 00.332.402h1.785c.162 0 .3-.115.331-.273l.37-1.907h1.252c.708 0 1.304-.077 1.77-.229.481-.156.923-.42 1.314-.787.324-.297.59-.631.792-.992.202-.362.347-.765.431-1.198.21-1.075.04-1.936-.504-2.558-.538-.616-1.384-.928-2.514-.928zm-2.567 5.062h1.086c.72 0 1.257-.135 1.609-.406.352-.272.59-.725.713-1.36.119-.608.065-1.038-.161-1.289-.226-.25-.678-.375-1.357-.375h-1.223l-.667 3.43zm2.567-4.724c1.033 0 1.786.271 2.26.813.474.543.616 1.3.427 2.271-.078.4-.21.766-.394 1.098a3.66 3.66 0 01-.727.91 3.154 3.154 0 01-1.189.713c-.436.142-.99.213-1.666.213h-1.53l-.423 2.18H19.29l1.593-8.198h3.434" fill="#fff"/> + <defs> + <radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.786 10.326) scale(20.4194)"> + <stop stop-color="#AEB2D5"/> + <stop offset=".3" stop-color="#AEB2D5"/> + <stop offset=".75" stop-color="#484C89"/> + <stop offset="1" stop-color="#484C89"/> + </radialGradient> + </defs> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/python.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/python.svg new file mode 100644 index 0000000000000..9b8d0a2836c28 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/python.svg @@ -0,0 +1,19 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <g clip-path="url(#clip0)"> + <path d="M15.897 4.007c-6.078 0-5.698 2.635-5.698 2.635l.007 2.73h5.8v.82H7.901s-3.889-.44-3.889 5.691c0 6.133 3.394 5.915 3.394 5.915h2.026v-2.845s-.109-3.395 3.34-3.395h5.753s3.231.053 3.231-3.123v-5.25s.491-3.178-5.86-3.178zm-3.198 1.836a1.043 1.043 0 11.002 2.086 1.043 1.043 0 01-.002-2.086z" fill="url(#paint0_linear)"/> + <path d="M16.07 27.822c6.077 0 5.698-2.635 5.698-2.635l-.007-2.73h-5.8v-.82h8.103s3.89.44 3.89-5.692c0-6.132-3.395-5.915-3.395-5.915h-2.026v2.846s.11 3.394-3.34 3.394h-5.752s-3.232-.052-3.232 3.124v5.25s-.49 3.178 5.86 3.178zm3.198-1.836a1.04 1.04 0 01-1.044-1.043 1.041 1.041 0 011.443-.965 1.042 1.042 0 010 1.929 1.04 1.04 0 01-.4.079z" fill="url(#paint1_linear)"/> + </g> + <defs> + <linearGradient id="paint0_linear" x1="6.314" y1="6.149" x2="18.178" y2="17.894" gradientUnits="userSpaceOnUse"> + <stop stop-color="#387EB8"/> + <stop offset="1" stop-color="#366994"/> + </linearGradient> + <linearGradient id="paint1_linear" x1="13.596" y1="13.691" x2="26.337" y2="25.735" gradientUnits="userSpaceOnUse"> + <stop stop-color="#FFE052"/> + <stop offset="1" stop-color="#FFC331"/> + </linearGradient> + <clipPath id="clip0"> + <path fill="#fff" d="M4 4h24v23.904H4z"/> + </clipPath> + </defs> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg new file mode 100644 index 0000000000000..fdc54b91f9c29 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg @@ -0,0 +1,125 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <g clip-path="url(#clip0)"> + <path d="M22.51 19.726l-13.64 8.1 17.662-1.198 1.36-17.81-5.381 10.908z" fill="url(#paint0_linear)"/> + <path d="M26.561 26.616l-1.518-10.478-4.135 5.46 5.653 5.018z" fill="url(#paint1_linear)"/> + <path d="M26.582 26.616l-11.122-.873-6.532 2.06 17.654-1.187z" fill="url(#paint2_linear)"/> + <path d="M8.944 27.806l2.779-9.102-6.114 1.307 3.335 7.795z" fill="url(#paint3_linear)"/> + <path d="M20.907 21.628l-2.556-10.014-7.317 6.858 9.873 3.156z" fill="url(#paint4_linear)"/> + <path d="M27.313 11.755l-6.916-5.648-1.926 6.226 8.842-.578z" fill="url(#paint5_linear)"/> + <path d="M24.078 4.093L20.011 6.34l-2.566-2.278 6.633.03z" fill="url(#paint6_linear)"/> + <path d="M4 23.064l1.704-3.107-1.378-3.702L4 23.065z" fill="url(#paint7_linear)"/> + <path d="M4.234 16.138l1.387 3.933 6.026-1.352 6.88-6.394 1.941-6.166L17.411 4l-5.198 1.945c-1.637 1.523-4.815 4.537-4.93 4.593-.113.058-2.098 3.81-3.049 5.6z" fill="#fff"/> + <path d="M9.104 9.071c3.549-3.519 8.124-5.598 9.88-3.826 1.754 1.771-.107 6.076-3.656 9.594s-8.067 5.711-9.822 3.94c-1.756-1.77.048-6.19 3.598-9.708z" fill="url(#paint8_linear)"/> + <path d="M8.944 27.802l2.757-9.13 9.155 2.94c-3.31 3.104-6.992 5.729-11.912 6.19z" fill="url(#paint9_linear)"/> + <path d="M18.539 12.308l2.35 9.31c2.765-2.908 5.247-6.034 6.462-9.9l-8.812.59z" fill="url(#paint10_linear)"/> + <path d="M27.327 11.765c.94-2.839 1.158-6.911-3.278-7.667l-3.64 2.01 6.918 5.657z" fill="url(#paint11_linear)"/> + <path d="M4 23.023c.13 4.686 3.51 4.755 4.95 4.796l-3.326-7.767L4 23.023z" fill="#9E1209"/> + <path d="M18.552 12.322c2.125 1.306 6.407 3.929 6.494 3.977.135.076 1.846-2.886 2.234-4.56l-8.728.583z" fill="url(#paint12_radial)"/> + <path d="M11.697 18.671l3.686 7.11c2.179-1.182 3.885-2.621 5.448-4.164l-9.134-2.946z" fill="url(#paint13_radial)"/> + <path d="M5.61 20.062l-.522 6.217c.985 1.346 2.34 1.463 3.762 1.358-1.028-2.56-3.083-7.68-3.24-7.575z" fill="url(#paint14_linear)"/> + <path d="M20.388 6.123l7.321 1.028c-.39-1.656-1.59-2.724-3.635-3.058l-3.686 2.03z" fill="url(#paint15_linear)"/> + </g> + <defs> + <linearGradient id="paint0_linear" x1="24.992" y1="29.993" x2="19.957" y2="21.091" gradientUnits="userSpaceOnUse"> + <stop stop-color="#FB7655"/> + <stop offset=".41" stop-color="#E42B1E"/> + <stop offset=".99" stop-color="#900"/> + <stop offset="1" stop-color="#900"/> + </linearGradient> + <linearGradient id="paint1_linear" x1="27.503" y1="22.518" x2="20.425" y2="21.135" gradientUnits="userSpaceOnUse"> + <stop stop-color="#871101"/> + <stop offset=".99" stop-color="#911209"/> + <stop offset="1" stop-color="#911209"/> + </linearGradient> + <linearGradient id="paint2_linear" x1="22.305" y1="30.263" x2="22.214" y2="25.774" gradientUnits="userSpaceOnUse"> + <stop stop-color="#871101"/> + <stop offset=".99" stop-color="#911209"/> + <stop offset="1" stop-color="#911209"/> + </linearGradient> + <linearGradient id="paint3_linear" x1="8.666" y1="19.362" x2="10.771" y2="25.533" gradientUnits="userSpaceOnUse"> + <stop stop-color="#fff"/> + <stop offset=".23" stop-color="#E57252"/> + <stop offset=".46" stop-color="#DE3B20"/> + <stop offset=".99" stop-color="#A60003"/> + <stop offset="1" stop-color="#A60003"/> + </linearGradient> + <linearGradient id="paint4_linear" x1="15.593" y1="13.251" x2="15.975" y2="19.93" gradientUnits="userSpaceOnUse"> + <stop stop-color="#fff"/> + <stop offset=".23" stop-color="#E4714E"/> + <stop offset=".56" stop-color="#BE1A0D"/> + <stop offset=".99" stop-color="#A80D00"/> + <stop offset="1" stop-color="#A80D00"/> + </linearGradient> + <linearGradient id="paint5_linear" x1="21.739" y1="7.078" x2="22.297" y2="11.928" gradientUnits="userSpaceOnUse"> + <stop stop-color="#fff"/> + <stop offset=".18" stop-color="#E46342"/> + <stop offset=".4" stop-color="#C82410"/> + <stop offset=".99" stop-color="#A80D00"/> + <stop offset="1" stop-color="#A80D00"/> + </linearGradient> + <linearGradient id="paint6_linear" x1="18.347" y1="5.392" x2="19.134" y2="2.055" gradientUnits="userSpaceOnUse"> + <stop stop-color="#fff"/> + <stop offset=".54" stop-color="#C81F11"/> + <stop offset=".99" stop-color="#BF0905"/> + <stop offset="1" stop-color="#BF0905"/> + </linearGradient> + <linearGradient id="paint7_linear" x1="4.471" y1="17.694" x2="6.529" y2="18.984" gradientUnits="userSpaceOnUse"> + <stop stop-color="#fff"/> + <stop offset=".31" stop-color="#DE4024"/> + <stop offset=".99" stop-color="#BF190B"/> + <stop offset="1" stop-color="#BF190B"/> + </linearGradient> + <linearGradient id="paint8_linear" x1="1.762" y1="22.704" x2="20.255" y2="3.635" gradientUnits="userSpaceOnUse"> + <stop stop-color="#BD0012"/> + <stop offset=".07" stop-color="#fff"/> + <stop offset=".17" stop-color="#fff"/> + <stop offset=".27" stop-color="#C82F1C"/> + <stop offset=".33" stop-color="#820C01"/> + <stop offset=".46" stop-color="#A31601"/> + <stop offset=".72" stop-color="#B31301"/> + <stop offset=".99" stop-color="#E82609"/> + <stop offset="1" stop-color="#E82609"/> + </linearGradient> + <linearGradient id="paint9_linear" x1="15.948" y1="24.625" x2="10.714" y2="22.427" gradientUnits="userSpaceOnUse"> + <stop stop-color="#8C0C01"/> + <stop offset=".54" stop-color="#990C00"/> + <stop offset=".99" stop-color="#A80D0E"/> + <stop offset="1" stop-color="#A80D0E"/> + </linearGradient> + <linearGradient id="paint10_linear" x1="25.529" y1="17.93" x2="20.138" y2="14.101" gradientUnits="userSpaceOnUse"> + <stop stop-color="#7E110B"/> + <stop offset=".99" stop-color="#9E0C00"/> + <stop offset="1" stop-color="#9E0C00"/> + </linearGradient> + <linearGradient id="paint11_linear" x1="27.349" y1="9.781" x2="24.814" y2="7.207" gradientUnits="userSpaceOnUse"> + <stop stop-color="#79130D"/> + <stop offset=".99" stop-color="#9E120B"/> + <stop offset="1" stop-color="#9E120B"/> + </linearGradient> + <linearGradient id="paint14_linear" x1="7.216" y1="27.797" x2="2.671" y2="24.024" gradientUnits="userSpaceOnUse"> + <stop stop-color="#8B2114"/> + <stop offset=".43" stop-color="#9E100A"/> + <stop offset=".99" stop-color="#B3100C"/> + <stop offset="1" stop-color="#B3100C"/> + </linearGradient> + <linearGradient id="paint15_linear" x1="22.648" y1="5.181" x2="23.939" y2="8.445" gradientUnits="userSpaceOnUse"> + <stop stop-color="#B31000"/> + <stop offset=".44" stop-color="#910F08"/> + <stop offset=".99" stop-color="#791C12"/> + <stop offset="1" stop-color="#791C12"/> + </linearGradient> + <radialGradient id="paint12_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(6.07279 0 0 3.17318 21.345 13.573)"> + <stop stop-color="#A80D00"/> + <stop offset=".99" stop-color="#7E0E08"/> + <stop offset="1" stop-color="#7E0E08"/> + </radialGradient> + <radialGradient id="paint13_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(8.07284 0 0 6.28431 12.935 21.576)"> + <stop stop-color="#A30C00"/> + <stop offset=".99" stop-color="#800E08"/> + <stop offset="1" stop-color="#800E08"/> + </radialGradient> + <clipPath id="clip0"> + <path fill="#fff" d="M4 4h24v23.904H4z"/> + </clipPath> + </defs> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg new file mode 100644 index 0000000000000..87043159ed8c3 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg @@ -0,0 +1,3 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M5 20.245l2.438-1.485c.47.84.898 1.55 1.924 1.55.984 0 1.604-.387 1.604-1.894V8.172h2.994V18.46c0 3.12-1.818 4.54-4.47 4.54-2.394 0-3.784-1.248-4.49-2.754zm10.586-.323l2.438-1.42c.642 1.055 1.476 1.83 2.95 1.83 1.241 0 2.032-.625 2.032-1.486 0-1.033-.812-1.398-2.18-2l-.75-.324c-2.159-.925-3.592-2.087-3.592-4.54 0-2.26 1.711-3.982 4.384-3.982 1.903 0 3.272.667 4.255 2.41l-2.33 1.507c-.514-.925-1.07-1.291-1.925-1.291-.877 0-1.433.56-1.433 1.29 0 .905.556 1.27 1.84 1.83l.748.323C24.567 15.167 26 16.286 26 18.803 26 21.515 23.883 23 21.039 23c-2.78 0-4.576-1.334-5.453-3.078" fill="#000"/> +</svg> diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 8f87b3473b2e4..b4b4e7866e9b7 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -66,6 +66,8 @@ exports[`Error SERVICE_AGENT_NAME 1`] = `"java"`; exports[`Error SERVICE_ENVIRONMENT 1`] = `undefined`; +exports[`Error SERVICE_FRAMEWORK_NAME 1`] = `undefined`; + exports[`Error SERVICE_NAME 1`] = `"service name"`; exports[`Error SERVICE_NODE_NAME 1`] = `undefined`; @@ -176,6 +178,8 @@ exports[`Span SERVICE_AGENT_NAME 1`] = `"java"`; exports[`Span SERVICE_ENVIRONMENT 1`] = `undefined`; +exports[`Span SERVICE_FRAMEWORK_NAME 1`] = `undefined`; + exports[`Span SERVICE_NAME 1`] = `"service name"`; exports[`Span SERVICE_NODE_NAME 1`] = `undefined`; @@ -286,6 +290,8 @@ exports[`Transaction SERVICE_AGENT_NAME 1`] = `"java"`; exports[`Transaction SERVICE_ENVIRONMENT 1`] = `undefined`; +exports[`Transaction SERVICE_FRAMEWORK_NAME 1`] = `undefined`; + exports[`Transaction SERVICE_NAME 1`] = `"service name"`; exports[`Transaction SERVICE_NODE_NAME 1`] = `undefined`; diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index ce2db4964a412..14233aad0f53c 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -7,6 +7,7 @@ export const SERVICE_NAME = 'service.name'; export const SERVICE_ENVIRONMENT = 'service.environment'; export const SERVICE_AGENT_NAME = 'agent.name'; +export const SERVICE_FRAMEWORK_NAME = 'service.framework.name'; export const SERVICE_NODE_NAME = 'service.node.name'; export const SERVICE_VERSION = 'service.version'; export const URL_FULL = 'url.full'; diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 548b29346e483..f4354baa97655 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -10,6 +10,7 @@ import { ILicense } from '../../licensing/public'; export interface ServiceConnectionNode { 'service.name': string; 'service.environment': string | null; + 'service.framework.name': string | null; 'agent.name': string; } export interface ExternalConnectionNode { diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index 04e2a43a4b8f1..85d71784b55c7 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -16,7 +16,8 @@ import { getServicesProjection } from '../../../common/projections/services'; import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { SERVICE_AGENT_NAME, - SERVICE_NAME + SERVICE_NAME, + SERVICE_FRAMEWORK_NAME } from '../../../common/elasticsearch_fieldnames'; export interface IEnvOptions { @@ -92,6 +93,11 @@ async function getServicesData(options: IEnvOptions) { terms: { field: SERVICE_AGENT_NAME } + }, + service_framework_name: { + terms: { + field: SERVICE_FRAMEWORK_NAME + } } } } @@ -109,7 +115,11 @@ async function getServicesData(options: IEnvOptions) { 'service.name': bucket.key as string, 'agent.name': (bucket.agent_name.buckets[0]?.key as string | undefined) || '', - 'service.environment': options.environment || null + 'service.environment': options.environment || null, + 'service.framework.name': + (bucket.service_framework_name.buckets[0]?.key as + | string + | undefined) || null }; }) || [] ); diff --git a/x-pack/plugins/apm/server/lib/services/map.ts b/x-pack/plugins/apm/server/lib/services/map.ts deleted file mode 100644 index 97bb925674e26..0000000000000 --- a/x-pack/plugins/apm/server/lib/services/map.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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 cytoscape from 'cytoscape'; -import { PromiseReturnType } from '../../../typings/common'; - -// This response right now just returns experimental data. -export type ServiceMapResponse = PromiseReturnType<typeof getServiceMap>; -export async function getServiceMap(): Promise<cytoscape.ElementDefinition[]> { - return [ - { data: { id: 'client', agentName: 'js-base' } }, - { data: { id: 'opbeans-node', agentName: 'nodejs' } }, - { data: { id: 'opbeans-python', agentName: 'python' } }, - { data: { id: 'opbeans-java', agentName: 'java' } }, - { data: { id: 'opbeans-ruby', agentName: 'ruby' } }, - { data: { id: 'opbeans-go', agentName: 'go' } }, - { data: { id: 'opbeans-go-2', agentName: 'go' } }, - { data: { id: 'opbeans-dotnet', agentName: 'dotnet' } }, - { data: { id: 'database', agentName: 'database' } }, - { data: { id: 'external API', agentName: 'external' } }, - - { - data: { - id: 'opbeans-client~opbeans-node', - source: 'client', - target: 'opbeans-node' - } - }, - { - data: { - id: 'opbeans-client~opbeans-python', - source: 'client', - target: 'opbeans-python' - } - }, - { - data: { - id: 'opbeans-python~opbeans-go', - source: 'opbeans-python', - target: 'opbeans-go' - } - }, - { - data: { - id: 'opbeans-python~opbeans-go-2', - source: 'opbeans-python', - target: 'opbeans-go-2' - } - }, - { - data: { - id: 'opbeans-python~opbeans-dotnet', - source: 'opbeans-python', - target: 'opbeans-dotnet' - } - }, - { - data: { - id: 'opbeans-node~opbeans-java', - source: 'opbeans-node', - target: 'opbeans-java' - } - }, - { - data: { - id: 'opbeans-node~database', - source: 'opbeans-node', - target: 'database' - } - }, - { - data: { - id: 'opbeans-go-2~opbeans-ruby', - source: 'opbeans-go-2', - target: 'opbeans-ruby' - } - }, - { - data: { - id: 'opbeans-go-2~external API', - source: 'opbeans-go-2', - target: 'external API' - } - } - ]; -} From ffab68d01bd4cf1abee7a8278329f30dada065c4 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Mon, 2 Mar 2020 19:32:04 -0500 Subject: [PATCH 26/26] [Endpoint] Alert Details Overview (#58412) --- x-pack/plugins/endpoint/common/types.ts | 144 ++++++++++++++--- .../endpoint/store/alerts/action.ts | 9 +- .../store/alerts/alert_details.test.ts | 65 ++++++++ .../endpoint/store/alerts/middleware.ts | 11 +- .../store/alerts/mock_alert_result_list.ts | 153 ++++++++++++++++-- .../endpoint/store/alerts/reducer.ts | 6 + .../endpoint/store/alerts/selectors.ts | 25 ++- .../public/applications/endpoint/types.ts | 11 +- .../endpoint/view/alerts/details/index.ts | 7 + .../details/metadata/file_accordion.tsx | 80 +++++++++ .../details/metadata/general_accordion.tsx | 68 ++++++++ .../details/metadata/hash_accordion.tsx | 49 ++++++ .../details/metadata/host_accordion.tsx | 55 +++++++ .../view/alerts/details/metadata/index.ts | 12 ++ .../metadata/source_process_accordion.tsx | 97 +++++++++++ .../source_process_token_accordion.tsx | 45 ++++++ .../view/alerts/details/overview/index.tsx | 94 +++++++++++ .../details/overview/metadata_panel.tsx | 40 +++++ .../endpoint/view/alerts/formatted_date.tsx | 22 +++ .../endpoint/view/alerts/index.test.tsx | 3 - .../endpoint/view/alerts/index.tsx | 97 ++++++----- .../endpoint/view/alerts/resolver.tsx | 1 + 22 files changed, 986 insertions(+), 108 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_details.test.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/index.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/file_accordion.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/hash_accordion.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/host_accordion.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/index.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_accordion.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_token_accordion.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/metadata_panel.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/formatted_date.tsx diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index 6d904fda6f747..d804350a9002d 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -96,6 +96,59 @@ export interface EndpointResultList { request_page_index: number; } +export interface OSFields { + full: string; + name: string; + version: string; + variant: string; +} +export interface HostFields { + id: string; + hostname: string; + ip: string[]; + mac: string[]; + os: OSFields; +} +export interface HashFields { + md5: string; + sha1: string; + sha256: string; +} +export interface MalwareClassifierFields { + identifier: string; + score: number; + threshold: number; + version: string; +} +export interface PrivilegesFields { + description: string; + name: string; + enabled: boolean; +} +export interface ThreadFields { + id: number; + service_name: string; + start: number; + start_address: number; + start_address_module: string; +} +export interface DllFields { + pe: { + architecture: string; + imphash: string; + }; + code_signature: { + subject_name: string; + trusted: boolean; + }; + compile_time: number; + hash: HashFields; + malware_classifier: MalwareClassifierFields; + mapped_address: number; + mapped_size: number; + path: string; +} + /** * Describes an Alert Event. * Should be in line with ECS schema. @@ -109,26 +162,78 @@ export type AlertEvent = Immutable<{ event: { id: string; action: string; + category: string; + kind: string; + dataset: string; + module: string; + type: string; }; - file_classification: { - malware_classification: { - score: number; + process: { + code_signature: { + subject_name: string; + trusted: boolean; }; - }; - process?: { - unique_pid: number; + command_line: string; + domain: string; pid: number; + ppid: number; + entity_id: string; + parent: { + pid: number; + entity_id: string; + }; + name: string; + hash: HashFields; + pe: { + imphash: string; + }; + executable: string; + sid: string; + start: number; + malware_classifier: MalwareClassifierFields; + token: { + domain: string; + type: string; + user: string; + sid: string; + integrity_level: number; + integrity_level_name: string; + privileges: PrivilegesFields[]; + }; + thread: ThreadFields[]; + uptime: number; + user: string; }; - host: { - hostname: string; - ip: string; - os: { - name: string; + file: { + owner: string; + name: string; + path: string; + accessed: number; + mtime: number; + created: number; + size: number; + hash: HashFields; + pe: { + imphash: string; + }; + code_signature: { + trusted: boolean; + subject_name: string; }; + malware_classifier: { + features: { + data: { + buffer: string; + decompressed_size: number; + encoding: string; + }; + }; + } & MalwareClassifierFields; + temp_file_path: string; }; + host: HostFields; thread: {}; - endpoint?: {}; - endgame?: {}; + dll: DllFields[]; }>; /** @@ -161,18 +266,7 @@ export interface EndpointMetadata { id: string; name: string; }; - host: { - id: string; - hostname: string; - ip: string[]; - mac: string[]; - os: { - name: string; - full: string; - version: string; - variant: string; - }; - }; + host: HostFields; } /** diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/action.ts index a628a95003a7f..6c6310a7349ed 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/action.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Immutable } from '../../../../../common/types'; +import { Immutable, AlertData } from '../../../../../common/types'; import { AlertListData } from '../../types'; interface ServerReturnedAlertsData { @@ -12,4 +12,9 @@ interface ServerReturnedAlertsData { readonly payload: Immutable<AlertListData>; } -export type AlertAction = ServerReturnedAlertsData; +interface ServerReturnedAlertDetailsData { + readonly type: 'serverReturnedAlertDetailsData'; + readonly payload: Immutable<AlertData>; +} + +export type AlertAction = ServerReturnedAlertsData | ServerReturnedAlertDetailsData; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_details.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_details.test.ts new file mode 100644 index 0000000000000..4edc31831eb14 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_details.test.ts @@ -0,0 +1,65 @@ +/* + * 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 { Store, createStore, applyMiddleware } from 'redux'; +import { History } from 'history'; +import { alertListReducer } from './reducer'; +import { AlertListState } from '../../types'; +import { alertMiddlewareFactory } from './middleware'; +import { AppAction } from '../action'; +import { coreMock } from 'src/core/public/mocks'; +import { createBrowserHistory } from 'history'; + +describe('alert details tests', () => { + let store: Store<AlertListState, AppAction>; + let coreStart: ReturnType<typeof coreMock.createStart>; + let history: History<never>; + /** + * A function that waits until a selector returns true. + */ + let selectorIsTrue: (selector: (state: AlertListState) => boolean) => Promise<void>; + beforeEach(() => { + coreStart = coreMock.createStart(); + history = createBrowserHistory(); + const middleware = alertMiddlewareFactory(coreStart); + store = createStore(alertListReducer, applyMiddleware(middleware)); + + selectorIsTrue = async selector => { + // If the selector returns true, we're done + while (selector(store.getState()) !== true) { + // otherwise, wait til the next state change occurs + await new Promise(resolve => { + const unsubscribe = store.subscribe(() => { + unsubscribe(); + resolve(); + }); + }); + } + }; + }); + describe('when the user is on the alert list page with a selected alert in the url', () => { + beforeEach(() => { + const firstResponse: Promise<unknown> = Promise.resolve(1); + const secondResponse: Promise<unknown> = Promise.resolve(2); + coreStart.http.get.mockReturnValueOnce(firstResponse).mockReturnValueOnce(secondResponse); + + // Simulates user navigating to the /alerts page + store.dispatch({ + type: 'userChangedUrl', + payload: { + ...history.location, + pathname: '/alerts', + search: '?selected_alert=q9ncfh4q9ctrmc90umcq4', + }, + }); + }); + + it('should return alert details data', async () => { + // wait for alertDetails to be defined + await selectorIsTrue(state => state.alertDetails !== undefined); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts index 76a6867418bd8..2cb381e901b4e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertResultList } from '../../../../../common/types'; +import { AlertResultList, AlertData } from '../../../../../common/types'; import { AppAction } from '../action'; import { MiddlewareFactory, AlertListState } from '../../types'; -import { isOnAlertPage, apiQueryParams } from './selectors'; +import { isOnAlertPage, apiQueryParams, hasSelectedAlert, uiQueryParams } from './selectors'; export const alertMiddlewareFactory: MiddlewareFactory<AlertListState> = coreStart => { return api => next => async (action: AppAction) => { @@ -19,5 +19,12 @@ export const alertMiddlewareFactory: MiddlewareFactory<AlertListState> = coreSta }); api.dispatch({ type: 'serverReturnedAlertsData', payload: response }); } + if (action.type === 'userChangedUrl' && isOnAlertPage(state) && hasSelectedAlert(state)) { + const uiParams = uiQueryParams(state); + const response: AlertData = await coreStart.http.get( + `/api/endpoint/alerts/${uiParams.selected_alert}` + ); + api.dispatch({ type: 'serverReturnedAlertDetailsData', payload: response }); + } }; }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts index 8eadb3e7fb3df..7db94fc9d4266 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts @@ -32,29 +32,152 @@ export const mockAlertResultList: (options?: { id: 'ced9c68e-b94a-4d66-bb4c-6106514f0a2f', version: '3.0.0', }, - event: { - id: '2f1c0928-3876-4e11-acbb-9199257c7b1c', - action: 'open', - }, - file_classification: { - malware_classification: { - score: 3, - }, - }, - process: { - pid: 107, - unique_pid: 1, - }, host: { + id: 'xrctvybuni', hostname: 'HD-c15-bc09190a', - ip: '10.179.244.14', + ip: ['10.179.244.14'], + mac: ['xsertcyvbunimkn56edtyf'], os: { - name: 'Windows', + full: 'Windows 10', + name: 'windows', + version: '10', + variant: '3', }, }, thread: {}, prev: null, next: null, + event: { + id: '2f1c0928-3876-4e11-acbb-9199257c7b1c', + action: 'creation', + category: 'malware', + dataset: 'endpoint', + kind: 'alert', + module: 'endpoint', + type: 'creation', + }, + file: { + accessed: 1542789400, + created: 1542789400, + hash: { + md5: '4ace3baaa509d08510405e1b169e325b', + sha1: '27fb21cf5db95ffca43b234affa99becc4023b9d', + sha256: '6ed1c836dbf099be7845bdab7671def2c157643761b52251e04e9b6ee109ec75', + }, + pe: { + imphash: '835d619dfdf3cc727cebd91300ab3462', + }, + mtime: 1542789400, + owner: 'Administrators', + name: 'test name', + path: 'C:\\Windows\\TEMP\\tmp0000008f\\tmp00001be5', + size: 188416, + code_signature: { + subject_name: 'Cybereason Inc', + trusted: false, + }, + malware_classifier: { + features: { + data: { + buffer: + 'eAHtnU1oHHUUwHsQ7MGDiIIUD4sH8WBBxJtopiLoUY0pYo2ZTbJJ0yQ17m4+ms/NRzeVWpuUWCL4sWlEYvFQ8KJQ6NCTEA8eRD30sIo3PdSriLi7837Pko3LbHZ2M5m+XObHm/d/X////83O7jCZvzacHBpPplNdfalkdjSdyty674Ft59dN71Dpb9v5eKh8LMEHjsCF2wIfVlRKsHROYPGkQO5+gY2vBSYYdWZFYGwEO/cITHMqkxPYnBBY+07gtCuQ9gSGigJ5lPPYGXcE+jA4z3Ad1ZtAUiDUyrEEPYzqRnIKgxd/Rgc7gygPo5wn95PouN7OeEYJ1UXiJgRmvscgp/LOziIkkSyT+xRVnXhZ4DKh5goCkzidRHkGO4uvCyw9LDDtCay8ILCAzrJOJaGuZwUuvSewivJVIPsklq8JbL4qMJsTSCcExrGs83WKU295ZFo5lr2TaZbcUw5FeJy8tgTeLpCy2iGeS67ABXzlgbEi1UC5FxcZnA4y/CLK82Qxi847FGGZRTLsCUxR1aWEwOp1AmOjDRYYzgwusL9WfqBiGJxnVAanixTq7Dp22LBdlWMJzlOx8wmBK2Rx5WmBLJIRwtAijOQE+ooCb2B5xBOYRtlfNeXpLpA7oyZRTqHzGenkmIJPnhBIMrzTwSA6H93CO5l+c1NA99f6IwLH8fUKdjTmDpTbgS50+gGVnECnE4PpooC2guPoaPADSHrcncNHmEHtAFkq3+EI+A37zsrrTvH3WTkvJLoOTyBp10wx2JcgVCRahA4NrICE4a+hrMXsA3qAHItW188E8ejO7XV3eh/KCYwxlamEwCgL8lN2wTntfrhY/U0g/5KAdvUpT+AszWqBdqH7VLeeZrExK9Cv1UgIDKA8g/cx7QAEP+AhAfRaMKB2HOJh+BSFSqKjSytNGBlc6PrpxvK7lCVDxbSG3Z7AhCMwx6gelwgLAltXBXJUTH29j+U1LHdipx/QprfKfGnF0sBpdBYxmEQyTzW0h6/0khcuhhJYRufym+i4VKMocJMs/KvfoW3/UJb4PeZOSZVONThZz4djP/75TAXa/CVfOvX3RgVLIDreLPN1pP1osW7lGmHsEhjBOzf+EPBE4vndvWz5xb/cChxGcv1LAb+tluALKnZ47isf1MXvz1ZMlsCXbXtPceqhrcp1ps6YHwQeBXLEPCf7q23tl9uJui0bGBgYRAccv7uXr/g5Af+2oNTrpgTa/vnpjBvpLAwM4gRBPvIZGBgYGBgYGBgYGBgYGBgYGBgYGBgYNAOc9oMXs4GBgYFBcNBnww5QzDXgRtPSaZ5lg/itsRaslgZ3bnWEEVnhMetIBwiiVnlbCbWrEftrt11zdwWnseFW1QO63w1is3ptD1pV9xG0t+zvfUrzrvh380qwXWAVCw6h78GIfG7ZlzltXu6hd+y92fECRFhjuH3bXG8N43oXEHperdzvUbteaDxhVTUeq25fqhG1X6Ai8mtF6BDXz2wR+dzSgg4Qsxls5T11XMG+82y8GkG+b7kL69xg7mF1SFvhBgYGsYH/Xi7HE+PVkiB2jt1bNZxT+k4558jR53ydz5//1m1KOgYGBgYGBgYGEQfnsYaG2z1sdPJS79XQSu91ndobOAHCaN5vNzUk1bceQVzUpbw3iOuT+UFmR18bHrp3gyhDC56lCd1y85w2+HSNUwVhhdGC7blLf+bV/fqtvhMg1NDjCcugB1QXswbs8ekj/v1BgzFHBIIsyP+HfwFdMpzu', + decompressed_size: 27831, + encoding: 'zlib', + }, + }, + identifier: 'endpointpe', + score: 1, + threshold: 0.66, + version: '3.0.33', + }, + temp_file_path: 'C:\\Windows\\TEMP\\1bb9abfc-ca14-47b2-9f2c-10c323df42f9', + }, + process: { + pid: 1076, + ppid: 432, + entity_id: 'wertqwer', + parent: { + pid: 432, + entity_id: 'adsfsdaf', + }, + name: 'test name', + code_signature: { + subject_name: 'Cybereason Inc', + trusted: true, + }, + command_line: '"C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe"', + domain: 'NT AUTHORITY', + executable: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + hash: { + md5: '1f2d082566b0fc5f2c238a5180db7451', + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + }, + pe: { + imphash: 'c30d230b81c734e82e86e2e2fe01cd01', + }, + malware_classifier: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', + }, + thread: [ + { + id: 1652, + service_name: 'CybereasonAntiMalware', + start: 1542788400, + start_address: 8791698721056, + start_address_module: 'C:\\Program Files\\Cybereason ActiveProbe\\gzfltum.dll', + }, + ], + sid: 'S-1-5-18', + start: 1542788400, + token: { + domain: 'NT AUTHORITY', + integrity_level: 16384, + integrity_level_name: 'system', + privileges: [ + { + description: 'Replace a process level token', + enabled: false, + name: 'SeAssignPrimaryTokenPrivilege', + }, + ], + sid: 'S-1-5-18', + type: 'tokenPrimary', + user: 'SYSTEM', + }, + uptime: 1025, + user: 'SYSTEM', + }, + dll: [ + { + pe: { + architecture: 'x64', + imphash: 'c30d230b81c734e82e86e2e2fe01cd01', + }, + code_signature: { + subject_name: 'Cybereason Inc', + trusted: true, + }, + compile_time: 1534424710, + hash: { + md5: '1f2d082566b0fc5f2c238a5180db7451', + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + }, + malware_classifier: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', + }, + mapped_address: 5362483200, + mapped_size: 0, + path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + }, + ], }); } const mock: AlertResultList = { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts index 77d7397d72581..ee172fa80f1fe 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts @@ -11,6 +11,7 @@ import { AppAction } from '../action'; const initialState = (): AlertListState => { return { alerts: [], + alertDetails: undefined, pageSize: 10, pageIndex: 0, total: 0, @@ -43,6 +44,11 @@ export const alertListReducer: Reducer<AlertListState, AppAction> = ( ...state, location: action.payload, }; + } else if (action.type === 'serverReturnedAlertDetailsData') { + return { + ...state, + alertDetails: action.payload, + }; } return state; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts index f217e3cda9191..7ce7c2d08691e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts @@ -15,7 +15,7 @@ import { AlertsAPIQueryParams, CreateStructuredSelector, } from '../../types'; -import { Immutable, LegacyEndpointEvent } from '../../../../../common/types'; +import { Immutable } from '../../../../../common/types'; const createStructuredSelector: CreateStructuredSelector = createStructuredSelectorWithBadType; /** @@ -23,6 +23,8 @@ const createStructuredSelector: CreateStructuredSelector = createStructuredSelec */ export const alertListData = (state: AlertListState) => state.alerts; +export const selectedAlertDetailsData = (state: AlertListState) => state.alertDetails; + /** * Returns the alert list pagination data from state */ @@ -96,20 +98,11 @@ export const hasSelectedAlert: (state: AlertListState) => boolean = createSelect /** * Determine if the alert event is most likely compatible with LegacyEndpointEvent. */ -function isAlertEventLegacyEndpointEvent(event: { endgame?: {} }): event is LegacyEndpointEvent { - return event.endgame !== undefined && 'unique_pid' in event.endgame; -} - -export const selectedEvent: ( +export const selectedAlertIsLegacyEndpointEvent: ( state: AlertListState -) => LegacyEndpointEvent | undefined = createSelector( - uiQueryParams, - alertListData, - ({ selected_alert: selectedAlert }, alertList) => { - const found = alertList.find(alert => alert.event.id === selectedAlert); - if (!found) { - return found; - } - return isAlertEventLegacyEndpointEvent(found) ? found : undefined; +) => boolean = createSelector(selectedAlertDetailsData, function(event) { + if (event === undefined) { + return false; } -); + return 'endgame' in event; +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index 6498462a8fc06..b46785d3190e5 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -93,19 +93,22 @@ export type AlertListData = AlertResultList; export interface AlertListState { /** Array of alert items. */ - alerts: ImmutableArray<AlertData>; + readonly alerts: ImmutableArray<AlertData>; /** The total number of alerts on the page. */ - total: number; + readonly total: number; /** Number of alerts per page. */ - pageSize: number; + readonly pageSize: number; /** Page number, starting at 0. */ - pageIndex: number; + readonly pageIndex: number; /** Current location object from React Router history. */ readonly location?: Immutable<EndpointAppLocation>; + + /** Specific Alert data to be shown in the details view */ + readonly alertDetails?: Immutable<AlertData>; } /** diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/index.ts new file mode 100644 index 0000000000000..1c78309474737 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { AlertDetailsOverview } from './overview'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/file_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/file_accordion.tsx new file mode 100644 index 0000000000000..ac67e54f38779 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/file_accordion.tsx @@ -0,0 +1,80 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; +import { Immutable, AlertData } from '../../../../../../../common/types'; +import { FormattedDate } from '../../formatted_date'; + +export const FileAccordion = memo(({ alertData }: { alertData: Immutable<AlertData> }) => { + const columns = useMemo(() => { + return [ + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileName', { + defaultMessage: 'File Name', + }), + description: alertData.file.name, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.filePath', { + defaultMessage: 'File Path', + }), + description: alertData.file.path, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileSize', { + defaultMessage: 'File Size', + }), + description: alertData.file.size, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileCreated', { + defaultMessage: 'File Created', + }), + description: <FormattedDate timestamp={alertData.file.created} />, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileModified', { + defaultMessage: 'File Modified', + }), + description: <FormattedDate timestamp={alertData.file.mtime} />, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileAccessed', { + defaultMessage: 'File Accessed', + }), + description: <FormattedDate timestamp={alertData.file.accessed} />, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.signer', { + defaultMessage: 'Signer', + }), + description: alertData.file.code_signature.subject_name, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.owner', { + defaultMessage: 'Owner', + }), + description: alertData.file.owner, + }, + ]; + }, [alertData]); + + return ( + <EuiAccordion + id="alertDetailsFileAccordion" + buttonContent={i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.accordionTitles.file', + { + defaultMessage: 'File', + } + )} + paddingSize="l" + > + <EuiDescriptionList type="column" listItems={columns} /> + </EuiAccordion> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx new file mode 100644 index 0000000000000..070c78c968585 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx @@ -0,0 +1,68 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; +import { Immutable, AlertData } from '../../../../../../../common/types'; +import { FormattedDate } from '../../formatted_date'; + +export const GeneralAccordion = memo(({ alertData }: { alertData: Immutable<AlertData> }) => { + const columns = useMemo(() => { + return [ + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.alertType', { + defaultMessage: 'Alert Type', + }), + description: alertData.event.category, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.eventType', { + defaultMessage: 'Event Type', + }), + description: alertData.event.kind, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.status', { + defaultMessage: 'Status', + }), + description: 'TODO', + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.dateCreated', { + defaultMessage: 'Date Created', + }), + description: <FormattedDate timestamp={alertData['@timestamp']} />, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.malwareScore', { + defaultMessage: 'MalwareScore', + }), + description: alertData.file.malware_classifier.score, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileName', { + defaultMessage: 'File Name', + }), + description: alertData.file.name, + }, + ]; + }, [alertData]); + return ( + <EuiAccordion + id="alertDetailsAlertAccordion" + buttonContent={i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.accordionTitles.alert', + { + defaultMessage: 'Alert', + } + )} + paddingSize="l" + initialIsOpen={true} + > + <EuiDescriptionList type="column" listItems={columns} /> + </EuiAccordion> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/hash_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/hash_accordion.tsx new file mode 100644 index 0000000000000..b2be083ce8f59 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/hash_accordion.tsx @@ -0,0 +1,49 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; +import { Immutable, AlertData } from '../../../../../../../common/types'; + +export const HashAccordion = memo(({ alertData }: { alertData: Immutable<AlertData> }) => { + const columns = useMemo(() => { + return [ + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.md5', { + defaultMessage: 'MD5', + }), + description: alertData.file.hash.md5, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.sha1', { + defaultMessage: 'SHA1', + }), + description: alertData.file.hash.sha1, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.sha256', { + defaultMessage: 'SHA256', + }), + description: alertData.file.hash.sha256, + }, + ]; + }, [alertData]); + + return ( + <EuiAccordion + id="alertDetailsHashAccordion" + buttonContent={i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.accordionTitles.hash', + { + defaultMessage: 'Hash', + } + )} + paddingSize="l" + > + <EuiDescriptionList type="column" listItems={columns} /> + </EuiAccordion> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/host_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/host_accordion.tsx new file mode 100644 index 0000000000000..4108781f0a79b --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/host_accordion.tsx @@ -0,0 +1,55 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; +import { Immutable, AlertData } from '../../../../../../../common/types'; + +export const HostAccordion = memo(({ alertData }: { alertData: Immutable<AlertData> }) => { + const columns = useMemo(() => { + return [ + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.hostName', { + defaultMessage: 'Host Name', + }), + description: alertData.host.hostname, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.hostIP', { + defaultMessage: 'Host IP', + }), + description: alertData.host.ip.join(', '), + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.status', { + defaultMessage: 'Status', + }), + description: 'TODO', + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.os', { + defaultMessage: 'OS', + }), + description: alertData.host.os.name, + }, + ]; + }, [alertData]); + + return ( + <EuiAccordion + id="alertDetailsHostAccordion" + buttonContent={i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.accordionTitles.host', + { + defaultMessage: 'Host', + } + )} + paddingSize="l" + > + <EuiDescriptionList type="column" listItems={columns} /> + </EuiAccordion> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/index.ts new file mode 100644 index 0000000000000..1eb755242d701 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/index.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +export { GeneralAccordion } from './general_accordion'; +export { HostAccordion } from './host_accordion'; +export { HashAccordion } from './hash_accordion'; +export { FileAccordion } from './file_accordion'; +export { SourceProcessAccordion } from './source_process_accordion'; +export { SourceProcessTokenAccordion } from './source_process_token_accordion'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_accordion.tsx new file mode 100644 index 0000000000000..4c961ad4b4964 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_accordion.tsx @@ -0,0 +1,97 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; +import { Immutable, AlertData } from '../../../../../../../common/types'; + +export const SourceProcessAccordion = memo(({ alertData }: { alertData: Immutable<AlertData> }) => { + const columns = useMemo(() => { + return [ + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.processID', { + defaultMessage: 'Process ID', + }), + description: alertData.process.pid, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.processName', { + defaultMessage: 'Process Name', + }), + description: alertData.process.name, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.processPath', { + defaultMessage: 'Process Path', + }), + description: alertData.process.executable, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.md5', { + defaultMessage: 'MD5', + }), + description: alertData.process.hash.md5, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.sha1', { + defaultMessage: 'SHA1', + }), + description: alertData.process.hash.sha1, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.sha256', { + defaultMessage: 'SHA256', + }), + description: alertData.process.hash.sha256, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.malwareScore', { + defaultMessage: 'MalwareScore', + }), + description: alertData.process.malware_classifier.score, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.parentProcessID', { + defaultMessage: 'Parent Process ID', + }), + description: alertData.process.parent.pid, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.signer', { + defaultMessage: 'Signer', + }), + description: alertData.process.code_signature.subject_name, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.username', { + defaultMessage: 'Username', + }), + description: alertData.process.token.user, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.domain', { + defaultMessage: 'Domain', + }), + description: alertData.process.token.domain, + }, + ]; + }, [alertData]); + + return ( + <EuiAccordion + id="alertDetailsSourceProcessAccordion" + buttonContent={i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.accordionTitles.sourceProcess', + { + defaultMessage: 'Source Process', + } + )} + paddingSize="l" + > + <EuiDescriptionList type="column" listItems={columns} /> + </EuiAccordion> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_token_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_token_accordion.tsx new file mode 100644 index 0000000000000..7d75d4478afb3 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_token_accordion.tsx @@ -0,0 +1,45 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; +import { Immutable, AlertData } from '../../../../../../../common/types'; + +export const SourceProcessTokenAccordion = memo( + ({ alertData }: { alertData: Immutable<AlertData> }) => { + const columns = useMemo(() => { + return [ + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.sid', { + defaultMessage: 'SID', + }), + description: alertData.process.token.sid, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.integrityLevel', { + defaultMessage: 'Integrity Level', + }), + description: alertData.process.token.integrity_level, + }, + ]; + }, [alertData]); + + return ( + <EuiAccordion + id="alertDetailsSourceProcessTokenAccordion" + buttonContent={i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.accordionTitles.sourceProcessToken', + { + defaultMessage: 'Source Process Token', + } + )} + paddingSize="l" + > + <EuiDescriptionList type="column" listItems={columns} /> + </EuiAccordion> + ); + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx new file mode 100644 index 0000000000000..080c70ca43bae --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx @@ -0,0 +1,94 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiTitle, EuiText, EuiHealth, EuiTabbedContent } from '@elastic/eui'; +import { useAlertListSelector } from '../../hooks/use_alerts_selector'; +import * as selectors from '../../../../store/alerts/selectors'; +import { MetadataPanel } from './metadata_panel'; +import { FormattedDate } from '../../formatted_date'; +import { AlertDetailResolver } from '../../resolver'; + +export const AlertDetailsOverview = memo(() => { + const alertDetailsData = useAlertListSelector(selectors.selectedAlertDetailsData); + if (alertDetailsData === undefined) { + return null; + } + const selectedAlertIsLegacyEndpointEvent = useAlertListSelector( + selectors.selectedAlertIsLegacyEndpointEvent + ); + + const tabs = useMemo(() => { + return [ + { + id: 'overviewMetadata', + name: i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.overview', + { + defaultMessage: 'Overview', + } + ), + content: ( + <> + <EuiSpacer /> + <MetadataPanel /> + </> + ), + }, + { + id: 'overviewResolver', + name: i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.resolver', + { + defaultMessage: 'Resolver', + } + ), + content: ( + <> + <EuiSpacer /> + {selectedAlertIsLegacyEndpointEvent && <AlertDetailResolver />} + </> + ), + }, + ]; + }, [selectedAlertIsLegacyEndpointEvent]); + + return ( + <> + <section className="details-overview-summary"> + <EuiTitle size="s"> + <h3> + <FormattedMessage + id="xpack.endpoint.application.endpoint.alertDetails.overview.title" + defaultMessage="Detected Malicious File" + /> + </h3> + </EuiTitle> + <EuiSpacer /> + <EuiText> + <p> + <FormattedMessage + id="xpack.endpoint.application.endpoint.alertDetails.overview.summary" + defaultMessage="MalwareScore detected the opening of a document on {hostname} on {date}" + values={{ + hostname: alertDetailsData.host.hostname, + date: <FormattedDate timestamp={alertDetailsData['@timestamp']} />, + }} + /> + </p> + </EuiText> + <EuiSpacer /> + <EuiText> + Endpoint Status: <EuiHealth color="success">Online</EuiHealth> + </EuiText> + <EuiText>Alert Status: Open</EuiText> + <EuiSpacer /> + </section> + <EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} /> + </> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/metadata_panel.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/metadata_panel.tsx new file mode 100644 index 0000000000000..556d7bea2e310 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/metadata_panel.tsx @@ -0,0 +1,40 @@ +/* + * 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, { memo } from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { useAlertListSelector } from '../../hooks/use_alerts_selector'; +import * as selectors from '../../../../store/alerts/selectors'; +import { + GeneralAccordion, + HostAccordion, + HashAccordion, + FileAccordion, + SourceProcessAccordion, + SourceProcessTokenAccordion, +} from '../metadata'; + +export const MetadataPanel = memo(() => { + const alertDetailsData = useAlertListSelector(selectors.selectedAlertDetailsData); + if (alertDetailsData === undefined) { + return null; + } + return ( + <section className="overview-metadata-panel"> + <GeneralAccordion alertData={alertDetailsData} /> + <EuiSpacer /> + <HostAccordion alertData={alertDetailsData} /> + <EuiSpacer /> + <HashAccordion alertData={alertDetailsData} /> + <EuiSpacer /> + <FileAccordion alertData={alertDetailsData} /> + <EuiSpacer /> + <SourceProcessAccordion alertData={alertDetailsData} /> + <EuiSpacer /> + <SourceProcessTokenAccordion alertData={alertDetailsData} /> + </section> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/formatted_date.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/formatted_date.tsx new file mode 100644 index 0000000000000..731bd31b26cef --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/formatted_date.tsx @@ -0,0 +1,22 @@ +/* + * 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, { memo } from 'react'; +import { FormattedDate as ReactIntlFormattedDate } from '@kbn/i18n/react'; + +export const FormattedDate = memo(({ timestamp }: { timestamp: number }) => { + const date = new Date(timestamp); + return ( + <ReactIntlFormattedDate + value={date} + year="numeric" + month="2-digit" + day="2-digit" + hour="2-digit" + minute="2-digit" + second="2-digit" + /> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx index fe362f21a178e..aae44824c3164 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx @@ -140,9 +140,6 @@ describe('when on the alerting page', () => { it('should show the flyout', async () => { await render().findByTestId('alertDetailFlyout'); }); - it('should render resolver', async () => { - await render().findByTestId('alertResolver'); - }); describe('when the user clicks the close button on the flyout', () => { let renderResult: reactTestingLibrary.RenderResult; beforeEach(async () => { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx index 3c229484ede4e..5d405f8c6fbae 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx @@ -17,15 +17,21 @@ import { EuiFlyoutBody, EuiTitle, EuiBadge, + EuiLoadingSpinner, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageContentBody, + EuiLink, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useHistory, Link } from 'react-router-dom'; -import { FormattedDate } from 'react-intl'; +import { useHistory } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; import { urlFromQueryParams } from './url_from_query_params'; import { AlertData } from '../../../../../common/types'; import * as selectors from '../../store/alerts/selectors'; import { useAlertListSelector } from './hooks/use_alerts_selector'; -import { AlertDetailResolver } from './resolver'; +import { AlertDetailsOverview } from './details'; +import { FormattedDate } from './formatted_date'; export const AlertIndex = memo(() => { const history = useHistory(); @@ -87,7 +93,6 @@ export const AlertIndex = memo(() => { const alertListData = useAlertListSelector(selectors.alertListData); const hasSelectedAlert = useAlertListSelector(selectors.hasSelectedAlert); const queryParams = useAlertListSelector(selectors.uiQueryParams); - const selectedEvent = useAlertListSelector(selectors.selectedEvent); const onChangeItemsPerPage = useCallback( newPageSize => { @@ -119,10 +124,10 @@ export const AlertIndex = memo(() => { history.push(urlFromQueryParams(paramsWithoutSelectedAlert)); }, [history, queryParams]); - const datesForRows: Map<AlertData, Date> = useMemo(() => { + const timestampForRows: Map<AlertData, number> = useMemo(() => { return new Map( alertListData.map(alertData => { - return [alertData, new Date(alertData['@timestamp'])]; + return [alertData, alertData['@timestamp']]; }) ); }, [alertListData]); @@ -136,9 +141,11 @@ export const AlertIndex = memo(() => { const row = alertListData[rowIndex % pageSize]; if (columnId === 'alert_type') { return ( - <Link + <EuiLink data-testid="alertTypeCellLink" - to={urlFromQueryParams({ ...queryParams, selected_alert: row.event.id })} + onClick={() => + history.push(urlFromQueryParams({ ...queryParams, selected_alert: row.id })) + } > {i18n.translate( 'xpack.endpoint.application.endpoint.alerts.alertType.maliciousFileDescription', @@ -146,7 +153,7 @@ export const AlertIndex = memo(() => { defaultMessage: 'Malicious File', } )} - </Link> + </EuiLink> ); } else if (columnId === 'event_type') { return row.event.action; @@ -157,19 +164,9 @@ export const AlertIndex = memo(() => { } else if (columnId === 'host_name') { return row.host.hostname; } else if (columnId === 'timestamp') { - const date = datesForRows.get(row)!; - if (date && isFinite(date.getTime())) { - return ( - <FormattedDate - value={date} - year="numeric" - month="2-digit" - day="2-digit" - hour="2-digit" - minute="2-digit" - second="2-digit" - /> - ); + const timestamp = timestampForRows.get(row)!; + if (timestamp) { + return <FormattedDate timestamp={timestamp} />; } else { return ( <EuiBadge color="warning"> @@ -185,11 +182,11 @@ export const AlertIndex = memo(() => { } else if (columnId === 'archived') { return null; } else if (columnId === 'malware_score') { - return row.file_classification.malware_classification.score; + return row.file.malware_classifier.score; } return null; }; - }, [alertListData, datesForRows, pageSize, queryParams, total]); + }, [total, alertListData, pageSize, history, queryParams, timestampForRows]); const pagination = useMemo(() => { return { @@ -201,6 +198,16 @@ export const AlertIndex = memo(() => { }; }, [onChangeItemsPerPage, onChangePage, pageIndex, pageSize]); + const columnVisibility = useMemo( + () => ({ + visibleColumns, + setVisibleColumns, + }), + [setVisibleColumns, visibleColumns] + ); + + const selectedAlertData = useAlertListSelector(selectors.selectedAlertDetailsData); + return ( <> {hasSelectedAlert && ( @@ -215,29 +222,37 @@ export const AlertIndex = memo(() => { </EuiTitle> </EuiFlyoutHeader> <EuiFlyoutBody> - <AlertDetailResolver selectedEvent={selectedEvent} /> + {selectedAlertData ? <AlertDetailsOverview /> : <EuiLoadingSpinner size="xl" />} </EuiFlyoutBody> </EuiFlyout> )} <EuiPage data-test-subj="alertListPage" data-testid="alertListPage"> <EuiPageBody> <EuiPageContent> - <EuiDataGrid - aria-label="Alert List" - rowCount={total} - columns={columns} - columnVisibility={useMemo( - () => ({ - visibleColumns, - setVisibleColumns, - }), - [setVisibleColumns, visibleColumns] - )} - renderCellValue={renderCellValue} - pagination={pagination} - data-test-subj="alertListGrid" - data-testid="alertListGrid" - /> + <EuiPageContentHeader> + <EuiPageContentHeaderSection> + <EuiTitle size="l"> + <h1> + <FormattedMessage + id="xpack.endpoint.alertList.viewTitle" + defaultMessage="Alerts" + /> + </h1> + </EuiTitle> + </EuiPageContentHeaderSection> + </EuiPageContentHeader> + <EuiPageContentBody> + <EuiDataGrid + aria-label="Alert List" + rowCount={total} + columns={columns} + columnVisibility={columnVisibility} + renderCellValue={renderCellValue} + pagination={pagination} + data-test-subj="alertListGrid" + data-testid="alertListGrid" + /> + </EuiPageContentBody> </EuiPageContent> </EuiPageBody> </EuiPage> diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx index c7ef7f73dfe05..ec1dab45d50ab 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx @@ -18,6 +18,7 @@ export const AlertDetailResolver = styled( ({ className, selectedEvent }: { className?: string; selectedEvent?: LegacyEndpointEvent }) => { const context = useKibana<EndpointPluginServices>(); const { store } = storeFactory(context); + return ( <div className={className} data-test-subj="alertResolver" data-testid="alertResolver"> <Provider store={store}>