From 2010ec6ac4ba518fd032afb044f6ea0b47e3f587 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 27 Aug 2020 18:22:53 +0100 Subject: [PATCH 01/23] fix(NA): keystore read path on serve (#75659) Co-authored-by: Elastic Machine --- src/cli/serve/serve.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 345156b2491a1..c08f3aec64335 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -149,7 +149,7 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { ); merge(extraCliOptions); - merge(readKeystore(get('path.data'))); + merge(readKeystore()); return rawConfig; } From ebfba81ba54b4b25f07a98fc3eefe7a7927a3e02 Mon Sep 17 00:00:00 2001 From: Brent Kimmel Date: Thu, 27 Aug 2020 13:25:01 -0400 Subject: [PATCH 02/23] [Security Solution][Resolver] break/wrap for process detail (#76095) * [Security Solution][Resolver]break/wrap for process detail * add an enzyme test to check for the breakers --- .../public/resolver/mocks/resolver_tree.ts | 2 +- .../public/resolver/view/panel.test.tsx | 17 ++++++++-- .../view/panels/panel_content_utilities.tsx | 32 +++++++++++++++++++ .../resolver/view/panels/process_details.tsx | 16 +++++++--- .../view/panels/related_event_detail.tsx | 30 +---------------- 5 files changed, 60 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts index 7edf4f8071ed8..8bd5953e9cb41 100644 --- a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts @@ -177,7 +177,7 @@ export function mockTreeWithNoAncestorsAnd2Children({ const origin: ResolverEvent = mockEndpointEvent({ pid: 0, entityID: originID, - name: 'c', + name: 'c.ext', parentEntityId: 'none', timestamp: 0, }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx index 037337fb2f868..1add907ae933d 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx @@ -81,7 +81,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an }; }) ).toYieldEqualTo({ - title: 'c', + title: 'c.ext', titleIcon: 'Running Process', detailEntries: [ ['process.executable', 'executable'], @@ -94,6 +94,19 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an ], }); }); + it('should have breaking opportunities (s) in node titles to allow wrapping', async () => { + await expect( + simulator().map(() => { + const titleWrapper = simulator().testSubject('resolver:node-detail:title'); + return { + wordBreaks: titleWrapper.find('wbr').length, + }; + }) + ).toYieldEqualTo({ + // The GeneratedText component adds 1 after the period and one at the end + wordBreaks: 2, + }); + }); }); const queryStringWithFirstChildSelected = urlSearch(resolverComponentInstanceID, { @@ -174,7 +187,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an .testSubject('resolver:node-list:node-link:title') .map((node) => node.text()); }) - ).toYieldEqualTo(['c', 'd', 'e']); + ).toYieldEqualTo(['c.ext', 'd', 'e']); }); }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx index 5c7a4a476efba..19f0aa3fe1d67 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx @@ -43,6 +43,38 @@ const betaBadgeLabel = i18n.translate( } ); +/** + * A component that renders an element with breaking opportunities (``s) + * spliced into text children at word boundaries. + */ +export const GeneratedText = React.memo(function ({ children }) { + return <>{processedValue()}; + + function processedValue() { + return React.Children.map(children, (child) => { + if (typeof child === 'string') { + const valueSplitByWordBoundaries = child.split(/\b/); + + if (valueSplitByWordBoundaries.length < 2) { + return valueSplitByWordBoundaries[0]; + } + + return [ + valueSplitByWordBoundaries[0], + ...valueSplitByWordBoundaries + .splice(1) + .reduce(function (generatedTextMemo: Array, value, index) { + return [...generatedTextMemo, value, ]; + }, []), + ]; + } else { + return child; + } + }); + } +}); +GeneratedText.displayName = 'GeneratedText'; + /** * A component to keep time representations in blocks so they don't wrap * and look bad. diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx index 15711909c4c9b..e86e3e6baf4a4 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx @@ -19,7 +19,7 @@ import { FormattedMessage } from 'react-intl'; import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; import * as selectors from '../../store/selectors'; import * as event from '../../../../common/endpoint/models/event'; -import { formatDate, StyledBreadcrumbs } from './panel_content_utilities'; +import { formatDate, StyledBreadcrumbs, GeneratedText } from './panel_content_utilities'; import { processPath, processPid, @@ -39,6 +39,10 @@ const StyledDescriptionList = styled(EuiDescriptionList)` } `; +const StyledTitle = styled('h4')` + overflow-wrap: break-word; +`; + /** * A description list view of all the Metadata that goes with a particular process event, like: * Created, PID, User/Domain, etc. @@ -114,7 +118,7 @@ export const ProcessDetails = memo(function ProcessDetails({ .map((entry) => { return { ...entry, - description: String(entry.description), + description: {String(entry.description)}, }; }); @@ -163,13 +167,15 @@ export const ProcessDetails = memo(function ProcessDetails({ -

+ - {processName} -

+ + {processName} + +
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx index da4cd3c9dacad..6aacf91c56178 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx @@ -10,7 +10,7 @@ import { EuiSpacer, EuiText, EuiDescriptionList, EuiTextColor, EuiTitle } from ' import styled from 'styled-components'; import { useSelector } from 'react-redux'; import { FormattedMessage } from 'react-intl'; -import { StyledBreadcrumbs, BoldCode, StyledTime } from './panel_content_utilities'; +import { StyledBreadcrumbs, BoldCode, StyledTime, GeneratedText } from './panel_content_utilities'; import * as event from '../../../../common/endpoint/models/event'; import { ResolverEvent } from '../../../../common/endpoint/types'; import * as selectors from '../../store/selectors'; @@ -57,34 +57,6 @@ const TitleHr = memo(() => { }); TitleHr.displayName = 'TitleHR'; -const GeneratedText = React.memo(function ({ children }) { - return <>{processedValue()}; - - function processedValue() { - return React.Children.map(children, (child) => { - if (typeof child === 'string') { - const valueSplitByWordBoundaries = child.split(/\b/); - - if (valueSplitByWordBoundaries.length < 2) { - return valueSplitByWordBoundaries[0]; - } - - return [ - valueSplitByWordBoundaries[0], - ...valueSplitByWordBoundaries - .splice(1) - .reduce(function (generatedTextMemo: Array, value, index) { - return [...generatedTextMemo, value, ]; - }, []), - ]; - } else { - return child; - } - }); - } -}); -GeneratedText.displayName = 'GeneratedText'; - /** * Take description list entries and prepare them for display by * seeding with `` tags. From 165752b05f2e2e847c3204a762a44c430f58ebac Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 27 Aug 2020 10:39:44 -0700 Subject: [PATCH 03/23] [DOCS] Add monitoring.ui.logs.index (#75830) --- docs/settings/monitoring-settings.asciidoc | 43 ++++++++++++---------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 5b8fa0725d96b..d538519eefcc4 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -5,12 +5,12 @@ Monitoring settings ++++ -By default, the Monitoring application is enabled, but data collection -is disabled. When you first start {kib} monitoring, you are prompted to -enable data collection. If you are using {stack-security-features}, you must be -signed in as a user with the `cluster:manage` privilege to enable -data collection. The built-in `superuser` role has this privilege and the -built-in `elastic` user has this role. +By default, *{stack-monitor-app}* is enabled, but data collection is disabled. +When you first start {kib} monitoring, you are prompted to enable data +collection. If you are using {stack-security-features}, you must be signed in as +a user with the `cluster:manage` privilege to enable data collection. The +built-in `superuser` role has this privilege and the built-in `elastic` user has +this role. You can adjust how monitoring data is collected from {kib} and displayed in {kib} by configuring settings in the @@ -49,7 +49,7 @@ For more information, see in {kib} to the {es} monitoring cluster and to verify licensing status on the {es} monitoring cluster. + + - Every other request performed by the Stack Monitoring UI to the monitoring {es} + Every other request performed by *{stack-monitor-app}* to the monitoring {es} cluster uses the authenticated user's credentials, which must be the same on both the {es} monitoring cluster and the {es} production cluster. + + @@ -60,7 +60,7 @@ For more information, see in {kib} to the {es} monitoring cluster and to verify licensing status on the {es} monitoring cluster. + + - Every other request performed by the Stack Monitoring UI to the monitoring {es} + Every other request performed by *{stack-monitor-app}* to the monitoring {es} cluster uses the authenticated user's credentials, which must be the same on both the {es} monitoring cluster and the {es} production cluster. + + @@ -83,7 +83,7 @@ These settings control how data is collected from {kib}. |=== | `monitoring.kibana.collection.enabled` | Set to `true` (default) to enable data collection from the {kib} NodeJS server - for {kib} Dashboards to be featured in the Monitoring. + for {kib} dashboards to be featured in *{stack-monitor-app}*. | `monitoring.kibana.collection.interval` | Specifies the number of milliseconds to wait in between data sampling on the @@ -96,16 +96,26 @@ These settings control how data is collected from {kib}. [[monitoring-ui-settings]] ==== Monitoring UI settings -These settings adjust how the {kib} Monitoring page displays monitoring data. +These settings adjust how *{stack-monitor-app}* displays monitoring data. However, the defaults work best in most circumstances. For more information about configuring {kib}, see -{kibana-ref}/settings.html[Setting Kibana Server Properties]. +{kibana-ref}/settings.html[Setting {kib} server properties]. [cols="2*<"] |=== | `monitoring.ui.elasticsearch.logFetchCount` - | Specifies the number of log entries to display in the Monitoring UI. Defaults to - `10`. The maximum value is `50`. + | Specifies the number of log entries to display in *{stack-monitor-app}*. + Defaults to `10`. The maximum value is `50`. + +| `monitoring.ui.enabled` + | Set to `false` to hide *{stack-monitor-app}*. The monitoring back-end + continues to run as an agent for sending {kib} stats to the monitoring + cluster. Defaults to `true`. + +| `monitoring.ui.logs.index` + | Specifies the name of the indices that are shown on the + <> page in *{stack-monitor-app}*. The default value + is `filebeat-*`. | `monitoring.ui.max_bucket_size` | Specifies the number of term buckets to return out of the overall terms list when @@ -120,18 +130,13 @@ about configuring {kib}, see `monitoring.ui.collection.interval` in `elasticsearch.yml`, use the same value in this setting. -| `monitoring.ui.enabled` - | Set to `false` to hide the Monitoring UI in {kib}. The monitoring back-end - continues to run as an agent for sending {kib} stats to the monitoring - cluster. Defaults to `true`. - |=== [float] [[monitoring-ui-cgroup-settings]] ===== Monitoring UI container settings -The Monitoring UI exposes the Cgroup statistics that we collect for you to make +*{stack-monitor-app}* exposes the Cgroup statistics that we collect for you to make better decisions about your container performance, rather than guessing based on the overall machine performance. If you are not running your applications in a container, then Cgroup statistics are not useful. From c31acce6499932fd39babcb4a984f58260df4239 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 27 Aug 2020 13:54:09 -0400 Subject: [PATCH 04/23] Fix more broken usages of `bulkCreate` (#76005) --- .../tutorial/saved_objects_installer.js | 9 +++++--- .../tutorial/saved_objects_installer.test.js | 21 +++++++++++++++++++ .../models/data_recognizer/data_recognizer.ts | 7 ++++--- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/plugins/home/public/application/components/tutorial/saved_objects_installer.js b/src/plugins/home/public/application/components/tutorial/saved_objects_installer.js index 790c6d9c2574e..dd63827c38c5d 100644 --- a/src/plugins/home/public/application/components/tutorial/saved_objects_installer.js +++ b/src/plugins/home/public/application/components/tutorial/saved_objects_installer.js @@ -62,9 +62,12 @@ class SavedObjectsInstallerUi extends React.Component { let resp; try { - resp = await this.props.bulkCreate(this.props.savedObjects, { - overwrite: this.state.overwrite, - }); + // Filter out the saved object version field, if present, to avoid inadvertently triggering optimistic concurrency control. + const objectsToCreate = this.props.savedObjects.map( + // eslint-disable-next-line no-unused-vars + ({ version, ...savedObject }) => savedObject + ); + resp = await this.props.bulkCreate(objectsToCreate, { overwrite: this.state.overwrite }); } catch (error) { if (!this._isMounted) { return; diff --git a/src/plugins/home/public/application/components/tutorial/saved_objects_installer.test.js b/src/plugins/home/public/application/components/tutorial/saved_objects_installer.test.js index 6cc02184fbc16..e7b7d8ed1d7fd 100644 --- a/src/plugins/home/public/application/components/tutorial/saved_objects_installer.test.js +++ b/src/plugins/home/public/application/components/tutorial/saved_objects_installer.test.js @@ -79,4 +79,25 @@ describe('bulkCreate', () => { expect(component).toMatchSnapshot(); }); + + test('should filter out saved object version before calling bulkCreate', async () => { + const bulkCreateMock = jest.fn().mockResolvedValue({ + savedObjects: [savedObject], + }); + const component = mountWithIntl( + + ); + + findTestSubject(component, 'loadSavedObjects').simulate('click'); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(bulkCreateMock).toHaveBeenCalledWith([savedObject], expect.any(Object)); + }); }); diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index cc42a545c11e2..206baacd98322 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -20,6 +20,7 @@ import { getAuthorizationHeader } from '../../lib/request_authorization'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; import { KibanaObjects, + KibanaObjectConfig, ModuleDataFeed, ModuleJob, Module, @@ -100,7 +101,7 @@ interface ObjectExistResponse { id: string; type: string; exists: boolean; - savedObject?: any; + savedObject?: { id: string; type: string; attributes: KibanaObjectConfig }; } interface SaveResults { @@ -678,14 +679,14 @@ export class DataRecognizer { let results = { saved_objects: [] as any[] }; const filteredSavedObjects = objectExistResults .filter((o) => o.exists === false) - .map((o) => o.savedObject); + .map((o) => o.savedObject!); if (filteredSavedObjects.length) { results = await this.savedObjectsClient.bulkCreate( // Add an empty migrationVersion attribute to each saved object to ensure // it is automatically migrated to the 7.0+ format with a references attribute. filteredSavedObjects.map((doc) => ({ ...doc, - migrationVersion: doc.migrationVersion || {}, + migrationVersion: {}, })) ); } From 1bd8f4127531a5b0426416b4ee264f1af6166a1f Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 27 Aug 2020 13:50:15 -0500 Subject: [PATCH 05/23] [ML] Add indicator if there are stopped partitions in categorization job wizard (#75709) --- .../common/results_loader/results_loader.ts | 4 + .../category_stopped_partitions.tsx | 137 ++++++++++++++++++ .../metric_selection_summary.tsx | 2 + 3 files changed, 143 insertions(+) create mode 100644 x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts index 110b031cd1dc0..2b250b9622286 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts @@ -126,6 +126,10 @@ export class ResultsLoader { this._results$.next(this._results); } + public get results$() { + return this._results$; + } + public subscribeToResults(func: ResultsSubscriber) { return this._results$.subscribe(func); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx new file mode 100644 index 0000000000000..5e28a2f7c6975 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx @@ -0,0 +1,137 @@ +/* + * 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, { FC, useContext, useEffect, useState, useMemo, useCallback } from 'react'; +import { EuiBasicTable, EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { from } from 'rxjs'; +import { switchMap, takeWhile, tap } from 'rxjs/operators'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { CategorizationJobCreator } from '../../../../../common/job_creator'; +import { ml } from '../../../../../../../services/ml_api_service'; +import { extractErrorProperties } from '../../../../../../../../../common/util/errors'; + +const NUMBER_OF_PREVIEW = 5; +export const CategoryStoppedPartitions: FC = () => { + const { jobCreator: jc, resultsLoader } = useContext(JobCreatorContext); + const jobCreator = jc as CategorizationJobCreator; + const [tableRow, setTableRow] = useState>([]); + const [stoppedPartitionsError, setStoppedPartitionsError] = useState(); + + const columns = useMemo( + () => [ + { + field: 'partitionName', + name: i18n.translate( + 'xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsPreviewColumnName', + { + defaultMessage: 'Stopped partition names', + } + ), + render: (partition: any) => ( + + {partition} + + ), + }, + ], + [] + ); + + const loadCategoryStoppedPartitions = useCallback(async () => { + try { + const { jobs } = await ml.results.getCategoryStoppedPartitions([jobCreator.jobId]); + + if ( + !Array.isArray(jobs) && // if jobs is object of jobId: [partitions] + Array.isArray(jobs[jobCreator.jobId]) && + jobs[jobCreator.jobId].length > 0 + ) { + return jobs[jobCreator.jobId]; + } + } catch (e) { + const error = extractErrorProperties(e); + // might get 404 because job has not been created yet and that's ok + if (error.statusCode !== 404) { + setStoppedPartitionsError(error.message); + } + } + }, [jobCreator.jobId]); + + useEffect(() => { + // only need to run this check if jobCreator.perPartitionStopOnWarn is turned on + if (jobCreator.perPartitionCategorization && jobCreator.perPartitionStopOnWarn) { + // subscribe to result updates + const resultsSubscription = resultsLoader.results$ + .pipe( + switchMap(() => { + return from(loadCategoryStoppedPartitions()); + }), + tap((results) => { + if (Array.isArray(results)) { + setTableRow( + results.slice(0, NUMBER_OF_PREVIEW).map((partitionName) => ({ + partitionName, + })) + ); + } + }), + takeWhile((results) => { + return !results || (Array.isArray(results) && results.length <= NUMBER_OF_PREVIEW); + }) + ) + .subscribe(); + return () => resultsSubscription.unsubscribe(); + } + }, []); + + return ( + <> + {stoppedPartitionsError && ( + <> + + + } + /> + + )} + {Array.isArray(tableRow) && tableRow.length > 0 && ( + <> + +
+ +
+ + + } + /> + + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx index 768d8c394fb8f..9f66fb95b53a8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx @@ -11,6 +11,7 @@ import { Results, Anomaly } from '../../../../../common/results_loader'; import { LineChartPoint } from '../../../../../common/chart_loader'; import { EventRateChart } from '../../../charts/event_rate_chart'; import { TopCategories } from './top_categories'; +import { CategoryStoppedPartitions } from './category_stopped_partitions'; const DTR_IDX = 0; @@ -73,6 +74,7 @@ export const CategorizationDetectorsSummary: FC = () => { fadeChart={jobIsRunning} /> + ); }; From a7b0f7a102f4785d6c9cefbbdc07c1049087dfe0 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 27 Aug 2020 12:03:20 -0700 Subject: [PATCH 06/23] [Enterprise Search] Add reusable FlashMessages helper (#75901) * Set up basic shared FlashMessages & FlashMessagesLogic * Add top-level FlashMessagesProvider and history listener - This ensures that: - Our FlashMessagesLogic is a global state that persists throughout the entire app and only unmounts when the app itself does (allowing for persistent messages if needed) - history.listen enables the same behavior as previously, where flash messages would be cleared between page views * Set up queued messages that appear on page nav/load * [AS] Add FlashMessages component to Engines Overview + add Kea/Redux context/state to mountWithContext (in order for tests to pass) * Fix missing type exports, replace previous IFlashMessagesProps * [WS] Remove flashMessages state in OverviewLogic - in favor of either connecting it or using FlashMessagesLogic directly in the future * PR feedback: DRY out EUI callout color type def * PR Feedback: make flashMessages method names more explicit * PR Feedback: Shorter FlashMessagesLogic type names * PR feedback: Typing Co-authored-by: Byron Hulcher Co-authored-by: Byron Hulcher --- .../__mocks__/mount_with_context.mock.tsx | 9 +- .../engine_overview/engine_overview.tsx | 2 + .../public/applications/index.tsx | 2 + .../flash_messages/flash_messages.test.tsx | 64 +++++++++ .../shared/flash_messages/flash_messages.tsx | 43 ++++++ .../flash_messages_logic.test.ts | 136 ++++++++++++++++++ .../flash_messages/flash_messages_logic.ts | 87 +++++++++++ .../flash_messages_provider.test.tsx | 46 ++++++ .../flash_messages_provider.tsx | 30 ++++ .../shared/flash_messages/index.ts | 14 ++ .../public/applications/shared/types.ts | 9 +- .../overview/__mocks__/overview_logic.mock.ts | 1 - .../views/overview/overview_logic.test.ts | 9 -- .../views/overview/overview_logic.ts | 11 +- 14 files changed, 434 insertions(+), 29 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx index 9f8fda856eed6..826e0482acef7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx @@ -8,6 +8,10 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mount, ReactWrapper } from 'enzyme'; +import { Provider } from 'react-redux'; +import { Store } from 'redux'; +import { getContext, resetContext } from 'kea'; + import { I18nProvider } from '@kbn/i18n/react'; import { KibanaContext } from '../'; import { mockKibanaContext } from './kibana_context.mock'; @@ -24,11 +28,14 @@ import { mockLicenseContext } from './license_context.mock'; * const wrapper = mountWithContext(, { config: { host: 'someOverride' } }); */ export const mountWithContext = (children: React.ReactNode, context?: object) => { + resetContext({ createStore: true }); + const store = getContext().store as Store; + return mount( - {children} + {children} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx index 74bcd9aeafb28..c3b47b2b585bd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -16,6 +16,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; +import { FlashMessages } from '../../../shared/flash_messages'; import { LicenseContext, ILicenseContext, hasPlatinumLicense } from '../../../shared/licensing'; import { KibanaContext, IKibanaContext } from '../../../index'; @@ -88,6 +89,7 @@ export const EngineOverview: React.FC = () => { +

diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 60e4cedf413f2..a54295548004a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -22,6 +22,7 @@ import { } from 'src/core/public'; import { ClientConfigType, ClientData, PluginsSetup } from '../plugin'; import { LicenseProvider } from './shared/licensing'; +import { FlashMessagesProvider } from './shared/flash_messages'; import { HttpProvider } from './shared/http'; import { IExternalUrl } from './shared/enterprise_search_url'; import { IInitialAppData } from '../../common/types'; @@ -69,6 +70,7 @@ export const renderApp = ( + diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx new file mode 100644 index 0000000000000..59bb7ee5b9625 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx @@ -0,0 +1,64 @@ +/* + * 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 '../../__mocks__/kea.mock'; + +import { useValues } from 'kea'; +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiCallOut } from '@elastic/eui'; + +import { FlashMessages } from './'; + +describe('FlashMessages', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('does not render if no messages exist', () => { + (useValues as jest.Mock).mockImplementationOnce(() => ({ messages: [] })); + + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders an array of flash messages & types', () => { + const mockMessages = [ + { type: 'success', message: 'Hello world!!' }, + { + type: 'error', + message: 'Whoa nelly!', + description:
Something went wrong
, + }, + { type: 'info', message: 'Everything is fine, nothing is ruined' }, + { type: 'warning', message: 'Uh oh' }, + { type: 'info', message: 'Testing multiples of same type' }, + ]; + (useValues as jest.Mock).mockImplementationOnce(() => ({ messages: mockMessages })); + + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(5); + expect(wrapper.find(EuiCallOut).first().prop('color')).toEqual('success'); + expect(wrapper.find('[data-test-subj="error"]')).toHaveLength(1); + expect(wrapper.find(EuiCallOut).last().prop('iconType')).toEqual('iInCircle'); + }); + + it('renders any children', () => { + (useValues as jest.Mock).mockImplementationOnce(() => ({ messages: [{ type: 'success' }] })); + + const wrapper = shallow( + + + + ); + + expect(wrapper.find('[data-test-subj="testing"]').text()).toContain('Some action'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx new file mode 100644 index 0000000000000..5a909a287795c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx @@ -0,0 +1,43 @@ +/* + * 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, { Fragment } from 'react'; +import { useValues } from 'kea'; +import { EuiCallOut, EuiCallOutProps, EuiSpacer } from '@elastic/eui'; + +import { FlashMessagesLogic, IFlashMessagesValues } from './flash_messages_logic'; + +const FLASH_MESSAGE_TYPES = { + success: { color: 'success' as EuiCallOutProps['color'], icon: 'check' }, + info: { color: 'primary' as EuiCallOutProps['color'], icon: 'iInCircle' }, + warning: { color: 'warning' as EuiCallOutProps['color'], icon: 'alert' }, + error: { color: 'danger' as EuiCallOutProps['color'], icon: 'cross' }, +}; + +export const FlashMessages: React.FC = ({ children }) => { + const { messages } = useValues(FlashMessagesLogic) as IFlashMessagesValues; + + // If we have no messages to display, do not render the element at all + if (!messages.length) return null; + + return ( +
+ {messages.map(({ type, message, description }, index) => ( + + + {description} + + + + ))} + {children} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts new file mode 100644 index 0000000000000..136912847baa9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts @@ -0,0 +1,136 @@ +/* + * 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 { resetContext } from 'kea'; + +import { FlashMessagesLogic, IFlashMessage } from './flash_messages_logic'; + +describe('FlashMessagesLogic', () => { + const DEFAULT_VALUES = { + messages: [], + queuedMessages: [], + historyListener: null, + }; + + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + }); + + it('has expected default values', () => { + FlashMessagesLogic.mount(); + expect(FlashMessagesLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('setFlashMessages()', () => { + it('sets an array of messages', () => { + const messages: IFlashMessage[] = [ + { type: 'success', message: 'Hello world!!' }, + { type: 'error', message: 'Whoa nelly!', description: 'Uh oh' }, + { type: 'info', message: 'Everything is fine, nothing is ruined' }, + ]; + + FlashMessagesLogic.mount(); + FlashMessagesLogic.actions.setFlashMessages(messages); + + expect(FlashMessagesLogic.values.messages).toEqual(messages); + }); + + it('automatically converts to an array if a single message obj is passed in', () => { + const message = { type: 'success', message: 'I turn into an array!' } as IFlashMessage; + + FlashMessagesLogic.mount(); + FlashMessagesLogic.actions.setFlashMessages(message); + + expect(FlashMessagesLogic.values.messages).toEqual([message]); + }); + }); + + describe('clearFlashMessages()', () => { + it('sets messages back to an empty array', () => { + FlashMessagesLogic.mount(); + FlashMessagesLogic.actions.setFlashMessages('test' as any); + FlashMessagesLogic.actions.clearFlashMessages(); + + expect(FlashMessagesLogic.values.messages).toEqual([]); + }); + }); + + describe('setQueuedMessages()', () => { + it('sets an array of messages', () => { + const queuedMessage: IFlashMessage = { type: 'error', message: 'You deleted a thing' }; + + FlashMessagesLogic.mount(); + FlashMessagesLogic.actions.setQueuedMessages(queuedMessage); + + expect(FlashMessagesLogic.values.queuedMessages).toEqual([queuedMessage]); + }); + }); + + describe('clearQueuedMessages()', () => { + it('sets queued messages back to an empty array', () => { + FlashMessagesLogic.mount(); + FlashMessagesLogic.actions.setQueuedMessages('test' as any); + FlashMessagesLogic.actions.clearQueuedMessages(); + + expect(FlashMessagesLogic.values.queuedMessages).toEqual([]); + }); + }); + + describe('history listener logic', () => { + describe('setHistoryListener()', () => { + it('sets the historyListener value', () => { + FlashMessagesLogic.mount(); + FlashMessagesLogic.actions.setHistoryListener('test' as any); + + expect(FlashMessagesLogic.values.historyListener).toEqual('test'); + }); + }); + + describe('listenToHistory()', () => { + it('listens for history changes and clears messages on change', () => { + FlashMessagesLogic.mount(); + FlashMessagesLogic.actions.setQueuedMessages(['queuedMessages'] as any); + jest.spyOn(FlashMessagesLogic.actions, 'clearFlashMessages'); + jest.spyOn(FlashMessagesLogic.actions, 'setFlashMessages'); + jest.spyOn(FlashMessagesLogic.actions, 'clearQueuedMessages'); + jest.spyOn(FlashMessagesLogic.actions, 'setHistoryListener'); + + const mockListener = jest.fn(() => jest.fn()); + const history = { listen: mockListener } as any; + FlashMessagesLogic.actions.listenToHistory(history); + + expect(mockListener).toHaveBeenCalled(); + expect(FlashMessagesLogic.actions.setHistoryListener).toHaveBeenCalled(); + + const mockHistoryChange = (mockListener.mock.calls[0] as any)[0]; + mockHistoryChange(); + expect(FlashMessagesLogic.actions.clearFlashMessages).toHaveBeenCalled(); + expect(FlashMessagesLogic.actions.setFlashMessages).toHaveBeenCalledWith([ + 'queuedMessages', + ]); + expect(FlashMessagesLogic.actions.clearQueuedMessages).toHaveBeenCalled(); + }); + }); + + describe('beforeUnmount', () => { + it('removes history listener on unmount', () => { + const mockUnlistener = jest.fn(); + const unmount = FlashMessagesLogic.mount(); + + FlashMessagesLogic.actions.setHistoryListener(mockUnlistener); + unmount(); + + expect(mockUnlistener).toHaveBeenCalled(); + }); + + it('does not crash if no listener exists', () => { + const unmount = FlashMessagesLogic.mount(); + unmount(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts new file mode 100644 index 0000000000000..96c7817832c52 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts @@ -0,0 +1,87 @@ +/* + * 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 { kea } from 'kea'; +import { ReactNode } from 'react'; +import { History } from 'history'; + +import { IKeaLogic, TKeaReducers, IKeaParams } from '../types'; + +export interface IFlashMessage { + type: 'success' | 'info' | 'warning' | 'error'; + message: ReactNode; + description?: ReactNode; +} + +export interface IFlashMessagesValues { + messages: IFlashMessage[]; + queuedMessages: IFlashMessage[]; + historyListener: Function | null; +} +export interface IFlashMessagesActions { + setFlashMessages(messages: IFlashMessage | IFlashMessage[]): void; + clearFlashMessages(): void; + setQueuedMessages(messages: IFlashMessage | IFlashMessage[]): void; + clearQueuedMessages(): void; + listenToHistory(history: History): void; + setHistoryListener(historyListener: Function): void; +} + +const convertToArray = (messages: IFlashMessage | IFlashMessage[]) => + !Array.isArray(messages) ? [messages] : messages; + +export const FlashMessagesLogic = kea({ + actions: (): IFlashMessagesActions => ({ + setFlashMessages: (messages) => ({ messages: convertToArray(messages) }), + clearFlashMessages: () => null, + setQueuedMessages: (messages) => ({ messages: convertToArray(messages) }), + clearQueuedMessages: () => null, + listenToHistory: (history) => history, + setHistoryListener: (historyListener) => ({ historyListener }), + }), + reducers: (): TKeaReducers => ({ + messages: [ + [], + { + setFlashMessages: (_, { messages }) => messages, + clearFlashMessages: () => [], + }, + ], + queuedMessages: [ + [], + { + setQueuedMessages: (_, { messages }) => messages, + clearQueuedMessages: () => [], + }, + ], + historyListener: [ + null, + { + setHistoryListener: (_, { historyListener }) => historyListener, + }, + ], + }), + listeners: ({ values, actions }): Partial => ({ + listenToHistory: (history) => { + // On React Router navigation, clear previous flash messages and load any queued messages + const unlisten = history.listen(() => { + actions.clearFlashMessages(); + actions.setFlashMessages(values.queuedMessages); + actions.clearQueuedMessages(); + }); + actions.setHistoryListener(unlisten); + }, + }), + events: ({ values }) => ({ + beforeUnmount: () => { + const { historyListener: removeHistoryListener } = values; + if (removeHistoryListener) removeHistoryListener(); + }, + }), +} as IKeaParams) as IKeaLogic< + IFlashMessagesValues, + IFlashMessagesActions +>; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.test.tsx new file mode 100644 index 0000000000000..bcd7abd6d7ce2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.test.tsx @@ -0,0 +1,46 @@ +/* + * 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 '../../__mocks__/shallow_usecontext.mock'; +import '../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { useValues, useActions } from 'kea'; + +import { mockHistory } from '../../__mocks__'; + +import { FlashMessagesProvider } from './'; + +describe('FlashMessagesProvider', () => { + const props = { history: mockHistory as any }; + const listenToHistory = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + (useActions as jest.Mock).mockImplementationOnce(() => ({ listenToHistory })); + }); + + it('does not render', () => { + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('listens to history on mount', () => { + shallow(); + + expect(listenToHistory).toHaveBeenCalledWith(mockHistory); + }); + + it('does not add another history listener if one already exists', () => { + (useValues as jest.Mock).mockImplementationOnce(() => ({ historyListener: 'exists' as any })); + + shallow(); + + expect(listenToHistory).not.toHaveBeenCalledWith(props); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx new file mode 100644 index 0000000000000..584124468a91f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx @@ -0,0 +1,30 @@ +/* + * 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, { useEffect } from 'react'; +import { useValues, useActions } from 'kea'; +import { History } from 'history'; + +import { + FlashMessagesLogic, + IFlashMessagesValues, + IFlashMessagesActions, +} from './flash_messages_logic'; + +interface IFlashMessagesProviderProps { + history: History; +} + +export const FlashMessagesProvider: React.FC = ({ history }) => { + const { historyListener } = useValues(FlashMessagesLogic) as IFlashMessagesValues; + const { listenToHistory } = useActions(FlashMessagesLogic) as IFlashMessagesActions; + + useEffect(() => { + if (!historyListener) listenToHistory(history); + }, []); + + return null; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts new file mode 100644 index 0000000000000..74e233ad6b320 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { FlashMessages } from './flash_messages'; +export { + FlashMessagesLogic, + IFlashMessage, + IFlashMessagesValues, + IFlashMessagesActions, +} from './flash_messages_logic'; +export { FlashMessagesProvider } from './flash_messages_provider'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index a8e08323c5e3b..561016d36921d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -4,14 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface IFlashMessagesProps { - info?: string[]; - warning?: string[]; - error?: string[]; - success?: string[]; - isWrapped?: boolean; - children?: React.ReactNode; -} +export { IFlashMessage } from './flash_messages'; export interface IKeaLogic { mount(): Function; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts index 5588c4fc53b67..05715c648e5dc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts @@ -22,7 +22,6 @@ export const mockLogicValues = { personalSourcesCount: 0, sourcesCount: 0, dataLoading: true, - flashMessages: {}, } as IOverviewValues; export const mockLogicActions = { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts index 3fbf0e60b5b49..61108d7cb1f2f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts @@ -76,15 +76,6 @@ describe('OverviewLogic', () => { }); }); - describe('setFlashMessages', () => { - it('will set `flashMessages`', () => { - const flashMessages = { error: ['error'] }; - OverviewLogic.actions.setFlashMessages(flashMessages); - - expect(OverviewLogic.values.flashMessages).toEqual(flashMessages); - }); - }); - describe('initializeOverview', () => { it('calls API and sets values', async () => { const setServerDataSpy = jest.spyOn(OverviewLogic.actions, 'setServerData'); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts index 057bce1b4056c..6606e5b55cb33 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts @@ -8,7 +8,7 @@ import { kea } from 'kea'; import { HttpLogic } from '../../../shared/http'; import { IAccount, IOrganization } from '../../types'; -import { IFlashMessagesProps, IKeaLogic, TKeaReducers, IKeaParams } from '../../../shared/types'; +import { IKeaLogic, TKeaReducers, IKeaParams } from '../../../shared/types'; import { IFeedActivity } from './recent_activity'; @@ -30,19 +30,16 @@ export interface IOverviewServerData { export interface IOverviewActions { setServerData(serverData: IOverviewServerData): void; - setFlashMessages(flashMessages: IFlashMessagesProps): void; initializeOverview(): void; } export interface IOverviewValues extends IOverviewServerData { dataLoading: boolean; - flashMessages: IFlashMessagesProps; } export const OverviewLogic = kea({ actions: (): IOverviewActions => ({ setServerData: (serverData) => serverData, - setFlashMessages: (flashMessages) => ({ flashMessages }), initializeOverview: () => null, }), reducers: (): TKeaReducers => ({ @@ -70,12 +67,6 @@ export const OverviewLogic = kea({ setServerData: (_, { canCreateInvitations }) => canCreateInvitations, }, ], - flashMessages: [ - {}, - { - setFlashMessages: (_, { flashMessages }) => flashMessages, - }, - ], hasUsers: [ false, { From e2e9d96df674e7cdbcaa94f33f9ca4098c0c9cc8 Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Thu, 27 Aug 2020 12:15:47 -0700 Subject: [PATCH 07/23] accessibility test for Painless lab (#75688) * accessibility test for painless lab * skipped a test due to aria-violation * skipped tests due to aria-violation and added datatestsubj * removed the unwanted import * incorporate review comments * feedback incorporated * review comments incorporated * removed unwanted expect --- .../components/output_pane/output_pane.tsx | 1 + .../public/application/constants.tsx | 3 + .../test/accessibility/apps/painless_lab.ts | 65 +++++++++++++++++++ x-pack/test/accessibility/config.ts | 1 + x-pack/test/functional/config.js | 4 ++ 5 files changed, 74 insertions(+) create mode 100644 x-pack/test/accessibility/apps/painless_lab.ts diff --git a/x-pack/plugins/painless_lab/public/application/components/output_pane/output_pane.tsx b/x-pack/plugins/painless_lab/public/application/components/output_pane/output_pane.tsx index e6a97bb02f738..ce597b27cc2a3 100644 --- a/x-pack/plugins/painless_lab/public/application/components/output_pane/output_pane.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/output_pane/output_pane.tsx @@ -50,6 +50,7 @@ export const OutputPane: FunctionComponent = ({ isLoading, response }) => {defaultLabel} @@ -41,6 +42,7 @@ export const painlessContextOptions = [ { value: 'filter', inputDisplay: filterLabel, + 'data-test-subj': 'filterButtonDropdown', dropdownDisplay: ( <> {filterLabel} @@ -57,6 +59,7 @@ export const painlessContextOptions = [ { value: 'score', inputDisplay: scoreLabel, + 'data-test-subj': 'scoreButtonDropdown', dropdownDisplay: ( <> {scoreLabel} diff --git a/x-pack/test/accessibility/apps/painless_lab.ts b/x-pack/test/accessibility/apps/painless_lab.ts new file mode 100644 index 0000000000000..0ec8285f50ec8 --- /dev/null +++ b/x-pack/test/accessibility/apps/painless_lab.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 { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'security']); + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const a11y = getService('a11y'); + + describe('Accessibility Painless Lab Editor', () => { + before(async () => { + await PageObjects.common.navigateToApp('painlessLab'); + }); + + it('renders the page without a11y errors', async () => { + await PageObjects.common.navigateToApp('painlessLab'); + await a11y.testAppSnapshot(); + }); + + it('click on the output button', async () => { + const painlessTabsOutput = await find.byCssSelector( + '[data-test-subj="painlessTabs"] #output' + ); + await painlessTabsOutput.click(); + await a11y.testAppSnapshot(); + }); + + it('click on the parameters button', async () => { + const painlessTabsParameters = await find.byCssSelector( + '[data-test-subj="painlessTabs"] #parameters' + ); + await painlessTabsParameters.click(); + await a11y.testAppSnapshot(); + }); + + // github.com/elastic/kibana/issues/75876 + it.skip('click on the context button', async () => { + const painlessTabsContext = await find.byCssSelector( + '[data-test-subj="painlessTabs"] #context' + ); + await painlessTabsContext.click(); + await a11y.testAppSnapshot(); + }); + + it.skip('click on the Basic button', async () => { + await testSubjects.click('basicButtonDropdown'); + await a11y.testAppSnapshot(); + }); + + it.skip('click on the Filter button', async () => { + await testSubjects.click('filterButtonDropdown'); + await a11y.testAppSnapshot(); + }); + + it.skip('click on the Score button', async () => { + await testSubjects.click('scoreButtonDropdown'); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 7f4543d014def..0a95805754314 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -20,6 +20,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/grok_debugger'), require.resolve('./apps/search_profiler'), require.resolve('./apps/uptime'), + require.resolve('./apps/painless_lab'), ], pageObjects, services, diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index cdc6292ba808a..16e2cd1559fce 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -128,6 +128,10 @@ export default async function ({ readConfigFile }) { pathname: '/app/dev_tools', hash: '/searchprofiler', }, + painlessLab: { + pathname: '/app/dev_tools', + hash: '/painless_lab', + }, spaceSelector: { pathname: '/', }, From ca94923900f9ce424f08f4d32bea53b40bc0e552 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 27 Aug 2020 14:34:28 -0500 Subject: [PATCH 08/23] [Enterprise Search] Migrate util and components from ent-search (#76051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Migrate useDidUpdateEffect hook Migrates https://github.com/elastic/ent-search/blob/master/app/javascript/shared/utils/useDidUpdateEffect.ts with test added. * Migrate TruncateContent component * Migrate TableHeader component * Remove unused deps * Fix test name * Remove custom type in favor of DependencyList * Add stylesheet for truncated content * Actually import stylesheet 🤦🏼‍♂️ * Replace legacy mixin Co-authored-by: Elastic Machine --- .../applications/shared/table_header/index.ts | 7 +++ .../shared/table_header/table_header.test.tsx | 29 ++++++++++++ .../shared/table_header/table_header.tsx | 23 +++++++++ .../applications/shared/truncate/index.ts | 8 ++++ .../shared/truncate/truncate.test.tsx | 47 +++++++++++++++++++ .../applications/shared/truncate/truncate.ts | 13 +++++ .../shared/truncate/truncated_content.scss | 35 ++++++++++++++ .../shared/truncate/truncated_content.tsx | 35 ++++++++++++++ .../shared/use_did_update_effect/index.ts | 7 +++ .../use_did_update_effect.test.tsx | 33 +++++++++++++ .../use_did_update_effect.tsx | 23 +++++++++ 11 files changed, 260 insertions(+) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/table_header/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/truncate/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/table_header/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/index.ts new file mode 100644 index 0000000000000..34ce070fcde46 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/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 { TableHeader } from './table_header'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx new file mode 100644 index 0000000000000..70e2ac7ac6f0d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx @@ -0,0 +1,29 @@ +/* + * 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 { shallow } from 'enzyme'; +import { EuiTableHeader, EuiTableHeaderCell } from '@elastic/eui'; + +import { TableHeader } from './table_header'; + +const headerItems = ['foo', 'bar', 'baz']; + +describe('TableHeader', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTableHeader)).toHaveLength(1); + expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(3); + }); + + it('renders extra cell', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTableHeader)).toHaveLength(1); + expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(4); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.tsx new file mode 100644 index 0000000000000..e7f9617fdcd91 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.tsx @@ -0,0 +1,23 @@ +/* + * 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 { EuiTableHeader, EuiTableHeaderCell } from '@elastic/eui'; + +interface ITableHeaderProps { + headerItems: string[]; + extraCell?: boolean; +} + +export const TableHeader: React.FC = ({ headerItems, extraCell }) => ( + + {headerItems.map((item, i) => ( + {item} + ))} + {extraCell && } + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/index.ts new file mode 100644 index 0000000000000..d3ee618e92b5b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { truncate, truncateBeginning } from './truncate'; +export { TruncatedContent } from './truncated_content'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx new file mode 100644 index 0000000000000..aa8427cd822be --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx @@ -0,0 +1,47 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { TruncatedContent } from './'; + +const content = 'foobarbaz'; + +describe('TruncatedContent', () => { + it('renders with no truncation', () => { + const wrapper = shallow(); + + expect(wrapper.find('span.truncated-content')).toHaveLength(0); + expect(wrapper.text()).toEqual('foo'); + }); + + it('renders with truncation at the end', () => { + const wrapper = shallow(); + const element = wrapper.find('span.truncated-content'); + + expect(element).toHaveLength(1); + expect(element.prop('title')).toEqual(content); + expect(wrapper.text()).toEqual('foob…'); + expect(wrapper.find('span.truncated-content__tooltip')).toHaveLength(0); + }); + + it('renders with truncation at the beginning', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('span.truncated-content')).toHaveLength(1); + expect(wrapper.text()).toEqual('…rbaz'); + }); + + it('renders with inline tooltip', () => { + const wrapper = shallow(); + + expect(wrapper.find('span.truncated-content').prop('title')).toEqual(''); + expect(wrapper.find('span.truncated-content__tooltip')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.ts b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.ts new file mode 100644 index 0000000000000..36094e3abe258 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.ts @@ -0,0 +1,13 @@ +/* + * 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 function truncate(text: string, length: number) { + return `${text.substring(0, length)}…`; +} + +export function truncateBeginning(text: string, length: number) { + return `…${text.substring(text.length - length)}`; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.scss b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.scss new file mode 100644 index 0000000000000..701834acfed9d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.scss @@ -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. + */ + +.truncated-content { + position: relative; + z-index: 2; + display: inline-block; + white-space: nowrap; + + &__tooltip { + position: absolute; + top: 50%; + transform: translateY(-50%); + left: -3px; + margin-top: -1px; + background: $euiColorEmptyShade; + border-radius: 2px; + width: calc(100% + 4px); + height: calc(100% + 4px); + padding: 0 2px; + display: none; + align-items: center; + box-shadow: 0 1px 3px rgba(black, 0.1); + border: 1px solid $euiBorderColor; + width: auto; + white-space: nowrap; + + .truncated-content:hover & { + display: flex; + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.tsx new file mode 100644 index 0000000000000..7785f75b71d34 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.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 { truncate, truncateBeginning } from './'; + +import './truncated_content.scss'; + +interface ITruncatedContentProps { + content: string; + length: number; + beginning?: boolean; + tooltipType?: 'inline' | 'title'; +} + +export const TruncatedContent: React.FC = ({ + content, + length, + beginning = false, + tooltipType = 'inline', +}) => { + if (content.length <= length) return <>{content}; + + const inline = tooltipType === 'inline'; + return ( + + {beginning ? truncateBeginning(content, length) : truncate(content, length)} + {inline && {content}} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts new file mode 100644 index 0000000000000..05c60ebced088 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/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 { useDidUpdateEffect } from './use_did_update_effect'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx new file mode 100644 index 0000000000000..e3d2ffb44f01e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx @@ -0,0 +1,33 @@ +/* + * 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, { useState } from 'react'; +import { mount } from 'enzyme'; + +import { EuiLink } from '@elastic/eui'; + +import { useDidUpdateEffect } from './use_did_update_effect'; + +const fn = jest.fn(); + +const TestHook = ({ value }: { value: number }) => { + const [inputValue, setValue] = useState(value); + useDidUpdateEffect(fn, [inputValue]); + return setValue(2)} />; +}; + +const wrapper = mount(); + +describe('useDidUpdateEffect', () => { + it('should not fire function when value unchanged', () => { + expect(fn).not.toHaveBeenCalled(); + }); + + it('should fire function when value changed', () => { + wrapper.find(EuiLink).simulate('click'); + expect(fn).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx new file mode 100644 index 0000000000000..4c3e10fc84b84 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/* + * Sometimes we don't want to fire the initial useEffect call. + * This custom Hook only fires after the intial render has completed. + */ +import { useEffect, useRef, DependencyList } from 'react'; + +export const useDidUpdateEffect = (fn: Function, inputs: DependencyList) => { + const didMountRef = useRef(false); + + useEffect(() => { + if (didMountRef.current) { + fn(); + } else { + didMountRef.current = true; + } + }, inputs); +}; From 31507f82b6b4e6751fc694ec61b3b0cab3cbd83c Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Thu, 27 Aug 2020 15:43:52 -0400 Subject: [PATCH 09/23] [Resolver] Fix useSelector usage (#76129) In some cases we have selectors returning thunks. The thunks need to be called inside `useSelector` in order for a rerender to be reliably triggered. `useSelector` triggers a re-render if its return value changes. By calling the thunk inside of the selector passed to `useSelector`, we will trigger re-renders when needed. --- .../resolver/view/panels/process_details.tsx | 6 ++++-- .../view/panels/related_event_detail.tsx | 7 +++---- .../public/resolver/view/process_event_dot.tsx | 18 +++++++++++------- .../view/resolver_without_providers.tsx | 11 +++++++---- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx index e86e3e6baf4a4..1ec56b8aa169a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx @@ -31,7 +31,7 @@ import { import { CubeForProcess } from './cube_for_process'; import { ResolverEvent } from '../../../../common/endpoint/types'; import { useResolverTheme } from '../assets'; -import { CrumbInfo } from '../../types'; +import { CrumbInfo, ResolverState } from '../../types'; const StyledDescriptionList = styled(EuiDescriptionList)` &.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title { @@ -56,7 +56,9 @@ export const ProcessDetails = memo(function ProcessDetails({ }) { const processName = event.eventName(processEvent); const entityId = event.entityId(processEvent); - const isProcessTerminated = useSelector(selectors.isProcessTerminated)(entityId); + const isProcessTerminated = useSelector((state: ResolverState) => + selectors.isProcessTerminated(state)(entityId) + ); const processInfoEntry: EuiDescriptionListProps['listItems'] = useMemo(() => { const eventTime = event.eventTimestamp(processEvent); const dateTime = eventTime === undefined ? null : formatDate(eventTime); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx index 6aacf91c56178..dfafbae9c9a16 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx @@ -16,7 +16,7 @@ import { ResolverEvent } from '../../../../common/endpoint/types'; import * as selectors from '../../store/selectors'; import { useResolverDispatch } from '../use_resolver_dispatch'; import { PanelContentError } from './panel_content_error'; -import { CrumbInfo } from '../../types'; +import { CrumbInfo, ResolverState } from '../../types'; // Adding some styles to prevent horizontal scrollbars, per request from UX review const StyledDescriptionList = memo(styled(EuiDescriptionList)` @@ -126,9 +126,8 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({ relatedEventCategory = naString, sections, formattedDate, - ] = useSelector(selectors.relatedEventDisplayInfoByEntityAndSelfId)( - processEntityId, - relatedEventId + ] = useSelector((state: ResolverState) => + selectors.relatedEventDisplayInfoByEntityAndSelfId(state)(processEntityId, relatedEventId) ); const waitCrumbs = useMemo(() => { diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 2bb104801866f..baa8ce1fcdd86 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -12,7 +12,7 @@ import { htmlIdGenerator, EuiButton, EuiI18nNumber, EuiFlexGroup, EuiFlexItem } import { useSelector } from 'react-redux'; import { NodeSubMenu, subMenuAssets } from './submenu'; import { applyMatrix3 } from '../models/vector2'; -import { Vector2, Matrix3 } from '../types'; +import { Vector2, Matrix3, ResolverState } from '../types'; import { SymbolIds, useResolverTheme, calculateResolverFontSize } from './assets'; import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; import { useResolverDispatch } from './use_resolver_dispatch'; @@ -118,7 +118,9 @@ const UnstyledProcessEventDot = React.memo( // NB: this component should be taking nodeID as a `string` instead of handling this logic here throw new Error('Tried to render a node with no ID'); } - const relatedEventStats = useSelector(selectors.relatedEventsStats)(nodeID); + const relatedEventStats = useSelector((state: ResolverState) => + selectors.relatedEventsStats(state)(nodeID) + ); // define a standard way of giving HTML IDs to nodes based on their entity_id/nodeID. // this is used to link nodes via aria attributes @@ -126,11 +128,13 @@ const UnstyledProcessEventDot = React.memo( htmlIDPrefix, ]); - const ariaLevel: number | null = useSelector(selectors.ariaLevel)(nodeID); + const ariaLevel: number | null = useSelector((state: ResolverState) => + selectors.ariaLevel(state)(nodeID) + ); // the node ID to 'flowto' - const ariaFlowtoNodeID: string | null = useSelector(selectors.ariaFlowtoNodeID)(timeAtRender)( - nodeID + const ariaFlowtoNodeID: string | null = useSelector((state: ResolverState) => + selectors.ariaFlowtoNodeID(state)(timeAtRender)(nodeID) ); const isShowingEventActions = xScale > 0.8; @@ -290,8 +294,8 @@ const UnstyledProcessEventDot = React.memo( ? subMenuAssets.initialMenuStatus : relatedEventOptions; - const grandTotal: number | null = useSelector(selectors.relatedEventTotalForProcess)( - event as ResolverEvent + const grandTotal: number | null = useSelector((state: ResolverState) => + selectors.relatedEventTotalForProcess(state)(event as ResolverEvent) ); /* eslint-disable jsx-a11y/click-events-have-key-events */ diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index 32faeec043f2d..aa845e7283ebe 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -21,7 +21,7 @@ import { useStateSyncingActions } from './use_state_syncing_actions'; import { StyledMapContainer, StyledPanel, GraphContainer } from './styles'; import { entityIDSafeVersion } from '../../../common/endpoint/models/event'; import { SideEffectContext } from './side_effect_context'; -import { ResolverProps } from '../types'; +import { ResolverProps, ResolverState } from '../types'; /** * The highest level connected Resolver component. Needs a `Provider` in its ancestry to work. @@ -46,9 +46,12 @@ export const ResolverWithoutProviders = React.memo( // use this for the entire render in order to keep things in sync const timeAtRender = timestamp(); - const { processNodePositions, connectingEdgeLineSegments } = useSelector( - selectors.visibleNodesAndEdgeLines - )(timeAtRender); + const { + processNodePositions, + connectingEdgeLineSegments, + } = useSelector((state: ResolverState) => + selectors.visibleNodesAndEdgeLines(state)(timeAtRender) + ); const terminatedProcesses = useSelector(selectors.terminatedProcesses); const { projectionMatrix, ref: cameraRef, onMouseDown } = useCamera(); From 50193eaabb3993fa662efc83caa5712ef11ae64d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 27 Aug 2020 16:00:03 -0400 Subject: [PATCH 10/23] Fix alerts unable to create / update when the name has trailing whitepace(s) (#76079) * Trim alert name in API key name * Add API integration tests --- .../alerts/server/alerts_client.test.ts | 128 +++++++++++++++++- x-pack/plugins/alerts/server/alerts_client.ts | 4 +- .../tests/alerting/create.ts | 39 ++++++ .../tests/alerting/update.ts | 51 +++++++ 4 files changed, 219 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index d994269366ae6..f4aef62657abc 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -652,6 +652,70 @@ describe('create()', () => { expect(taskManager.schedule).toHaveBeenCalledTimes(0); }); + test('should trim alert name when creating API key', async () => { + const data = getMockData({ name: ' my alert name ' }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: false, + name: ' my alert name ', + alertTypeId: '123', + schedule: { interval: 10000 }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + + await alertsClient.create({ data }); + expect(alertsClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: 123/my alert name'); + }); + test('should validate params', async () => { const data = getMockData(); alertTypeRegistry.get.mockReturnValue({ @@ -2896,9 +2960,13 @@ describe('update()', () => { type: 'alert', attributes: { enabled: true, + tags: ['foo'], alertTypeId: 'myType', + schedule: { interval: '10s' }, consumer: 'myApp', scheduledTaskId: 'task-123', + params: {}, + throttle: null, actions: [ { group: 'default', @@ -2927,7 +2995,7 @@ describe('update()', () => { unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); alertTypeRegistry.get.mockReturnValue({ - id: '123', + id: 'myType', name: 'Test', actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', @@ -3489,6 +3557,64 @@ describe('update()', () => { ); }); + it('should trim alert name in the API key name', async () => { + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: false, + name: ' my alert name ', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: 'task-123', + apiKey: null, + }, + updated_at: new Date().toISOString(), + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + await alertsClient.update({ + id: '1', + data: { + ...existingAlert.attributes, + name: ' my alert name ', + }, + }); + + expect(alertsClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: myType/my alert name'); + }); + it('swallows error when invalidate API key throws', async () => { alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 80e021fc5cb6e..74aef644d58ca 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { omit, isEqual, map, uniq, pick, truncate } from 'lodash'; +import { omit, isEqual, map, uniq, pick, truncate, trim } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Logger, @@ -940,7 +940,7 @@ export class AlertsClient { } private generateAPIKeyName(alertTypeId: string, alertName: string) { - return truncate(`Alerting: ${alertTypeId}/${alertName}`, { length: 256 }); + return truncate(`Alerting: ${alertTypeId}/${trim(alertName)}`, { length: 256 }); } } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts index 8d7b9dec58cf1..983f87405a1a6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts @@ -347,6 +347,45 @@ export default function createAlertTests({ getService }: FtrProviderContext) { } }); + it('should handle create alert request appropriately when alert name has leading and trailing whitespaces', async () => { + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send( + getTestAlertData({ + name: ' leading and trailing whitespace ', + }) + ); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'create', + 'test.noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + expect(response.body.name).to.eql(' leading and trailing whitespace '); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it('should handle create alert request appropriately when alert type is unregistered', async () => { const response = await supertestWithoutAuth .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index ab3a92d0b3f70..48269cc1c4498 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -505,6 +505,57 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { } }); + it('should handle update alert request appropriately when alert name has leading and trailing whitespaces', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + const updatedData = { + name: ' leading and trailing whitespace ', + tags: ['bar'], + params: { + foo: true, + }, + schedule: { interval: '12s' }, + actions: [], + throttle: '1m', + }; + const response = await supertestWithoutAuth + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send(updatedData); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'update', + 'test.noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + expect(response.body.name).to.eql(' leading and trailing whitespace '); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it(`shouldn't update alert from another space`, async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) From 905c76242d650dce2f9288c43ba91c8cbd5184e0 Mon Sep 17 00:00:00 2001 From: Chris Cressman Date: Thu, 27 Aug 2020 16:11:18 -0400 Subject: [PATCH 11/23] Fixes App Search documentation links (#76133) Two links to App Search docs are pointing to outdated versions. Update the URLs. --- .../app_search/components/setup_guide/setup_guide.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx index fa55289e73e0b..204f355c7a31a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx @@ -19,8 +19,8 @@ export const SetupGuide: React.FC = () => ( Date: Thu, 27 Aug 2020 14:42:21 -0600 Subject: [PATCH 12/23] [Security_Solution][Resolver] Resolver loading and error state (#75600) --- .../data_access_layer/mocks/emptify_mock.ts | 88 +++++++++ .../data_access_layer/mocks/pausify_mock.ts | 124 +++++++++++++ .../resolver/test_utilities/extend_jest.ts | 12 +- .../resolver/view/clickthrough.test.tsx | 1 + .../view/resolver_loading_state.test.tsx | 167 ++++++++++++++++++ 5 files changed, 387 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts new file mode 100644 index 0000000000000..43282848dcf9a --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts @@ -0,0 +1,88 @@ +/* + * 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 { + ResolverRelatedEvents, + ResolverTree, + ResolverEntityIndex, +} from '../../../../common/endpoint/types'; +import { mockTreeWithNoProcessEvents } from '../../mocks/resolver_tree'; +import { DataAccessLayer } from '../../types'; + +type EmptiableRequests = 'relatedEvents' | 'resolverTree' | 'entities' | 'indexPatterns'; + +interface Metadata { + /** + * The `_id` of the document being analyzed. + */ + databaseDocumentID: string; + /** + * A record of entityIDs to be used in tests assertions. + */ + entityIDs: T; +} + +/** + * A simple mock dataAccessLayer that allows you to control whether a request comes back with data or empty. + */ +export function emptifyMock( + { + metadata, + dataAccessLayer, + }: { + dataAccessLayer: DataAccessLayer; + metadata: Metadata; + }, + dataShouldBeEmpty: EmptiableRequests[] +): { + dataAccessLayer: DataAccessLayer; + metadata: Metadata; +} { + return { + metadata, + dataAccessLayer: { + /** + * Fetch related events for an entity ID + */ + async relatedEvents(...args): Promise { + return dataShouldBeEmpty.includes('relatedEvents') + ? Promise.resolve({ + entityID: args[0], + events: [], + nextEvent: null, + }) + : dataAccessLayer.relatedEvents(...args); + }, + + /** + * Fetch a ResolverTree for a entityID + */ + async resolverTree(...args): Promise { + return dataShouldBeEmpty.includes('resolverTree') + ? Promise.resolve(mockTreeWithNoProcessEvents()) + : dataAccessLayer.resolverTree(...args); + }, + + /** + * Get an array of index patterns that contain events. + */ + indexPatterns(...args): string[] { + return dataShouldBeEmpty.includes('indexPatterns') + ? [] + : dataAccessLayer.indexPatterns(...args); + }, + + /** + * Get entities matching a document. + */ + async entities(...args): Promise { + return dataShouldBeEmpty.includes('entities') + ? Promise.resolve([]) + : dataAccessLayer.entities(...args); + }, + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts new file mode 100644 index 0000000000000..baddcdfd0cd84 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts @@ -0,0 +1,124 @@ +/* + * 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 { + ResolverRelatedEvents, + ResolverTree, + ResolverEntityIndex, +} from '../../../../common/endpoint/types'; +import { DataAccessLayer } from '../../types'; + +type PausableRequests = 'relatedEvents' | 'resolverTree' | 'entities'; + +interface Metadata { + /** + * The `_id` of the document being analyzed. + */ + databaseDocumentID: string; + /** + * A record of entityIDs to be used in tests assertions. + */ + entityIDs: T; +} + +/** + * A simple mock dataAccessLayer that allows you to manually pause and resume a request. + */ +export function pausifyMock({ + metadata, + dataAccessLayer, +}: { + dataAccessLayer: DataAccessLayer; + metadata: Metadata; +}): { + dataAccessLayer: DataAccessLayer; + metadata: Metadata; + pause: (pausableRequests: PausableRequests[]) => void; + resume: (pausableRequests: PausableRequests[]) => void; +} { + let relatedEventsPromise = Promise.resolve(); + let resolverTreePromise = Promise.resolve(); + let entitiesPromise = Promise.resolve(); + + let relatedEventsResolver: (() => void) | null; + let resolverTreeResolver: (() => void) | null; + let entitiesResolver: (() => void) | null; + + return { + metadata, + pause: (pausableRequests: PausableRequests[]) => { + const pauseRelatedEventsRequest = pausableRequests.includes('relatedEvents'); + const pauseResolverTreeRequest = pausableRequests.includes('resolverTree'); + const pauseEntitiesRequest = pausableRequests.includes('entities'); + + if (pauseRelatedEventsRequest && !relatedEventsResolver) { + relatedEventsPromise = new Promise((resolve) => { + relatedEventsResolver = resolve; + }); + } + if (pauseResolverTreeRequest && !resolverTreeResolver) { + resolverTreePromise = new Promise((resolve) => { + resolverTreeResolver = resolve; + }); + } + if (pauseEntitiesRequest && !entitiesResolver) { + entitiesPromise = new Promise((resolve) => { + entitiesResolver = resolve; + }); + } + }, + resume: (pausableRequests: PausableRequests[]) => { + const resumeEntitiesRequest = pausableRequests.includes('entities'); + const resumeResolverTreeRequest = pausableRequests.includes('resolverTree'); + const resumeRelatedEventsRequest = pausableRequests.includes('relatedEvents'); + + if (resumeEntitiesRequest && entitiesResolver) { + entitiesResolver(); + entitiesResolver = null; + } + if (resumeResolverTreeRequest && resolverTreeResolver) { + resolverTreeResolver(); + resolverTreeResolver = null; + } + if (resumeRelatedEventsRequest && relatedEventsResolver) { + relatedEventsResolver(); + relatedEventsResolver = null; + } + }, + dataAccessLayer: { + /** + * Fetch related events for an entity ID + */ + async relatedEvents(...args): Promise { + await relatedEventsPromise; + return dataAccessLayer.relatedEvents(...args); + }, + + /** + * Fetch a ResolverTree for a entityID + */ + async resolverTree(...args): Promise { + await resolverTreePromise; + return dataAccessLayer.resolverTree(...args); + }, + + /** + * Get an array of index patterns that contain events. + */ + indexPatterns(...args): string[] { + return dataAccessLayer.indexPatterns(...args); + }, + + /** + * Get entities matching a document. + */ + async entities(...args): Promise { + await entitiesPromise; + return dataAccessLayer.entities(...args); + }, + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts index df8f32d15a7ab..aa04221361de0 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts @@ -44,7 +44,7 @@ expect.extend({ const received: T[] = []; // Set to true if the test passes. - let pass: boolean = false; + let lastCheckPassed: boolean = false; // Async iterate over the iterable for await (const next of receivedIterable) { @@ -52,15 +52,17 @@ expect.extend({ received.push(next); // Use deep equals to compare the value to the expected value if (this.equals(next, expected)) { - // If the value is equal, break - pass = true; + lastCheckPassed = true; + } else if (lastCheckPassed) { + // the previous check passed but this one didn't + lastCheckPassed = false; break; } } // Use `pass` as set in the above loop (or initialized to `false`) // See https://jestjs.io/docs/en/expect#custom-matchers-api and https://jestjs.io/docs/en/expect#thisutils - const message = pass + const message = lastCheckPassed ? () => `${this.utils.matcherHint(matcherName, undefined, undefined, options)}\n\n` + `Expected: not ${this.utils.printExpected(expected)}\n${ @@ -84,7 +86,7 @@ expect.extend({ ) .join(`\n\n`)}`; - return { message, pass }; + return { message, pass: lastCheckPassed }; }, /** * A custom matcher that takes an async generator and compares each value it yields to an expected value. diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 358fcd17b998a..1e5ac093cac77 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -242,6 +242,7 @@ describe('Resolver, when analyzing a tree that has two related events for the or ); if (button) { button.simulate('click'); + button.simulate('click'); // The first click opened the menu, this second click closes it } }); it('should close the submenu', async () => { diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx new file mode 100644 index 0000000000000..c357ee18acfeb --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx @@ -0,0 +1,167 @@ +/* + * 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 { Simulator } from '../test_utilities/simulator'; +import { pausifyMock } from '../data_access_layer/mocks/pausify_mock'; +import { emptifyMock } from '../data_access_layer/mocks/emptify_mock'; +import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children'; +import '../test_utilities/extend_jest'; + +describe('Resolver: data loading and resolution states', () => { + let simulator: Simulator; + const resolverComponentInstanceID = 'resolver-loading-resolution-states'; + + describe('When entities data is being requested', () => { + beforeEach(() => { + const { + metadata: { databaseDocumentID }, + dataAccessLayer, + pause, + } = pausifyMock(noAncestorsTwoChildren()); + pause(['entities']); + simulator = new Simulator({ + dataAccessLayer, + databaseDocumentID, + resolverComponentInstanceID, + }); + }); + + it('should display a loading state', async () => { + await expect( + simulator.map(() => ({ + resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, + resolverGraphError: simulator.testSubject('resolver:graph:error').length, + resolverGraph: simulator.testSubject('resolver:graph').length, + })) + ).toYieldEqualTo({ + resolverGraphLoading: 1, + resolverGraphError: 0, + resolverGraph: 0, + }); + }); + }); + + describe('When resolver tree data is being requested', () => { + beforeEach(() => { + const { + metadata: { databaseDocumentID }, + dataAccessLayer, + pause, + } = pausifyMock(noAncestorsTwoChildren()); + pause(['resolverTree']); + simulator = new Simulator({ + dataAccessLayer, + databaseDocumentID, + resolverComponentInstanceID, + }); + }); + + it('should display a loading state', async () => { + await expect( + simulator.map(() => ({ + resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, + resolverGraphError: simulator.testSubject('resolver:graph:error').length, + resolverGraph: simulator.testSubject('resolver:graph').length, + })) + ).toYieldEqualTo({ + resolverGraphLoading: 1, + resolverGraphError: 0, + resolverGraph: 0, + }); + }); + }); + + describe("When the entities request doesn't return any data", () => { + beforeEach(() => { + const { + metadata: { databaseDocumentID }, + dataAccessLayer, + } = emptifyMock(noAncestorsTwoChildren(), ['entities']); + + simulator = new Simulator({ + dataAccessLayer, + databaseDocumentID, + resolverComponentInstanceID, + }); + }); + + it('should display an error', async () => { + await expect( + simulator.map(() => ({ + resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, + resolverGraphError: simulator.testSubject('resolver:graph:error').length, + resolverGraph: simulator.testSubject('resolver:graph').length, + })) + ).toYieldEqualTo({ + resolverGraphLoading: 0, + resolverGraphError: 1, + resolverGraph: 0, + }); + }); + }); + + describe("When the resolver tree request doesn't return any data", () => { + beforeEach(() => { + const { + metadata: { databaseDocumentID }, + dataAccessLayer, + } = emptifyMock(noAncestorsTwoChildren(), ['resolverTree']); + + simulator = new Simulator({ + dataAccessLayer, + databaseDocumentID, + resolverComponentInstanceID, + }); + }); + + it('should display a resolver graph with 0 nodes', async () => { + await expect( + simulator.map(() => ({ + resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, + resolverGraphError: simulator.testSubject('resolver:graph:error').length, + resolverGraph: simulator.testSubject('resolver:graph').length, + resolverGraphNodes: simulator.testSubject('resolver:node').length, + })) + ).toYieldEqualTo({ + resolverGraphLoading: 0, + resolverGraphError: 0, + resolverGraph: 1, + resolverGraphNodes: 0, + }); + }); + }); + + describe('When all resolver data requests successfully resolve', () => { + beforeEach(async () => { + const { + metadata: { databaseDocumentID }, + dataAccessLayer, + } = noAncestorsTwoChildren(); + + simulator = new Simulator({ + dataAccessLayer, + databaseDocumentID, + resolverComponentInstanceID, + }); + }); + + it('should display the resolver graph with 3 nodes', async () => { + await expect( + simulator.map(() => ({ + resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, + resolverGraphError: simulator.testSubject('resolver:graph:error').length, + resolverGraph: simulator.testSubject('resolver:graph').length, + resolverGraphNodes: simulator.testSubject('resolver:node').length, + })) + ).toYieldEqualTo({ + resolverGraphLoading: 0, + resolverGraphError: 0, + resolverGraph: 1, + resolverGraphNodes: 3, + }); + }); + }); +}); From 89ae03221b99c2cf06aa007b3f055e6f0cd43537 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 27 Aug 2020 14:19:41 -0700 Subject: [PATCH 13/23] [docs/getting-started] link to yarn v1 specifically (#76169) Co-authored-by: spalger --- docs/developer/getting-started/index.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer/getting-started/index.asciidoc b/docs/developer/getting-started/index.asciidoc index 10e603a8da8bb..9b334a55c4203 100644 --- a/docs/developer/getting-started/index.asciidoc +++ b/docs/developer/getting-started/index.asciidoc @@ -30,7 +30,7 @@ you can switch to the correct version when using nvm by running: nvm use ---- -Install the latest version of https://yarnpkg.com[yarn]. +Install the latest version of https://classic.yarnpkg.com/en/docs/install[yarn v1]. Bootstrap {kib} and install all the dependencies: From 64311d306f88f649677a26b1f9a50c2f39b1a2aa Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 27 Aug 2020 14:56:48 -0700 Subject: [PATCH 14/23] [plugin-helpers] improve 3rd party KP plugin support (#75019) Co-authored-by: Tyler Smalley Co-authored-by: spalger --- .../external-plugin-functional-tests.asciidoc | 4 +- packages/kbn-dev-utils/package.json | 18 +- packages/kbn-dev-utils/src/babel.ts | 59 +++++ packages/kbn-dev-utils/src/index.ts | 3 + .../src/parse_kibana_platform_plugin.ts | 59 +++++ packages/kbn-dev-utils/src/run/flags.ts | 4 +- .../kbn-dev-utils/src/serializers/index.ts | 1 + .../src/serializers/replace_serializer.ts | 36 +++ ...simple_kibana_platform_plugin_discovery.ts | 54 +---- .../src/streams.ts | 53 ++-- .../src/optimizer/kibana_platform_plugins.ts | 2 +- .../kbn-plugin-generator/src/plugin_types.ts | 60 +++++ .../src/render_template.ts | 25 +- .../template/README.md.ejs | 11 + .../template/package.json.ejs | 2 + packages/kbn-plugin-helpers/package.json | 35 +-- .../build_context.ts} | 20 +- packages/kbn-plugin-helpers/src/cli.ts | 123 ++++++---- packages/kbn-plugin-helpers/src/config.ts | 83 +++++++ .../src/{lib/run.ts => find_kibana_json.ts} | 26 +- .../src/{tasks/start => }/index.ts | 2 +- .../src/integration_tests/build.test.ts | 123 ++++++++++ .../commander_action.test.js.snap | 43 ---- .../src/lib/commander_action.test.js | 87 ------- .../src/lib/commander_action.ts | 36 --- .../kbn-plugin-helpers/src/lib/config_file.ts | 69 ------ .../lib/enable_collecting_unknown_options.ts | 30 --- .../kbn-plugin-helpers/src/lib/pipeline.ts | 23 -- .../src/lib/plugin_config.ts | 74 ------ packages/kbn-plugin-helpers/src/lib/utils.ts | 42 ---- .../kbn-plugin-helpers/src/lib/win_cmd.ts | 24 -- ...task.ts => load_kibana_platform_plugin.ts} | 41 ++-- .../tasks.ts => resolve_kibana_version.ts} | 33 +-- .../src/tasks/build/README.md | 19 -- .../src/tasks/build/build_task.ts | 63 ----- .../src/tasks/build/create_build.ts | 179 -------------- .../src/tasks/build/git_info.ts | 46 ---- .../src/tasks/build/index.ts | 20 -- .../build_action_test_plugin/package.json | 16 -- .../translations/es.json | 4 - .../create_build_test_plugin/index.js | 20 -- .../create_build_test_plugin/package.json | 16 -- .../translations/es.json | 4 - .../create_package_test_plugin/index.js | 20 -- .../create_package_test_plugin/package.json | 16 -- .../translations/es.json | 4 - .../__snapshots__/build_action.test.js.snap | 3 - .../integration_tests/build_action.test.js | 117 --------- .../integration_tests/create_build.test.js | 87 ------- .../integration_tests/create_package.test.js | 48 ---- .../src/tasks/build/rewrite_package_json.ts | 54 ----- .../src/{lib/docs.ts => tasks/clean.ts} | 26 +- .../create_package.ts => create_archive.ts} | 48 ++-- .../src/{lib => tasks}/index.ts | 10 +- .../kbn-plugin-helpers/src/tasks/optimize.ts | 53 ++++ .../src/tasks/start/README.md | 6 - .../src/tasks/start/start_task.ts | 51 ---- .../src/tasks/test/mocha/README.md | 45 ---- .../src/tasks/test/mocha/index.ts | 20 -- .../src/tasks/write_server_files.ts | 101 ++++++++ .../src/tasks/yarn_install.ts | 40 ++++ packages/kbn-plugin-helpers/tsconfig.json | 5 +- .../index.js => scripts/plugin_helpers.js | 3 +- src/setup_node_env/prebuilt_dev_only_entry.js | 1 + .../plugins/kbn_tp_run_pipeline/package.json | 2 +- .../kbn_tp_custom_visualizations/package.json | 2 +- x-pack/.kibana-plugin-helpers.json | 35 --- x-pack/gulpfile.js | 2 - x-pack/package.json | 4 +- x-pack/scripts/api_debug.js | 2 +- x-pack/scripts/functional_test_runner.js | 2 +- x-pack/scripts/functional_tests.js | 2 +- x-pack/scripts/functional_tests_server.js | 2 +- x-pack/scripts/jest.js | 2 +- x-pack/tasks/build.ts | 68 +++++- x-pack/tasks/dev.ts | 14 -- .../common/config.ts | 8 +- .../spaces_api_integration/common/config.ts | 6 +- yarn.lock | 226 +++++++++++++----- 79 files changed, 1146 insertions(+), 1681 deletions(-) create mode 100644 packages/kbn-dev-utils/src/babel.ts create mode 100644 packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts create mode 100644 packages/kbn-dev-utils/src/serializers/replace_serializer.ts rename packages/{kbn-plugin-generator => kbn-dev-utils}/src/streams.ts (62%) create mode 100644 packages/kbn-plugin-generator/src/plugin_types.ts rename packages/kbn-plugin-helpers/{bin/plugin-helpers.js => src/build_context.ts} (73%) mode change 100755 => 100644 create mode 100644 packages/kbn-plugin-helpers/src/config.ts rename packages/kbn-plugin-helpers/src/{lib/run.ts => find_kibana_json.ts} (65%) rename packages/kbn-plugin-helpers/src/{tasks/start => }/index.ts (96%) create mode 100644 packages/kbn-plugin-helpers/src/integration_tests/build.test.ts delete mode 100644 packages/kbn-plugin-helpers/src/lib/__snapshots__/commander_action.test.js.snap delete mode 100644 packages/kbn-plugin-helpers/src/lib/commander_action.test.js delete mode 100644 packages/kbn-plugin-helpers/src/lib/commander_action.ts delete mode 100644 packages/kbn-plugin-helpers/src/lib/config_file.ts delete mode 100644 packages/kbn-plugin-helpers/src/lib/enable_collecting_unknown_options.ts delete mode 100644 packages/kbn-plugin-helpers/src/lib/pipeline.ts delete mode 100644 packages/kbn-plugin-helpers/src/lib/plugin_config.ts delete mode 100644 packages/kbn-plugin-helpers/src/lib/utils.ts delete mode 100644 packages/kbn-plugin-helpers/src/lib/win_cmd.ts rename packages/kbn-plugin-helpers/src/{tasks/test/mocha/test_mocha_task.ts => load_kibana_platform_plugin.ts} (52%) rename packages/kbn-plugin-helpers/src/{lib/tasks.ts => resolve_kibana_version.ts} (58%) delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/README.md delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/build_task.ts delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/create_build.ts delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/git_info.ts delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/index.ts delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/build_action_test_plugin/package.json delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/build_action_test_plugin/translations/es.json delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/create_build_test_plugin/index.js delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/create_build_test_plugin/package.json delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/create_build_test_plugin/translations/es.json delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/create_package_test_plugin/index.js delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/create_package_test_plugin/package.json delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/create_package_test_plugin/translations/es.json delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__snapshots__/build_action.test.js.snap delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/build_action.test.js delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/create_build.test.js delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/create_package.test.js delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/rewrite_package_json.ts rename packages/kbn-plugin-helpers/src/{lib/docs.ts => tasks/clean.ts} (62%) rename packages/kbn-plugin-helpers/src/tasks/{build/create_package.ts => create_archive.ts} (53%) rename packages/kbn-plugin-helpers/src/{lib => tasks}/index.ts (83%) create mode 100644 packages/kbn-plugin-helpers/src/tasks/optimize.ts delete mode 100644 packages/kbn-plugin-helpers/src/tasks/start/README.md delete mode 100644 packages/kbn-plugin-helpers/src/tasks/start/start_task.ts delete mode 100644 packages/kbn-plugin-helpers/src/tasks/test/mocha/README.md delete mode 100644 packages/kbn-plugin-helpers/src/tasks/test/mocha/index.ts create mode 100644 packages/kbn-plugin-helpers/src/tasks/write_server_files.ts create mode 100644 packages/kbn-plugin-helpers/src/tasks/yarn_install.ts rename packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/build_action_test_plugin/index.js => scripts/plugin_helpers.js (88%) delete mode 100644 x-pack/.kibana-plugin-helpers.json delete mode 100644 x-pack/tasks/dev.ts diff --git a/docs/developer/plugin/external-plugin-functional-tests.asciidoc b/docs/developer/plugin/external-plugin-functional-tests.asciidoc index 706bf6af8ed9b..7e5b5b79d06e9 100644 --- a/docs/developer/plugin/external-plugin-functional-tests.asciidoc +++ b/docs/developer/plugin/external-plugin-functional-tests.asciidoc @@ -13,7 +13,7 @@ To get started copy and paste this example to `test/functional/config.js`: ["source","js"] ----------- import { resolve } from 'path'; -import { resolveKibanaPath } from '@kbn/plugin-helpers'; +import { REPO_ROOT } from '@kbn/dev-utils'; import { MyServiceProvider } from './services/my_service'; import { MyAppPageProvider } from './services/my_app_page'; @@ -24,7 +24,7 @@ export default async function ({ readConfigFile }) { // read the {kib} config file so that we can utilize some of // its services and PageObjects - const kibanaConfig = await readConfigFile(resolveKibanaPath('test/functional/config.js')); + const kibanaConfig = await readConfigFile(resolve(REPO_ROOT, 'test/functional/config.js')); return { // list paths to the files that contain your plugins tests diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index 768a67794517f..4f6f995f38f31 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -1,32 +1,38 @@ { "name": "@kbn/dev-utils", - "main": "./target/index.js", "version": "1.0.0", - "license": "Apache-2.0", "private": true, + "license": "Apache-2.0", + "main": "./target/index.js", "scripts": { "build": "tsc", "kbn:bootstrap": "yarn build", "kbn:watch": "yarn build --watch" }, "dependencies": { + "@babel/core": "^7.11.1", "axios": "^0.19.0", "chalk": "^4.1.0", + "cheerio": "0.22.0", "dedent": "^0.7.0", "execa": "^4.0.2", "exit-hook": "^2.2.0", "getopts": "^2.2.5", + "globby": "^8.0.1", "load-json-file": "^6.2.0", - "normalize-path": "^3.0.0", + "markdown-it": "^10.0.0", "moment": "^2.24.0", + "normalize-path": "^3.0.0", "rxjs": "^6.5.5", "strip-ansi": "^6.0.0", "tree-kill": "^1.2.2", - "tslib": "^2.0.0" + "vinyl": "^2.2.0" }, "devDependencies": { - "typescript": "4.0.2", + "@kbn/babel-preset": "1.0.0", "@kbn/expect": "1.0.0", - "chance": "1.0.18" + "@types/vinyl": "^2.0.4", + "chance": "1.0.18", + "typescript": "4.0.2" } } diff --git a/packages/kbn-dev-utils/src/babel.ts b/packages/kbn-dev-utils/src/babel.ts new file mode 100644 index 0000000000000..e48fe81d0232c --- /dev/null +++ b/packages/kbn-dev-utils/src/babel.ts @@ -0,0 +1,59 @@ +/* + * 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 File from 'vinyl'; +import * as Babel from '@babel/core'; + +const transformedFiles = new WeakSet(); + +/** + * Returns a promise that resolves when the file has been + * mutated so the contents of the file are tranformed with + * babel, include inline sourcemaps, and the filename has + * been updated to use .js. + * + * If the file was previously transformed with this function + * the promise will just resolve immediately. + */ +export async function transformFileWithBabel(file: File) { + if (!(file.contents instanceof Buffer)) { + throw new Error('file must be buffered'); + } + + if (transformedFiles.has(file)) { + return; + } + + const source = file.contents.toString('utf8'); + const result = await Babel.transformAsync(source, { + babelrc: false, + configFile: false, + sourceMaps: 'inline', + filename: file.path, + presets: [require.resolve('@kbn/babel-preset/node_preset')], + }); + + if (!result || typeof result.code !== 'string') { + throw new Error('babel transformation failed without an error...'); + } + + file.contents = Buffer.from(result.code); + file.extname = '.js'; + transformedFiles.add(file); +} diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 798746d159f60..2871fe2ffcf4a 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -41,3 +41,6 @@ export * from './stdio'; export * from './ci_stats_reporter'; export * from './plugin_list'; export * from './simple_kibana_platform_plugin_discovery'; +export * from './streams'; +export * from './babel'; +export * from './parse_kibana_platform_plugin'; diff --git a/packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts b/packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts new file mode 100644 index 0000000000000..83d8c2684d7ca --- /dev/null +++ b/packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts @@ -0,0 +1,59 @@ +/* + * 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 Path from 'path'; +import loadJsonFile from 'load-json-file'; + +export interface KibanaPlatformPlugin { + readonly directory: string; + readonly manifestPath: string; + readonly manifest: { + id: string; + ui: boolean; + server: boolean; + [key: string]: unknown; + }; +} + +export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformPlugin { + if (!Path.isAbsolute(manifestPath)) { + throw new TypeError('expected new platform manifest path to be absolute'); + } + + const manifest = loadJsonFile.sync(manifestPath); + if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { + throw new TypeError('expected new platform plugin manifest to be a JSON encoded object'); + } + + if (typeof manifest.id !== 'string') { + throw new TypeError('expected new platform plugin manifest to have a string id'); + } + + return { + directory: Path.dirname(manifestPath), + manifestPath, + manifest: { + ...manifest, + + ui: !!manifest.ui, + server: !!manifest.server, + id: manifest.id, + }, + }; +} diff --git a/packages/kbn-dev-utils/src/run/flags.ts b/packages/kbn-dev-utils/src/run/flags.ts index 12642bceca15a..54758b4a7dbf8 100644 --- a/packages/kbn-dev-utils/src/run/flags.ts +++ b/packages/kbn-dev-utils/src/run/flags.ts @@ -52,8 +52,8 @@ export function mergeFlagOptions(global: FlagOptions = {}, local: FlagOptions = boolean: [...(global.boolean || []), ...(local.boolean || [])], string: [...(global.string || []), ...(local.string || [])], default: { - ...global.alias, - ...local.alias, + ...global.default, + ...local.default, }, help: local.help, diff --git a/packages/kbn-dev-utils/src/serializers/index.ts b/packages/kbn-dev-utils/src/serializers/index.ts index e645a3be3fe5d..6e0ac0b8be029 100644 --- a/packages/kbn-dev-utils/src/serializers/index.ts +++ b/packages/kbn-dev-utils/src/serializers/index.ts @@ -21,3 +21,4 @@ export * from './absolute_path_serializer'; export * from './strip_ansi_serializer'; export * from './recursive_serializer'; export * from './any_instance_serizlizer'; +export * from './replace_serializer'; diff --git a/packages/kbn-dev-utils/src/serializers/replace_serializer.ts b/packages/kbn-dev-utils/src/serializers/replace_serializer.ts new file mode 100644 index 0000000000000..06096c4bee3a2 --- /dev/null +++ b/packages/kbn-dev-utils/src/serializers/replace_serializer.ts @@ -0,0 +1,36 @@ +/* + * 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 { createRecursiveSerializer } from './recursive_serializer'; + +type Replacer = (substring: string, ...args: any[]) => string; + +export function createReplaceSerializer( + toReplace: string | RegExp, + replaceWith: string | Replacer +) { + return createRecursiveSerializer( + typeof toReplace === 'string' + ? (v: any) => typeof v === 'string' && v.includes(toReplace) + : (v: any) => typeof v === 'string' && toReplace.test(v), + typeof replaceWith === 'string' + ? (v: string) => v.replace(toReplace, replaceWith) + : (v: string) => v.replace(toReplace, replaceWith) + ); +} diff --git a/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts b/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts index c7155b2b3c51b..c56d63edb9ac4 100644 --- a/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts +++ b/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts @@ -20,67 +20,37 @@ import Path from 'path'; import globby from 'globby'; -import loadJsonFile from 'load-json-file'; -export interface KibanaPlatformPlugin { - readonly directory: string; - readonly manifestPath: string; - readonly manifest: { - id: string; - [key: string]: unknown; - }; -} +import { parseKibanaPlatformPlugin } from './parse_kibana_platform_plugin'; /** * Helper to find the new platform plugins. */ -export function simpleKibanaPlatformPluginDiscovery(scanDirs: string[], paths: string[]) { +export function simpleKibanaPlatformPluginDiscovery(scanDirs: string[], pluginPaths: string[]) { const patterns = Array.from( new Set([ // find kibana.json files up to 5 levels within the scan dir ...scanDirs.reduce( (acc: string[], dir) => [ ...acc, - `${dir}/*/kibana.json`, - `${dir}/*/*/kibana.json`, - `${dir}/*/*/*/kibana.json`, - `${dir}/*/*/*/*/kibana.json`, - `${dir}/*/*/*/*/*/kibana.json`, + Path.resolve(dir, '*/kibana.json'), + Path.resolve(dir, '*/*/kibana.json'), + Path.resolve(dir, '*/*/*/kibana.json'), + Path.resolve(dir, '*/*/*/*/kibana.json'), + Path.resolve(dir, '*/*/*/*/*/kibana.json'), ], [] ), - ...paths.map((path) => `${path}/kibana.json`), + ...pluginPaths.map((path) => Path.resolve(path, `kibana.json`)), ]) ); const manifestPaths = globby.sync(patterns, { absolute: true }).map((path) => - // absolute paths returned from globby are using normalize or something so the path separators are `/` even on windows, Path.resolve solves this + // absolute paths returned from globby are using normalize or + // something so the path separators are `/` even on windows, + // Path.resolve solves this Path.resolve(path) ); - return manifestPaths.map( - (manifestPath): KibanaPlatformPlugin => { - if (!Path.isAbsolute(manifestPath)) { - throw new TypeError('expected new platform manifest path to be absolute'); - } - - const manifest = loadJsonFile.sync(manifestPath); - if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { - throw new TypeError('expected new platform plugin manifest to be a JSON encoded object'); - } - - if (typeof manifest.id !== 'string') { - throw new TypeError('expected new platform plugin manifest to have a string id'); - } - - return { - directory: Path.dirname(manifestPath), - manifestPath, - manifest: { - ...manifest, - id: manifest.id, - }, - }; - } - ); + return manifestPaths.map(parseKibanaPlatformPlugin); } diff --git a/packages/kbn-plugin-generator/src/streams.ts b/packages/kbn-dev-utils/src/streams.ts similarity index 62% rename from packages/kbn-plugin-generator/src/streams.ts rename to packages/kbn-dev-utils/src/streams.ts index 976008e879dd3..6a868f648e78d 100644 --- a/packages/kbn-plugin-generator/src/streams.ts +++ b/packages/kbn-dev-utils/src/streams.ts @@ -20,7 +20,6 @@ import { Transform } from 'stream'; import File from 'vinyl'; -import { Minimatch } from 'minimatch'; interface BufferedFile extends File { contents: Buffer; @@ -33,41 +32,31 @@ interface BufferedFile extends File { * mutate the file, replace it with another file (return a new File * object), or drop it from the stream (return null) */ -export const tapFileStream = ( +export const transformFileStream = ( fn: (file: BufferedFile) => File | void | null | Promise ) => new Transform({ objectMode: true, - transform(file: BufferedFile, _, cb) { - Promise.resolve(file) - .then(fn) - .then( - (result) => { - // drop the file when null is returned - if (result === null) { - cb(); - } else { - cb(undefined, result || file); - } - }, - (error) => cb(error) - ); - }, - }); + transform(file: File, _, cb) { + Promise.resolve() + .then(async () => { + if (file.isDirectory()) { + return cb(undefined, file); + } -export const excludeFiles = (globs: string[]) => { - const patterns = globs.map( - (g) => - new Minimatch(g, { - matchBase: true, - }) - ); + if (!(file.contents instanceof Buffer)) { + throw new Error('files must be buffered to use transformFileStream()'); + } - return tapFileStream((file) => { - const path = file.relative.replace(/\.ejs$/, ''); - const exclude = patterns.some((p) => p.match(path)); - if (exclude) { - return null; - } + const result = await fn(file as BufferedFile); + + if (result === null) { + // explicitly drop file if null is returned + cb(); + } else { + cb(undefined, result || file); + } + }) + .catch(cb); + }, }); -}; diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts index a848d779dc9a2..8a3379211927b 100644 --- a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts +++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts @@ -50,7 +50,7 @@ export function findKibanaPlatformPlugins(scanDirs: string[], paths: string[]) { directory, manifestPath, id: manifest.id, - isUiPlugin: !!manifest.ui, + isUiPlugin: manifest.ui, extraPublicDirs: extraPublicDirs || [], }; } diff --git a/packages/kbn-plugin-generator/src/plugin_types.ts b/packages/kbn-plugin-generator/src/plugin_types.ts new file mode 100644 index 0000000000000..ae5201f4e8dbb --- /dev/null +++ b/packages/kbn-plugin-generator/src/plugin_types.ts @@ -0,0 +1,60 @@ +/* + * 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 Path from 'path'; + +import { REPO_ROOT } from '@kbn/dev-utils'; + +export interface PluginType { + thirdParty: boolean; + installDir: string; +} + +export const PLUGIN_TYPE_OPTIONS: Array<{ name: string; value: PluginType }> = [ + { + name: 'Installable plugin', + value: { thirdParty: true, installDir: Path.resolve(REPO_ROOT, 'plugins') }, + }, + { + name: 'Kibana Example', + value: { thirdParty: false, installDir: Path.resolve(REPO_ROOT, 'examples') }, + }, + { + name: 'Kibana OSS', + value: { thirdParty: false, installDir: Path.resolve(REPO_ROOT, 'src/plugins') }, + }, + { + name: 'Kibana OSS Functional Testing', + value: { + thirdParty: false, + installDir: Path.resolve(REPO_ROOT, 'test/plugin_functional/plugins'), + }, + }, + { + name: 'X-Pack', + value: { thirdParty: false, installDir: Path.resolve(REPO_ROOT, 'x-pack/plugins') }, + }, + { + name: 'X-Pack Functional Testing', + value: { + thirdParty: false, + installDir: Path.resolve(REPO_ROOT, 'x-pack/test/plugin_functional/plugins'), + }, + }, +]; diff --git a/packages/kbn-plugin-generator/src/render_template.ts b/packages/kbn-plugin-generator/src/render_template.ts index 18bdcf1be1a6b..894088c119651 100644 --- a/packages/kbn-plugin-generator/src/render_template.ts +++ b/packages/kbn-plugin-generator/src/render_template.ts @@ -23,15 +23,32 @@ import { promisify } from 'util'; import vfs from 'vinyl-fs'; import prettier from 'prettier'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT, transformFileStream } from '@kbn/dev-utils'; import ejs from 'ejs'; +import { Minimatch } from 'minimatch'; import { snakeCase, camelCase, upperCamelCase } from './casing'; -import { excludeFiles, tapFileStream } from './streams'; import { Answers } from './ask_questions'; const asyncPipeline = promisify(pipeline); +const excludeFiles = (globs: string[]) => { + const patterns = globs.map( + (g) => + new Minimatch(g, { + matchBase: true, + }) + ); + + return transformFileStream((file) => { + const path = file.relative.replace(/\.ejs$/, ''); + const exclude = patterns.some((p) => p.match(path)); + if (exclude) { + return null; + } + }); +}; + /** * Stream all the files from the template directory, ignoring * certain files based on the answers, process the .ejs templates @@ -82,7 +99,7 @@ export async function renderTemplates({ ), // render .ejs templates and rename to not use .ejs extension - tapFileStream((file) => { + transformFileStream((file) => { if (file.extname !== '.ejs') { return; } @@ -108,7 +125,7 @@ export async function renderTemplates({ }), // format each file with prettier - tapFileStream((file) => { + transformFileStream((file) => { if (!file.extname) { return; } diff --git a/packages/kbn-plugin-generator/template/README.md.ejs b/packages/kbn-plugin-generator/template/README.md.ejs index 5f30bf0463305..2cd19c904263e 100755 --- a/packages/kbn-plugin-generator/template/README.md.ejs +++ b/packages/kbn-plugin-generator/template/README.md.ejs @@ -7,3 +7,14 @@ A Kibana plugin ## Development See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. + +<% if (thirdPartyPlugin) { %> +## Scripts +
+
yarn kbn bootstrap
+
Execute this to install node_modules and setup the dependencies in your plugin and in Kibana
+ +
yarn plugin-helpers build
+
Execute this to create a distributable version of this plugin that can be installed in Kibana
+
+<% } %> diff --git a/packages/kbn-plugin-generator/template/package.json.ejs b/packages/kbn-plugin-generator/template/package.json.ejs index cbd59894ca47c..ab234b1df2bc5 100644 --- a/packages/kbn-plugin-generator/template/package.json.ejs +++ b/packages/kbn-plugin-generator/template/package.json.ejs @@ -3,6 +3,8 @@ "version": "0.0.0", "private": true, "scripts": { + "build": "yarn plugin-helpers build", + "plugin-helpers": "node ../../scripts/plugin_helpers", "kbn": "node ../../scripts/kbn" } } diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index ba39528a1f809..129c58a4b4174 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -1,43 +1,32 @@ { "name": "@kbn/plugin-helpers", - "version": "9.0.2", + "version": "1.0.0", "private": true, "description": "Just some helpers for kibana plugin devs.", "license": "Apache-2.0", - "main": "target/lib/index.js", - "scripts": { - "kbn:bootstrap": "tsc" - }, + "main": "target/index.js", "bin": { "plugin-helpers": "bin/plugin-helpers.js" }, + "scripts": { + "kbn:bootstrap": "rm -rf target && tsc", + "kbn:watch": "tsc --watch" + }, "dependencies": { - "@babel/core": "^7.11.1", - "argv-split": "^2.0.1", - "commander": "^3.0.0", + "@kbn/dev-utils": "1.0.0", + "@kbn/optimizer": "1.0.0", "del": "^5.1.0", "execa": "^4.0.2", - "globby": "^8.0.1", - "gulp-babel": "^8.0.0", - "gulp-rename": "1.4.0", - "gulp-zip": "5.0.1", + "gulp-zip": "^5.0.2", "inquirer": "^1.2.2", - "minimatch": "^3.0.4", - "through2": "^2.0.3", - "through2-map": "^3.0.0", - "vinyl": "^2.2.0", + "load-json-file": "^6.2.0", "vinyl-fs": "^3.0.3" }, "devDependencies": { - "@types/gulp-rename": "^0.0.33", + "@types/decompress": "^4.2.3", "@types/gulp-zip": "^4.0.1", "@types/inquirer": "^6.5.0", - "@types/through2": "^2.0.35", - "@types/through2-map": "^3.0.0", - "@types/vinyl": "^2.0.4", + "decompress": "^4.2.1", "typescript": "4.0.2" - }, - "peerDependencies": { - "@kbn/babel-preset": "1.0.0" } } diff --git a/packages/kbn-plugin-helpers/bin/plugin-helpers.js b/packages/kbn-plugin-helpers/src/build_context.ts old mode 100755 new mode 100644 similarity index 73% rename from packages/kbn-plugin-helpers/bin/plugin-helpers.js rename to packages/kbn-plugin-helpers/src/build_context.ts index 175ff1019fa2d..62300d5a34e49 --- a/packages/kbn-plugin-helpers/bin/plugin-helpers.js +++ b/packages/kbn-plugin-helpers/src/build_context.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env node - /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -19,10 +17,16 @@ * under the License. */ -const nodeMajorVersion = parseFloat(process.version.replace(/^v(\d+)\..+/, '$1')); -if (nodeMajorVersion < 6) { - console.error('FATAL: kibana-plugin-helpers requires node 6+'); - process.exit(1); -} +import { ToolingLog } from '@kbn/dev-utils'; -require('../target/cli'); +import { Plugin } from './load_kibana_platform_plugin'; +import { Config } from './config'; + +export interface BuildContext { + log: ToolingLog; + plugin: Plugin; + config: Config; + sourceDir: string; + buildDir: string; + kibanaVersion: string; +} diff --git a/packages/kbn-plugin-helpers/src/cli.ts b/packages/kbn-plugin-helpers/src/cli.ts index 18ddc62cba8a6..21b6559f63650 100644 --- a/packages/kbn-plugin-helpers/src/cli.ts +++ b/packages/kbn-plugin-helpers/src/cli.ts @@ -17,59 +17,86 @@ * under the License. */ -import Fs from 'fs'; import Path from 'path'; -import program from 'commander'; +import { RunWithCommands, createFlagError, createFailError } from '@kbn/dev-utils'; -import { createCommanderAction } from './lib/commander_action'; -import { docs } from './lib/docs'; -import { enableCollectingUnknownOptions } from './lib/enable_collecting_unknown_options'; +import { findKibanaJson } from './find_kibana_json'; +import { loadKibanaPlatformPlugin } from './load_kibana_platform_plugin'; +import * as Tasks from './tasks'; +import { BuildContext } from './build_context'; +import { resolveKibanaVersion } from './resolve_kibana_version'; +import { loadConfig } from './config'; -const pkg = JSON.parse(Fs.readFileSync(Path.resolve(__dirname, '../package.json'), 'utf8')); -program.version(pkg.version); +export function runCli() { + new RunWithCommands({ + description: 'Some helper tasks for plugin-authors', + }) + .command({ + name: 'build', + description: ` + Copies files from the source into a zip archive that can be distributed for + installation into production Kibana installs. The archive includes the non- + development npm dependencies and builds itself using raw files in the source + directory so make sure they are clean/up to date. The resulting archive can + be found at: -enableCollectingUnknownOptions( - program - .command('start') - .description('Start kibana and have it include this plugin') - .on('--help', docs('start')) - .action( - createCommanderAction('start', (command) => ({ - flags: command.unknownOptions, - })) - ) -); + build/{plugin.id}-{kibanaVersion}.zip -program - .command('build [files...]') - .description('Build a distributable archive') - .on('--help', docs('build')) - .option('--skip-archive', "Don't create the zip file, leave the build path alone") - .option( - '-d, --build-destination ', - 'Target path for the build output, absolute or relative to the plugin root' - ) - .option('-b, --build-version ', 'Version for the build output') - .option('-k, --kibana-version ', 'Kibana version for the build output') - .action( - createCommanderAction('build', (command, files) => ({ - buildDestination: command.buildDestination, - buildVersion: command.buildVersion, - kibanaVersion: command.kibanaVersion, - skipArchive: Boolean(command.skipArchive), - files, - })) - ); + `, + flags: { + boolean: ['skip-archive'], + string: ['kibana-version'], + alias: { + k: 'kibana-version', + }, + help: ` + --skip-archive Don't create the zip file, just create the build/kibana directory + --kibana-version, -v Kibana version that the + `, + }, + async run({ log, flags }) { + const versionFlag = flags['kibana-version']; + if (versionFlag !== undefined && typeof versionFlag !== 'string') { + throw createFlagError('expected a single --kibana-version flag'); + } -program - .command('test:mocha [files...]') - .description('Run the server tests using mocha') - .on('--help', docs('test/mocha')) - .action( - createCommanderAction('testMocha', (command, files) => ({ - files, - })) - ); + const skipArchive = flags['skip-archive']; + if (skipArchive !== undefined && typeof skipArchive !== 'boolean') { + throw createFlagError('expected a single --skip-archive flag'); + } -program.parse(process.argv); + const pluginDir = await findKibanaJson(process.cwd()); + if (!pluginDir) { + throw createFailError( + `Unable to find Kibana Platform plugin in [${process.cwd()}] or any of its parent directories. Has it been migrated properly? Does it have a kibana.json file?` + ); + } + + const plugin = loadKibanaPlatformPlugin(pluginDir); + const config = await loadConfig(log, plugin); + const kibanaVersion = await resolveKibanaVersion(versionFlag, plugin); + const sourceDir = plugin.directory; + const buildDir = Path.resolve(plugin.directory, 'build/kibana', plugin.manifest.id); + + const context: BuildContext = { + log, + plugin, + config, + sourceDir, + buildDir, + kibanaVersion, + }; + + await Tasks.initTargets(context); + await Tasks.optimize(context); + await Tasks.writeServerFiles(context); + await Tasks.yarnInstall(context); + + if (skipArchive !== true) { + await Tasks.createArchive(context); + } + }, + }) + .execute(); +} diff --git a/packages/kbn-plugin-helpers/src/config.ts b/packages/kbn-plugin-helpers/src/config.ts new file mode 100644 index 0000000000000..bd5ad8ab6acc7 --- /dev/null +++ b/packages/kbn-plugin-helpers/src/config.ts @@ -0,0 +1,83 @@ +/* + * 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 Path from 'path'; + +import loadJsonFile from 'load-json-file'; + +import { ToolingLog } from '@kbn/dev-utils'; +import { Plugin } from './load_kibana_platform_plugin'; + +export interface Config { + skipInstallDependencies: boolean; + serverSourcePatterns?: string[]; +} + +const isArrayOfStrings = (v: any): v is string[] => + Array.isArray(v) && v.every((p) => typeof p === 'string'); + +export async function loadConfig(log: ToolingLog, plugin: Plugin): Promise { + try { + const path = Path.resolve(plugin.directory, '.kibana-plugin-helpers.json'); + const file = await loadJsonFile(path); + + if (!(typeof file === 'object' && file && !Array.isArray(file))) { + throw new TypeError(`expected config at [${path}] to be an object`); + } + + const { + skipInstallDependencies = false, + buildSourcePatterns, + serverSourcePatterns, + ...rest + } = file; + + if (typeof skipInstallDependencies !== 'boolean') { + throw new TypeError(`expected [skipInstallDependencies] at [${path}] to be a boolean`); + } + + if (buildSourcePatterns) { + log.warning( + `DEPRECATED: rename [buildSourcePatterns] to [serverSourcePatterns] in [${path}]` + ); + } + const ssp = buildSourcePatterns || serverSourcePatterns; + if (ssp !== undefined && !isArrayOfStrings(ssp)) { + throw new TypeError(`expected [serverSourcePatterns] at [${path}] to be an array of strings`); + } + + if (Object.keys(rest).length) { + throw new TypeError(`unexpected key in [${path}]: ${Object.keys(rest).join(', ')}`); + } + + log.info(`Loaded config file from [${path}]`); + return { + skipInstallDependencies, + serverSourcePatterns: ssp, + }; + } catch (error) { + if (error.code === 'ENOENT') { + return { + skipInstallDependencies: false, + }; + } + + throw error; + } +} diff --git a/packages/kbn-plugin-helpers/src/lib/run.ts b/packages/kbn-plugin-helpers/src/find_kibana_json.ts similarity index 65% rename from packages/kbn-plugin-helpers/src/lib/run.ts rename to packages/kbn-plugin-helpers/src/find_kibana_json.ts index 2b1a2a63c1074..9340309056830 100644 --- a/packages/kbn-plugin-helpers/src/lib/run.ts +++ b/packages/kbn-plugin-helpers/src/find_kibana_json.ts @@ -17,21 +17,21 @@ * under the License. */ -import { pluginConfig, PluginConfig } from './plugin_config'; -import { tasks, Tasks } from './tasks'; +import Path from 'path'; +import Fs from 'fs'; +import { promisify } from 'util'; -export interface TaskContext { - plugin: PluginConfig; - run: typeof run; - options?: any; -} +const existsAsync = promisify(Fs.exists); + +export async function findKibanaJson(directory: string): Promise { + if (await existsAsync(Path.resolve(directory, 'kibana.json'))) { + return directory; + } -export function run(name: keyof Tasks, options?: any) { - const action = tasks[name]; - if (!action) { - throw new Error('Invalid task: "' + name + '"'); + const parent = Path.dirname(directory); + if (parent === directory) { + return undefined; } - const plugin = pluginConfig(); - return action({ plugin, run, options }); + return findKibanaJson(parent); } diff --git a/packages/kbn-plugin-helpers/src/tasks/start/index.ts b/packages/kbn-plugin-helpers/src/index.ts similarity index 96% rename from packages/kbn-plugin-helpers/src/tasks/start/index.ts rename to packages/kbn-plugin-helpers/src/index.ts index cf34bdbadf416..a05bc698bde17 100644 --- a/packages/kbn-plugin-helpers/src/tasks/start/index.ts +++ b/packages/kbn-plugin-helpers/src/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export * from './start_task'; +export * from './cli'; diff --git a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts new file mode 100644 index 0000000000000..62f83cd672f3d --- /dev/null +++ b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts @@ -0,0 +1,123 @@ +/* + * 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 Path from 'path'; +import Fs from 'fs'; + +import execa from 'execa'; +import { createStripAnsiSerializer, REPO_ROOT, createReplaceSerializer } from '@kbn/dev-utils'; +import decompress from 'decompress'; +import del from 'del'; +import globby from 'globby'; +import loadJsonFile from 'load-json-file'; + +const PLUGIN_DIR = Path.resolve(REPO_ROOT, 'plugins/foo_test_plugin'); +const PLUGIN_BUILD_DIR = Path.resolve(PLUGIN_DIR, 'build'); +const PLUGIN_ARCHIVE = Path.resolve(PLUGIN_BUILD_DIR, `fooTestPlugin-7.5.0.zip`); +const TMP_DIR = Path.resolve(__dirname, '__tmp__'); + +expect.addSnapshotSerializer(createReplaceSerializer(/[\d\.]+ sec/g, '