({ rawResponse: { @@ -35,9 +36,13 @@ jest.spyOn(RxApi, 'lastValueFrom').mockImplementation(async () => ({ })); async function mountAndFindSubjects( - props: Omit + props: Omit< + DiscoverNoResultsProps, + 'onDisableFilters' | 'data' | 'isTimeBased' | 'stateContainer' + > ) { const services = createDiscoverServicesMock(); + const isTimeBased = props.dataView.isTimeBased(); let component: ReactWrapper; @@ -45,7 +50,8 @@ async function mountAndFindSubjects( component = await mountWithIntl( {}} {...props} /> diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results.tsx index bd010502df149..86f73e18ca4d0 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results.tsx @@ -10,10 +10,14 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { AggregateQuery, Filter, Query } from '@kbn/es-query'; +import { SearchResponseWarnings } from '@kbn/search-response-warnings'; import { NoResultsSuggestions } from './no_results_suggestions'; +import type { DiscoverStateContainer } from '../../services/discover_state'; +import { useDataState } from '../../hooks/use_data_state'; import './_no_results.scss'; export interface DiscoverNoResultsProps { + stateContainer: DiscoverStateContainer; isTimeBased?: boolean; query: Query | AggregateQuery | undefined; filters: Filter[] | undefined; @@ -22,12 +26,26 @@ export interface DiscoverNoResultsProps { } export function DiscoverNoResults({ + stateContainer, isTimeBased, query, filters, dataView, onDisableFilters, }: DiscoverNoResultsProps) { + const { documents$ } = stateContainer.dataState.data$; + const interceptedWarnings = useDataState(documents$).interceptedWarnings; + + if (interceptedWarnings?.length) { + return ( + + ); + } + return ( diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx index c55e8de773942..633f082c4792b 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx @@ -121,6 +121,7 @@ export const NoResultsSuggestions: React.FC = ({ layout="horizontal" color="plain" icon={} + hasBorder title={

{ + .then(({ records, textBasedQueryColumns, interceptedWarnings }) => { if (services.analytics) { const duration = window.performance.now() - startTime; reportPerformanceMetricEvent(services.analytics, { @@ -131,6 +131,7 @@ export function fetchAll( fetchStatus, result: records, textBasedQueryColumns, + interceptedWarnings, recordRawType, query, }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.ts index 4a4e388a27367..bce5f266d6def 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.ts @@ -11,7 +11,9 @@ import { lastValueFrom } from 'rxjs'; import { isCompleteResponse, ISearchSource } from '@kbn/data-plugin/public'; import { SAMPLE_SIZE_SETTING, buildDataTableRecordList } from '@kbn/discover-utils'; import type { EsHitRecord } from '@kbn/discover-utils/types'; +import { getSearchResponseInterceptedWarnings } from '@kbn/search-response-warnings'; import type { RecordsFetchResponse } from '../../../types'; +import { DISABLE_SHARD_FAILURE_WARNING } from '../../../../common/constants'; import { FetchDeps } from './fetch_all'; /** @@ -53,6 +55,7 @@ export const fetchDocuments = ( }), }, executionContext, + disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, }) .pipe( filter((res) => isCompleteResponse(res)), @@ -61,5 +64,21 @@ export const fetchDocuments = ( }) ); - return lastValueFrom(fetch$).then((records) => ({ records })); + return lastValueFrom(fetch$).then((records) => { + const adapter = inspectorAdapters.requests; + const interceptedWarnings = adapter + ? getSearchResponseInterceptedWarnings({ + services, + adapter, + options: { + disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, + }, + }) + : []; + + return { + records, + interceptedWarnings, + }; + }); }; diff --git a/src/plugins/discover/public/components/common/error_callout.tsx b/src/plugins/discover/public/components/common/error_callout.tsx index 0f1fbb722bf82..d0e914a81e851 100644 --- a/src/plugins/discover/public/components/common/error_callout.tsx +++ b/src/plugins/discover/public/components/common/error_callout.tsx @@ -10,9 +10,6 @@ import { EuiButton, EuiCallOut, EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, EuiLink, EuiModal, EuiModalBody, @@ -104,36 +101,31 @@ export const ErrorCallout = ({ /> ) : ( - - - - -

{formattedTitle}

-
- - } + title={

{formattedTitle}

} body={ - overrideDisplay?.body ?? ( - <> -

- {error.message} -

- {showErrorMessage} - - ) +
+ {overrideDisplay?.body ?? ( + <> +

+ {error.message} +

+ {showErrorMessage} + + )} +
} - css={css` - text-align: left; - `} data-test-subj={dataTestSubj} /> )} diff --git a/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx b/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx index e45faec8cbaa1..570c980e649e5 100644 --- a/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx +++ b/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx @@ -33,6 +33,7 @@ export function DiscoverDocTableEmbeddable(renderProps: DocTableEmbeddableProps) sharedItemTitle={renderProps.sharedItemTitle} isLoading={renderProps.isLoading} isPlainRecord={renderProps.isPlainRecord} + interceptedWarnings={renderProps.interceptedWarnings} dataTestSubj="embeddedSavedSearchDocTable" DocViewer={DocViewer} /> diff --git a/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx b/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx index 97ed5f3af9d14..6901df855984e 100644 --- a/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx +++ b/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx @@ -11,6 +11,7 @@ import './index.scss'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiText } from '@elastic/eui'; import { SAMPLE_SIZE_SETTING, usePager } from '@kbn/discover-utils'; +import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings'; import { ToolBarPagination, MAX_ROWS_PER_PAGE_OPTION, @@ -22,6 +23,7 @@ import { SavedSearchEmbeddableBase } from '../../embeddable/saved_search_embedda export interface DocTableEmbeddableProps extends DocTableProps { totalHitCount: number; rowsPerPageState?: number; + interceptedWarnings?: SearchResponseInterceptedWarning[]; onUpdateRowsPerPage?: (rowsPerPage?: number) => void; } @@ -101,6 +103,7 @@ export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => { return ( & filter?: (field: DataViewField, value: string[], operator: string) => void; hits?: DataTableRecord[]; totalHitCount?: number; + interceptedWarnings?: SearchResponseInterceptedWarning[]; onMoveColumn?: (column: string, index: number) => void; onUpdateRowHeight?: (rowHeight?: number) => void; onUpdateRowsPerPage?: (rowsPerPage?: number) => void; @@ -279,6 +284,7 @@ export class SavedSearchEmbeddable this.inspectorAdapters.requests!.reset(); searchProps.isLoading = true; + searchProps.interceptedWarnings = undefined; const wasAlreadyRendered = this.getOutput().rendered; @@ -357,9 +363,20 @@ export class SavedSearchEmbeddable }), }, executionContext, + disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, }) ); + if (this.inspectorAdapters.requests) { + searchProps.interceptedWarnings = getSearchResponseInterceptedWarnings({ + services: this.services, + adapter: this.inspectorAdapters.requests, + options: { + disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, + }, + }); + } + this.updateOutput({ ...this.getOutput(), loading: false, diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable_badge.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable_badge.tsx new file mode 100644 index 0000000000000..9944adb4be33c --- /dev/null +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable_badge.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + SearchResponseWarnings, + type SearchResponseInterceptedWarning, +} from '@kbn/search-response-warnings'; + +export interface SavedSearchEmbeddableBadgeProps { + interceptedWarnings: SearchResponseInterceptedWarning[] | undefined; +} + +export const SavedSearchEmbeddableBadge: React.FC = ({ + interceptedWarnings, +}) => { + return interceptedWarnings?.length ? ( + + ) : null; +}; diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable_base.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable_base.tsx index 17785570b9487..b41c70676c754 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable_base.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable_base.tsx @@ -9,7 +9,9 @@ import React from 'react'; import { css } from '@emotion/react'; import { EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; +import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings'; import { TotalDocuments } from '../application/main/components/total_documents/total_documents'; +import { SavedSearchEmbeddableBadge } from './saved_search_embeddable_badge'; const containerStyles = css` width: 100%; @@ -22,6 +24,7 @@ export interface SavedSearchEmbeddableBaseProps { prepend?: React.ReactElement; append?: React.ReactElement; dataTestSubj?: string; + interceptedWarnings?: SearchResponseInterceptedWarning[]; } export const SavedSearchEmbeddableBase: React.FC = ({ @@ -30,6 +33,7 @@ export const SavedSearchEmbeddableBase: React.FC prepend, append, dataTestSubj, + interceptedWarnings, children, }) => { return ( @@ -62,6 +66,12 @@ export const SavedSearchEmbeddableBase: React.FC {children} {Boolean(append) && {append}} + + {Boolean(interceptedWarnings?.length) && ( +
+ +
+ )} ); }; diff --git a/src/plugins/discover/public/embeddable/saved_search_grid.tsx b/src/plugins/discover/public/embeddable/saved_search_grid.tsx index 075a3ca930235..87258347b474e 100644 --- a/src/plugins/discover/public/embeddable/saved_search_grid.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_grid.tsx @@ -7,6 +7,7 @@ */ import React, { useState, memo } from 'react'; import type { DataTableRecord } from '@kbn/discover-utils/types'; +import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings'; import { DiscoverGrid, DiscoverGridProps } from '../components/discover_grid/discover_grid'; import './saved_search_grid.scss'; import { DiscoverGridFlyout } from '../components/discover_grid/discover_grid_flyout'; @@ -14,11 +15,13 @@ import { SavedSearchEmbeddableBase } from './saved_search_embeddable_base'; export interface DiscoverGridEmbeddableProps extends DiscoverGridProps { totalHitCount: number; + interceptedWarnings?: SearchResponseInterceptedWarning[]; } export const DataGridMemoized = memo(DiscoverGrid); export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { + const { interceptedWarnings, ...gridProps } = props; const [expandedDoc, setExpandedDoc] = useState(undefined); return ( @@ -26,9 +29,10 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { totalHitCount={props.totalHitCount} isLoading={props.isLoading} dataTestSubj="embeddedSavedSearchDocTable" + interceptedWarnings={props.interceptedWarnings} > { @@ -32,11 +33,14 @@ export interface ValueClickContext { export interface MultiValueClickContext { embeddable?: T; data: { - data: { + data: Array<{ table: Pick; - column: number; - value: any[]; - }; + cells: Array<{ + column: number; + row: number; + }>; + relation?: BooleanRelation; + }>; timeFieldName?: string; negate?: boolean; }; @@ -157,12 +161,27 @@ export const cellValueTrigger: Trigger = { export const isValueClickTriggerContext = ( context: ChartActionContext -): context is ValueClickContext => context.data && 'data' in context.data; +): context is ValueClickContext => { + return ( + context.data && + 'data' in context.data && + Array.isArray(context.data.data) && + context.data.data.length > 0 && + 'column' in context.data.data[0] + ); +}; export const isMultiValueClickTriggerContext = ( context: ChartActionContext -): context is MultiValueClickContext => - context.data && 'data' in context.data && !Array.isArray(context.data.data); +): context is MultiValueClickContext => { + return ( + context.data && + 'data' in context.data && + Array.isArray(context.data.data) && + context.data.data.length > 0 && + 'cells' in context.data.data[0] + ); +}; export const isRangeSelectTriggerContext = ( context: ChartActionContext diff --git a/src/plugins/unified_histogram/public/chart/histogram.test.tsx b/src/plugins/unified_histogram/public/chart/histogram.test.tsx index e935e3a05aa87..4c651f0b5e391 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.test.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.test.tsx @@ -207,6 +207,7 @@ describe('Histogram', () => { const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; const onLoad = component.find(embeddable).props().onLoad; const adapters = createDefaultInspectorAdapters(); + adapters.tables.tables.unifiedHistogram = { meta: { statistics: { totalCount: 100 } } } as any; const rawResponse = { _shards: { total: 1, @@ -215,14 +216,21 @@ describe('Histogram', () => { failed: 1, failures: [], }, + hits: { + total: 100, + max_score: null, + hits: [], + }, }; jest .spyOn(adapters.requests, 'getRequests') .mockReturnValue([{ response: { json: { rawResponse } } } as any]); - onLoad(false, adapters); + act(() => { + onLoad(false, adapters); + }); expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( - UnifiedHistogramFetchStatus.error, - undefined + UnifiedHistogramFetchStatus.complete, + 100 ); expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters }); }); diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 9983f2e0841dd..761e701e8f9a6 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -101,10 +101,10 @@ export function Histogram({ | undefined; const response = json?.rawResponse; - // Lens will swallow shard failures and return `isLoading: false` because it displays - // its own errors, but this causes us to emit onTotalHitsChange(UnifiedHistogramFetchStatus.complete, 0). - // This is incorrect, so we check for request failures and shard failures here, and emit an error instead. - if (requestFailed || response?._shards.failed) { + // The response can have `response?._shards.failed` but we should still be able to show hits number + // TODO: show shards warnings as a badge next to the total hits number + + if (requestFailed) { onTotalHitsChange?.(UnifiedHistogramFetchStatus.error, undefined); onChartLoad?.({ adapters: adapters ?? {} }); return; diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts b/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts index 6903cdf6b4256..c260d3171697b 100644 --- a/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts +++ b/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts @@ -206,6 +206,7 @@ const fetchTotalHitsSearchSource = async ({ executionContext: { description: 'fetch total hits', }, + disableShardFailureWarning: true, // TODO: show warnings as a badge next to total hits number }) .pipe( filter((res) => isCompleteResponse(res)), diff --git a/src/setup_node_env/dns_ipv4_first.js b/src/setup_node_env/dns_ipv4_first.js new file mode 100644 index 0000000000000..75dd876c46d35 --- /dev/null +++ b/src/setup_node_env/dns_ipv4_first.js @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// enables Node 16 default DNS lookup behavior for the current thread +require('dns').setDefaultResultOrder('ipv4first'); + +// overrides current process node options, so it can be restored in worker threads too +process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS || ''} --dns-result-order=ipv4first`; diff --git a/src/setup_node_env/index.js b/src/setup_node_env/index.js index 4f7babd66ac58..176785e10246a 100644 --- a/src/setup_node_env/index.js +++ b/src/setup_node_env/index.js @@ -10,7 +10,7 @@ require('./setup_env'); // restore < Node 16 default DNS lookup behavior -require('dns').setDefaultResultOrder('ipv4first'); +require('./dns_ipv4_first'); require('@kbn/babel-register').install(); require('./polyfill'); diff --git a/test/functional/apps/dashboard/group5/embed_mode.ts b/test/functional/apps/dashboard/group5/embed_mode.ts index 2599326daba43..16934fc9101a8 100644 --- a/test/functional/apps/dashboard/group5/embed_mode.ts +++ b/test/functional/apps/dashboard/group5/embed_mode.ts @@ -24,8 +24,7 @@ export default function ({ const screenshot = getService('screenshots'); const log = getService('log'); - // Failing: See https://github.com/elastic/kibana/issues/163207 - describe.skip('embed mode', () => { + describe('embed mode', () => { /* * Note: The baseline images used in all of the screenshot tests in this test suite were taken directly from the CI environment * in order to overcome a known issue with the pixel density of fonts being significantly different when running locally versus diff --git a/test/functional/apps/visualize/group5/_tsvb_time_series.ts b/test/functional/apps/visualize/group5/_tsvb_time_series.ts index 28aa95ad24263..eec30c52018a7 100644 --- a/test/functional/apps/visualize/group5/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/group5/_tsvb_time_series.ts @@ -23,8 +23,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const browser = getService('browser'); const kibanaServer = getService('kibanaServer'); - // Failing: See https://github.com/elastic/kibana/issues/162995 - describe.skip('visual builder', function describeIndexTests() { + describe('visual builder', function describeIndexTests() { before(async () => { await security.testUser.setRoles([ 'kibana_admin', @@ -167,7 +166,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('Clicking on the chart', () => { + describe('Clicking on the chart', function () { + this.tags('skipFirefox'); const act = async (visName: string, clickCoordinates: { x: number; y: number }) => { await testSubjects.click('visualizeSaveButton'); diff --git a/test/functional/apps/visualize/group6/_tsvb_tsdb_basic.ts b/test/functional/apps/visualize/group6/_tsvb_tsdb_basic.ts index 35769d524012d..44953ae2e6619 100644 --- a/test/functional/apps/visualize/group6/_tsvb_tsdb_basic.ts +++ b/test/functional/apps/visualize/group6/_tsvb_tsdb_basic.ts @@ -17,8 +17,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/163454 - describe.skip('visual builder tsdb check', function describeIndexTests() { + describe('visual builder tsdb check', function describeIndexTests() { before(async () => { log.info(`loading sample TSDB index...`); await esArchiver.load('test/functional/fixtures/es_archiver/kibana_sample_data_logs_tsdb'); @@ -62,15 +61,5 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(isFieldForAggregationValid).to.be(true); expect(await testSubjects.exists('visualization-error-text')).to.be(false); }); - - it('should show an error when using an unsupported tsdb field type', async () => { - await visualBuilder.selectAggType('Average'); - await visualBuilder.setFieldForAggregation('bytes_counter'); - // this is still returning true - const isFieldForAggregationValid = await visualBuilder.checkFieldForAggregationValidity(); - expect(isFieldForAggregationValid).to.be(true); - // but an error should appear in visualization - expect(await testSubjects.exists('visualization-error-text')).to.be(true); - }); }); } diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 67a8dd42b9ec1..4c5939f728f01 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -7,7 +7,6 @@ */ import type { DebugState } from '@elastic/charts'; -import expect from '@kbn/expect'; import { FtrService } from '../ftr_provider_context'; import { WebElementWrapper } from '../services/lib/web_element_wrapper'; @@ -845,10 +844,14 @@ export class VisualBuilderPageObject extends FtrService { ) { await this.setMetricsGroupBy('terms'); await this.common.sleep(1000); - const byField = await this.testSubjects.find('groupByField'); - await this.comboBox.setElement(byField, field); - const isSelected = await this.comboBox.isOptionSelected(byField, field); - expect(isSelected).to.be(true); + await this.retry.try(async () => { + const byField = await this.testSubjects.find('groupByField'); + await this.comboBox.setElement(byField, field); + const isSelected = await this.comboBox.isOptionSelected(byField, field); + if (!isSelected) { + throw new Error(`setMetricsGroupByTerms: failed to set '${field}' field`); + } + }); await this.setMetricsGroupByFiltering(filtering.include, filtering.exclude); } @@ -860,13 +863,17 @@ export class VisualBuilderPageObject extends FtrService { // In case of StaleElementReferenceError 'browser' service will try to find element again await fieldSelectAddButtonLast.click(); await this.common.sleep(2000); - const selectedByField = await this.find.byXPath( - `(//*[@data-test-subj='fieldSelectItem'])[last()]` - ); - await this.comboBox.setElement(selectedByField, field); - const isSelected = await this.comboBox.isOptionSelected(selectedByField, field); - expect(isSelected).to.be(true); + await this.retry.try(async () => { + const selectedByField = await this.find.byXPath( + `(//*[@data-test-subj='fieldSelectItem'])[last()]` + ); + await this.comboBox.setElement(selectedByField, field); + const isSelected = await this.comboBox.isOptionSelected(selectedByField, field); + if (!isSelected) { + throw new Error(`setAnotherGroupByTermsField: failed to set '${field}' field`); + } + }); } public async setMetricsGroupByFiltering(include?: string, exclude?: string) { diff --git a/test/functional/screenshots/baseline/dashboard_embed_mode.png b/test/functional/screenshots/baseline/dashboard_embed_mode.png index 0c6b375233679..592cd6c4ff403 100644 Binary files a/test/functional/screenshots/baseline/dashboard_embed_mode.png and b/test/functional/screenshots/baseline/dashboard_embed_mode.png differ diff --git a/test/functional/screenshots/baseline/dashboard_embed_mode_scrolling.png b/test/functional/screenshots/baseline/dashboard_embed_mode_scrolling.png index 822bcb90ae935..eed23bc7ed78f 100644 Binary files a/test/functional/screenshots/baseline/dashboard_embed_mode_scrolling.png and b/test/functional/screenshots/baseline/dashboard_embed_mode_scrolling.png differ diff --git a/test/functional/screenshots/baseline/dashboard_embed_mode_with_url_params.png b/test/functional/screenshots/baseline/dashboard_embed_mode_with_url_params.png index cbf96cdbf278b..26a18ede2d686 100644 Binary files a/test/functional/screenshots/baseline/dashboard_embed_mode_with_url_params.png and b/test/functional/screenshots/baseline/dashboard_embed_mode_with_url_params.png differ diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 673c092a546a9..66a2e385d3e6c 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -197,6 +197,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.cases.files.allowedMimeTypes (array)', 'xpack.cases.files.maxSize (number)', 'xpack.cases.markdownPlugins.lens (boolean)', + 'xpack.cases.stack.enabled (boolean)', 'xpack.ccr.ui.enabled (boolean)', 'xpack.cloud.base_url (string)', 'xpack.cloud.cname (string)', @@ -238,7 +239,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.graph.savePolicy (alternatives)', 'xpack.ilm.ui.enabled (boolean)', 'xpack.index_management.ui.enabled (boolean)', - 'xpack.index_management.enableIndexActions (boolean)', + 'xpack.index_management.enableIndexActions (any)', 'xpack.infra.sources.default.fields.message (array)', /** * xpack.infra.logs is conditional and will resolve to an object of properties diff --git a/test/scripts/jenkins_security_solution_cypress_firefox.sh b/test/scripts/jenkins_security_solution_cypress_firefox.sh deleted file mode 100755 index 79623d5a2a23b..0000000000000 --- a/test/scripts/jenkins_security_solution_cypress_firefox.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -source test/scripts/jenkins_test_setup_xpack.sh - -echo " -> Running security solution cypress tests" -cd "$XPACK_DIR" - -node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$KIBANA_INSTALL_DIR" \ - --config test/security_solution_cypress/config.firefox.ts - -echo "" -echo "" diff --git a/tsconfig.base.json b/tsconfig.base.json index 12504320663e3..8efe5c62421de 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1172,6 +1172,8 @@ "@kbn/screenshotting-plugin/*": ["x-pack/plugins/screenshotting/*"], "@kbn/search-examples-plugin": ["examples/search_examples"], "@kbn/search-examples-plugin/*": ["examples/search_examples/*"], + "@kbn/search-response-warnings": ["packages/kbn-search-response-warnings"], + "@kbn/search-response-warnings/*": ["packages/kbn-search-response-warnings/*"], "@kbn/searchprofiler-plugin": ["x-pack/plugins/searchprofiler"], "@kbn/searchprofiler-plugin/*": ["x-pack/plugins/searchprofiler/*"], "@kbn/security-api-integration-helpers": ["x-pack/test/security_api_integration/packages/helpers"], diff --git a/typings/index.d.ts b/typings/index.d.ts index d561c1444a77a..0134f5be84018 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -20,3 +20,5 @@ declare module 'react-syntax-highlighter/dist/cjs/prism-light'; declare module 'monaco-editor/esm/vs/basic-languages/markdown/markdown'; declare module 'monaco-editor/esm/vs/basic-languages/css/css'; declare module 'monaco-editor/esm/vs/basic-languages/yaml/yaml'; + +declare module 'find-cypress-specs'; diff --git a/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts b/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts index e3942b26ee6fa..5c4acce28b108 100644 --- a/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts @@ -114,6 +114,7 @@ const getIndexTemplatePutBody = (opts?: GetIndexTemplatePutBodyOpts) => { name: '.alerts-ilm-policy', rollover_alias: `.alerts-${context ? context : 'test'}.alerts-${namespace}`, }, + 'index.mapping.ignore_malformed': true, 'index.mapping.total_fields.limit': 2500, }, mappings: { @@ -640,6 +641,7 @@ describe('Alerts Service', () => { name: '.alerts-ilm-policy', rollover_alias: `.alerts-empty.alerts-default`, }, + 'index.mapping.ignore_malformed': true, 'index.mapping.total_fields.limit': 2500, }, mappings: { diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts index d4ce203a0d0e3..38c2207e5f410 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts @@ -42,6 +42,7 @@ const IndexTemplate = (namespace: string = 'default') => ({ name: 'test-ilm-policy', rollover_alias: `.alerts-test.alerts-${namespace}`, }, + 'index.mapping.ignore_malformed': true, 'index.mapping.total_fields.limit': 2500, }, }, diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts index a17fad2d875ed..388fe6344a51f 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts @@ -54,6 +54,7 @@ export const getIndexTemplate = ({ rollover_alias: indexPatterns.alias, }, 'index.mapping.total_fields.limit': totalFieldsLimit, + 'index.mapping.ignore_malformed': true, }, mappings: { dynamic: false, diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/transaction_details.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/transaction_details.cy.ts index dd73051be9f2e..6886fc582f631 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/transaction_details.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/transaction_details.cy.ts @@ -151,4 +151,30 @@ describe('Transaction details', () => { }); }); }); + + describe('when changing filters which results in no trace samples', () => { + it('trace waterfall must reset to empty state', () => { + cy.visitKibana( + `/app/apm/services/opbeans-java/transactions/view?${new URLSearchParams( + { + ...timeRange, + transactionName: 'GET /api/product', + } + )}` + ); + + cy.getByTestSubj('apmWaterfallButton').should('exist'); + + cy.getByTestSubj('apmUnifiedSearchBar') + .type(`_id: "123"`) + .type('{enter}'); + + cy.getByTestSubj('apmWaterfallButton').should('not.exist'); + cy.getByTestSubj('apmNoTraceFound').should('exist'); + + cy.reload(); + + cy.getByTestSubj('apmNoTraceFound').should('exist'); + }); + }); }); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx index 0c05b945c1b70..47642ebbdc6dc 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -20,7 +20,7 @@ import { useApmServiceContext } from '../../../../context/apm_service/use_apm_se import { useAnyOfApmParams } from '../../../../hooks/use_apm_params'; import { useTimeRange } from '../../../../hooks/use_time_range'; import { DurationDistributionChartWithScrubber } from '../../../shared/charts/duration_distribution_chart_with_scrubber'; -import { HeightRetainer } from '../../../shared/height_retainer'; +import { ResettingHeightRetainer } from '../../../shared/height_retainer/resetting_height_container'; import { fromQuery, push, toQuery } from '../../../shared/links/url_helpers'; import { TransactionTab } from '../waterfall_with_summary/transaction_tabs'; import { useTransactionDistributionChartData } from './use_transaction_distribution_chart_data'; @@ -99,7 +99,7 @@ export function TransactionDistribution({ ); return ( - +
-
+ ); } diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts b/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts index 81e0d736aee19..a01bb00ea0e44 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts @@ -56,7 +56,10 @@ export function useWaterfallFetcher({ [traceId, start, end, transactionId] ); - const waterfall = useMemo(() => getWaterfall(data), [data]); + const waterfall = useMemo( + () => getWaterfall(traceId ? data : INITIAL_DATA), + [data, traceId] + ); return { waterfall, status, error }; } diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx index e19400490133b..a14572f089137 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx @@ -60,8 +60,10 @@ export function WaterfallWithSummary({ const isLoading = waterfallFetchResult.status === FETCH_STATUS.LOADING || traceSamplesFetchStatus === FETCH_STATUS.LOADING; + // When traceId is not present, call to waterfallFetchResult will not be initiated const isSucceded = - waterfallFetchResult.status === FETCH_STATUS.SUCCESS && + (waterfallFetchResult.status === FETCH_STATUS.SUCCESS || + waterfallFetchResult.status === FETCH_STATUS.NOT_INITIATED) && traceSamplesFetchStatus === FETCH_STATUS.SUCCESS; useEffect(() => { @@ -96,6 +98,7 @@ export function WaterfallWithSummary({ })} } + data-test-subj="apmNoTraceFound" titleSize="s" /> ); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index c99bfd21c9ad1..51c6cdc54131d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -9,7 +9,7 @@ import React, { FC } from 'react'; import useObservable from 'react-use/lib/useObservable'; import ReactDOM from 'react-dom'; import { CoreStart } from '@kbn/core/public'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { IEmbeddable, EmbeddableFactory, @@ -62,7 +62,7 @@ const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { style={{ width: '100%', height: '100%', cursor: 'auto' }} > - + diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx index 12e1948260a96..2a875ff88f413 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { StartInitializer } from '../../../plugin'; import { RendererFactory } from '../../../../types'; import { AdvancedFilter } from './component'; @@ -24,7 +24,7 @@ export const advancedFilterFactory: StartInitializer> = height: 50, render(domNode, _, handlers) { ReactDOM.render( - + handlers.event({ name: 'applyFilterAction', data: filter })} value={handlers.getFilter()} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx index 76323793ab698..50f534a658359 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx @@ -9,7 +9,7 @@ import { fromExpression, toExpression, Ast } from '@kbn/interpreter'; import { get } from 'lodash'; import React from 'react'; import ReactDOM from 'react-dom'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { syncFilterExpression } from '../../../../public/lib/sync_filter_expression'; import { RendererFactory } from '../../../../types'; import { StartInitializer } from '../../../plugin'; @@ -97,7 +97,7 @@ export const dropdownFilterFactory: StartInitializer> = ); ReactDOM.render( - {filter}, + {filter}, domNode, () => handlers.done() ); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx index 9ae72a82c8870..52ae1e28f7904 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx @@ -9,7 +9,7 @@ import ReactDOM from 'react-dom'; import React from 'react'; import { toExpression } from '@kbn/interpreter'; import { UI_SETTINGS } from '@kbn/data-plugin/public'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { syncFilterExpression } from '../../../../public/lib/sync_filter_expression'; import { RendererStrings } from '../../../../i18n'; import { TimeFilter } from './components'; @@ -60,7 +60,7 @@ export const timeFilterFactory: StartInitializer> = ( } ReactDOM.render( - + handlers.event({ name: 'applyFilterAction', data: filter })} filter={filterExpression} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/index.tsx index 7566db85427ac..fd1e2415d0589 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/index.tsx @@ -9,7 +9,7 @@ import React, { CSSProperties } from 'react'; import ReactDOM from 'react-dom'; import { CoreTheme } from '@kbn/core/public'; import { Observable } from 'rxjs'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { defaultTheme$ } from '@kbn/presentation-util-plugin/common'; import { Markdown } from '@kbn/kibana-react-plugin/public'; import { StartInitializer } from '../../plugin'; @@ -30,7 +30,7 @@ export const getMarkdownRenderer = const fontStyle = config.font ? config.font.spec : {}; ReactDOM.render( - + +
+
{textString}
, domNode, diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index cc365451b46f0..fa544095a8598 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -17,7 +17,8 @@ import { includes, remove } from 'lodash'; import { AppMountParameters, CoreStart, CoreSetup, AppUpdater } from '@kbn/core/public'; -import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { PluginServices } from '@kbn/presentation-util-plugin/public'; import { CanvasStartDeps, CanvasSetupDeps } from './plugin'; @@ -75,7 +76,7 @@ export const renderApp = ({ - + @@ -151,7 +152,7 @@ export const initializeCanvas = async ( ], content: (domNode, { hideHelpMenu }) => { ReactDOM.render( - + diff --git a/x-pack/plugins/canvas/public/components/home/home.component.tsx b/x-pack/plugins/canvas/public/components/home/home.component.tsx index dbfbd8a920c7e..c29713da70d11 100644 --- a/x-pack/plugins/canvas/public/components/home/home.component.tsx +++ b/x-pack/plugins/canvas/public/components/home/home.component.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { KibanaPageTemplate } from '@kbn/kibana-react-plugin/public'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { withSuspense } from '@kbn/presentation-util-plugin/public'; import { WorkpadCreate } from './workpad_create'; @@ -48,7 +48,9 @@ export const Home = ({ activeTab = 'workpads' }: Props) => { ], }} > - {tab === 'workpads' ? : } + + {tab === 'workpads' ? : } + ); }; diff --git a/x-pack/plugins/canvas/tsconfig.json b/x-pack/plugins/canvas/tsconfig.json index 04d1e78fc9555..45b22e422dc6e 100644 --- a/x-pack/plugins/canvas/tsconfig.json +++ b/x-pack/plugins/canvas/tsconfig.json @@ -82,6 +82,8 @@ "@kbn/core-saved-objects-server", "@kbn/discover-utils", "@kbn/content-management-plugin", + "@kbn/react-kibana-context-theme", + "@kbn/shared-ux-page-kibana-template", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 86f32c87ebeeb..e2e453aaaf278 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -59,6 +59,9 @@ export interface CasesUiConfigType { maxSize?: number; allowedMimeTypes: string[]; }; + stack: { + enabled: boolean; + }; } export const StatusAll = 'all' as const; diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx index 77d38e87a2c41..bab441edbe80c 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx @@ -587,28 +587,60 @@ describe('createCommentUserActionBuilder', () => { }); }); - it('renders correctly an action', async () => { - const userAction = getHostIsolationUserAction(); - - const builder = createCommentUserActionBuilder({ - ...builderArgs, - caseData: { - ...builderArgs.caseData, - comments: [hostIsolationComment()], - }, - userAction, + describe('Host isolation action', () => { + it('renders correctly an action', async () => { + const userAction = getHostIsolationUserAction(); + + const builder = createCommentUserActionBuilder({ + ...builderArgs, + caseData: { + ...builderArgs.caseData, + comments: [hostIsolationComment()], + }, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByTestId('endpoint-action')).toBeInTheDocument(); + expect(screen.getByText('submitted isolate request on host')).toBeInTheDocument(); + expect(screen.getByText('host1')).toBeInTheDocument(); + expect(screen.getByText('I just isolated the host!')).toBeInTheDocument(); }); - const createdUserAction = builder.build(); - render( - - - - ); + it('shows the correct username', async () => { + const createdBy = { profileUid: userProfiles[0].uid }; + const userAction = getHostIsolationUserAction({ + createdBy, + }); - expect(screen.getByText('submitted isolate request on host')).toBeInTheDocument(); - expect(screen.getByText('host1')).toBeInTheDocument(); - expect(screen.getByText('I just isolated the host!')).toBeInTheDocument(); + const builder = createCommentUserActionBuilder({ + ...builderArgs, + caseData: { + ...builderArgs.caseData, + comments: [hostIsolationComment({ createdBy })], + }, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect( + screen.getAllByTestId('case-user-profile-avatar-damaged_raccoon')[0] + ).toBeInTheDocument(); + expect(screen.getAllByText('DR')[0]).toBeInTheDocument(); + expect(screen.getAllByText('Damaged Raccoon')[0]).toBeInTheDocument(); + }); }); describe('Attachment framework', () => { diff --git a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx index 1d3ba134f7e6d..4530e115a2f04 100644 --- a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx @@ -13,18 +13,11 @@ import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; import routeData from 'react-router'; import { useUpdateComment } from '../../containers/use_update_comment'; -import { - basicCase, - caseUserActions, - getHostIsolationUserAction, - getUserAction, - hostIsolationComment, -} from '../../containers/mock'; +import { basicCase, caseUserActions, getUserAction } from '../../containers/mock'; import { UserActions } from '.'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; import { UserActionActions } from '../../../common/types/domain'; -import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock'; import { getCaseConnectorsMockResponse } from '../../common/mock/connectors'; import type { UserActivityParams } from '../user_actions_activity_bar/types'; import { useFindCaseUserActions } from '../../containers/use_find_case_user_actions'; @@ -325,277 +318,4 @@ describe.skip(`UserActions`, () => { expect(screen.getAllByTestId('add-comment')[1].textContent).toContain(newComment); }); }); - - // FLAKY: https://github.com/elastic/kibana/issues/156742 - describe.skip('Host isolation action', () => { - it('renders in the cases details view', async () => { - const isolateAction = [getHostIsolationUserAction()]; - const props = { - ...defaultProps, - data: { ...defaultProps.data, comments: [...basicCase.comments, hostIsolationComment()] }, - }; - - useFindCaseUserActionsMock.mockReturnValue({ - ...defaultUseFindCaseUserActions, - data: { userActions: isolateAction }, - }); - - appMockRender.render(); - await waitFor(() => { - expect(screen.getByTestId('endpoint-action')).toBeInTheDocument(); - }); - }); - - it('shows the correct username', async () => { - const isolateAction = [ - getHostIsolationUserAction({ createdBy: { profileUid: userProfiles[0].uid } }), - ]; - const props = { - ...defaultProps, - userProfiles: userProfilesMap, - data: { - ...defaultProps.data, - comments: [hostIsolationComment({ createdBy: { profileUid: userProfiles[0].uid } })], - }, - }; - - useFindCaseUserActionsMock.mockReturnValue({ - ...defaultUseFindCaseUserActions, - data: { userActions: isolateAction }, - }); - - appMockRender.render(); - - expect( - screen.getAllByTestId('case-user-profile-avatar-damaged_raccoon')[0] - ).toBeInTheDocument(); - expect(screen.getAllByText('DR')[0]).toBeInTheDocument(); - expect(screen.getAllByText('Damaged Raccoon')[0]).toBeInTheDocument(); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/156750 - // FLAKY: https://github.com/elastic/kibana/issues/156749 - // FLAKY: https://github.com/elastic/kibana/issues/156748 - // FLAKY: https://github.com/elastic/kibana/issues/156747 - // FLAKY: https://github.com/elastic/kibana/issues/156746 - // FLAKY: https://github.com/elastic/kibana/issues/156745 - // FLAKY: https://github.com/elastic/kibana/issues/156744 - // FLAKY: https://github.com/elastic/kibana/issues/156743 - describe.skip('pagination', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('Loading spinner when user actions loading', () => { - useFindCaseUserActionsMock.mockReturnValue({ isLoading: true }); - useInfiniteFindCaseUserActionsMock.mockReturnValue({ isLoading: true }); - appMockRender.render( - - ); - - expect(screen.getByTestId('user-actions-loading')).toBeInTheDocument(); - }); - - it('renders two user actions list when user actions are more than 10', () => { - appMockRender.render(); - - expect(screen.getAllByTestId('user-actions-list')).toHaveLength(2); - }); - - it('renders only one user actions list when last page is 0', async () => { - useFindCaseUserActionsMock.mockReturnValue({ - ...defaultUseFindCaseUserActions, - data: { userActions: [] }, - }); - const props = { - ...defaultProps, - userActionsStats: { - total: 0, - totalComments: 0, - totalOtherActions: 0, - }, - }; - - appMockRender.render(); - - await waitForComponentToUpdate(); - - expect(screen.getAllByTestId('user-actions-list')).toHaveLength(1); - }); - - it('renders only one user actions list when last page is 1', async () => { - useFindCaseUserActionsMock.mockReturnValue({ - ...defaultUseFindCaseUserActions, - data: { userActions: [] }, - }); - const props = { - ...defaultProps, - userActionsStats: { - total: 1, - totalComments: 0, - totalOtherActions: 1, - }, - }; - - appMockRender.render(); - - await waitForComponentToUpdate(); - - expect(screen.getAllByTestId('user-actions-list')).toHaveLength(1); - }); - - it('renders only one action list when user actions are less than or equal to 10', async () => { - useFindCaseUserActionsMock.mockReturnValue({ - ...defaultUseFindCaseUserActions, - data: { userActions: [] }, - }); - const props = { - ...defaultProps, - userActionsStats: { - total: 10, - totalComments: 6, - totalOtherActions: 4, - }, - }; - - appMockRender.render(); - - await waitForComponentToUpdate(); - - expect(screen.getAllByTestId('user-actions-list')).toHaveLength(1); - }); - - it('call fetchNextPage on showMore button click', async () => { - useInfiniteFindCaseUserActionsMock.mockReturnValue({ - ...defaultInfiniteUseFindCaseUserActions, - hasNextPage: true, - }); - const props = { - ...defaultProps, - userActionsStats: { - total: 25, - totalComments: 10, - totalOtherActions: 15, - }, - }; - - appMockRender.render(); - - await waitForComponentToUpdate(); - - expect(screen.getAllByTestId('user-actions-list')).toHaveLength(2); - - const showMore = screen.getByTestId('cases-show-more-user-actions'); - - expect(showMore).toBeInTheDocument(); - - userEvent.click(showMore); - - await waitFor(() => { - expect(defaultInfiniteUseFindCaseUserActions.fetchNextPage).toHaveBeenCalled(); - }); - }); - - it('shows more button visible 21st user action added', async () => { - const mockUserActions = [ - ...caseUserActions, - getUserAction('comment', UserActionActions.create), - getUserAction('comment', UserActionActions.update), - getUserAction('comment', UserActionActions.create), - getUserAction('comment', UserActionActions.update), - getUserAction('comment', UserActionActions.create), - getUserAction('comment', UserActionActions.update), - getUserAction('comment', UserActionActions.create), - ]; - useInfiniteFindCaseUserActionsMock.mockReturnValue({ - ...defaultInfiniteUseFindCaseUserActions, - data: { - pages: [ - { - total: 20, - page: 1, - perPage: 10, - userActions: mockUserActions, - }, - ], - }, - }); - useFindCaseUserActionsMock.mockReturnValue({ - ...defaultUseFindCaseUserActions, - data: { - total: 20, - page: 2, - perPage: 10, - userActions: mockUserActions, - }, - }); - const props = { - ...defaultProps, - userActionsStats: { - total: 20, - totalComments: 10, - totalOtherActions: 10, - }, - }; - - const { rerender } = appMockRender.render(); - - await waitForComponentToUpdate(); - - expect(screen.getAllByTestId('user-actions-list')).toHaveLength(2); - expect(screen.queryByTestId('cases-show-more-user-actions')).not.toBeInTheDocument(); - - useInfiniteFindCaseUserActionsMock.mockReturnValue({ - ...defaultInfiniteUseFindCaseUserActions, - data: { - pages: [ - { - total: 21, - page: 1, - perPage: 10, - userActions: mockUserActions, - }, - { - total: 21, - page: 2, - perPage: 10, - userActions: [getUserAction('comment', UserActionActions.create)], - }, - ], - }, - hasNextPage: true, - }); - useFindCaseUserActionsMock.mockReturnValue({ - ...defaultUseFindCaseUserActions, - data: { - total: 21, - page: 2, - perPage: 10, - userActions: mockUserActions, - }, - }); - - const newProps = { - ...props, - userActionsStats: { - total: 21, - totalComments: 11, - totalOtherActions: 10, - }, - }; - - rerender(); - - await waitForComponentToUpdate(); - - expect(screen.getAllByTestId('user-actions-list')).toHaveLength(2); - - const firstUserActionsList = screen.getAllByTestId('user-actions-list')[0]; - - expect(firstUserActionsList.getElementsByTagName('li')).toHaveLength(11); - - expect(screen.getByTestId('cases-show-more-user-actions')).toBeInTheDocument(); - }); - }); }); diff --git a/x-pack/plugins/cases/public/plugin.test.ts b/x-pack/plugins/cases/public/plugin.test.ts new file mode 100644 index 0000000000000..bb81b7d501c5d --- /dev/null +++ b/x-pack/plugins/cases/public/plugin.test.ts @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PluginInitializerContext } from '@kbn/core/public'; +import { coreMock } from '@kbn/core/public/mocks'; +import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; +import { featuresPluginMock } from '@kbn/features-plugin/public/mocks'; +import { securityMock } from '@kbn/security-plugin/public/mocks'; +import { managementPluginMock } from '@kbn/management-plugin/public/mocks'; +import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; +import { lensPluginMock } from '@kbn/lens-plugin/public/mocks'; +import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks'; +import { mockStorage } from '@kbn/kibana-utils-plugin/public/storage/hashed_item_store/mock'; +import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks'; +import type { CasesPluginSetup, CasesPluginStart } from './types'; +import { CasesUiPlugin } from './plugin'; +import { ALLOWED_MIME_TYPES } from '../common/constants/mime_types'; + +function getConfig(overrides = {}) { + return { + markdownPlugins: { lens: true }, + files: { maxSize: 1, allowedMimeTypes: ALLOWED_MIME_TYPES }, + stack: { enabled: true }, + ...overrides, + }; +} + +describe('Cases Ui Plugin', () => { + let context: PluginInitializerContext; + let plugin: CasesUiPlugin; + let coreSetup: ReturnType; + let coreStart: ReturnType; + let pluginsSetup: jest.Mocked; + let pluginsStart: jest.Mocked; + + beforeEach(() => { + context = coreMock.createPluginInitializerContext(getConfig()); + plugin = new CasesUiPlugin(context); + coreSetup = coreMock.createSetup(); + coreStart = coreMock.createStart(); + + pluginsSetup = { + files: { + filesClientFactory: { asScoped: jest.fn(), asUnscoped: jest.fn() }, + registerFileKind: jest.fn(), + }, + security: securityMock.createSetup(), + management: managementPluginMock.createSetupContract(), + }; + + pluginsStart = { + licensing: licensingMock.createStart(), + uiActions: uiActionsPluginMock.createStartContract(), + files: { + filesClientFactory: { asScoped: jest.fn(), asUnscoped: jest.fn() }, + getAllFindKindDefinitions: jest.fn(), + getFileKindDefinition: jest.fn(), + }, + features: featuresPluginMock.createStart(), + security: securityMock.createStart(), + data: dataPluginMock.createStartContract(), + embeddable: embeddablePluginMock.createStartContract(), + lens: lensPluginMock.createStartContract(), + contentManagement: contentManagementMock.createStartContract(), + storage: { + store: { + getItem: mockStorage.getItem, + setItem: mockStorage.setItem, + removeItem: mockStorage.removeItem, + clear: mockStorage.clear, + }, + get: jest.fn(), + set: jest.fn(), + clear: jest.fn(), + remove: jest.fn(), + }, + triggersActionsUi: triggersActionsUiMock.createStart(), + }; + }); + + describe('setup()', () => { + it('should start setup cases plugin correctly', async () => { + const setup = plugin.setup(coreSetup, pluginsSetup); + + expect(setup).toMatchInlineSnapshot(` + Object { + "attachmentFramework": Object { + "registerExternalReference": [Function], + "registerPersistableState": [Function], + }, + } + `); + }); + + it('should register kibana feature when stack is enabled', async () => { + plugin.setup(coreSetup, pluginsSetup); + + expect( + pluginsSetup.management.sections.section.insightsAndAlerting.registerApp + ).toHaveBeenCalled(); + }); + + it('should not register kibana feature when stack is disabled', async () => { + context = coreMock.createPluginInitializerContext(getConfig({ stack: { enabled: false } })); + const pluginWithStackDisabled = new CasesUiPlugin(context); + + pluginWithStackDisabled.setup(coreSetup, pluginsSetup); + + expect( + pluginsSetup.management.sections.section.insightsAndAlerting.registerApp + ).not.toHaveBeenCalled(); + }); + }); + + describe('start', () => { + it('should start cases plugin correctly', async () => { + const pluginStart = plugin.start(coreStart, pluginsStart); + + expect(pluginStart).toStrictEqual({ + api: { + cases: { + bulkGet: expect.any(Function), + find: expect.any(Function), + getCasesMetrics: expect.any(Function), + getCasesStatus: expect.any(Function), + }, + getRelatedCases: expect.any(Function), + }, + helpers: { + canUseCases: expect.any(Function), + getRuleIdFromEvent: expect.any(Function), + getUICapabilities: expect.any(Function), + groupAlertsByRule: expect.any(Function), + }, + hooks: { + useCasesAddToExistingCaseModal: expect.any(Function), + useCasesAddToNewCaseFlyout: expect.any(Function), + }, + ui: { + getAllCasesSelectorModal: expect.any(Function), + getCases: expect.any(Function), + getCasesContext: expect.any(Function), + getRecentCases: expect.any(Function), + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/plugin.ts b/x-pack/plugins/cases/public/plugin.ts index 82d62f2c07da7..f4909f344e7fc 100644 --- a/x-pack/plugins/cases/public/plugin.ts +++ b/x-pack/plugins/cases/public/plugin.ts @@ -75,30 +75,32 @@ export class CasesUiPlugin }); } - plugins.management.sections.section.insightsAndAlerting.registerApp({ - id: APP_ID, - title: APP_TITLE, - order: 1, - async mount(params: ManagementAppMountParams) { - const [coreStart, pluginsStart] = (await core.getStartServices()) as [ - CoreStart, - CasesPluginStart, - unknown - ]; - - const { renderApp } = await import('./application'); - - return renderApp({ - mountParams: params, - coreStart, - pluginsStart, - storage, - kibanaVersion, - externalReferenceAttachmentTypeRegistry, - persistableStateAttachmentTypeRegistry, - }); - }, - }); + if (config.stack.enabled) { + plugins.management.sections.section.insightsAndAlerting.registerApp({ + id: APP_ID, + title: APP_TITLE, + order: 1, + async mount(params: ManagementAppMountParams) { + const [coreStart, pluginsStart] = (await core.getStartServices()) as [ + CoreStart, + CasesPluginStart, + unknown + ]; + + const { renderApp } = await import('./application'); + + return renderApp({ + mountParams: params, + coreStart, + pluginsStart, + storage, + kibanaVersion, + externalReferenceAttachmentTypeRegistry, + persistableStateAttachmentTypeRegistry, + }); + }, + }); + } return { attachmentFramework: { diff --git a/x-pack/plugins/cases/server/config.test.ts b/x-pack/plugins/cases/server/config.test.ts index 54fc42f694bc2..352faac983f29 100644 --- a/x-pack/plugins/cases/server/config.test.ts +++ b/x-pack/plugins/cases/server/config.test.ts @@ -106,6 +106,9 @@ describe('config validation', () => { "markdownPlugins": Object { "lens": true, }, + "stack": Object { + "enabled": true, + }, } `); }); diff --git a/x-pack/plugins/cases/server/config.ts b/x-pack/plugins/cases/server/config.ts index c2daeb73b03de..7e30671ee4734 100644 --- a/x-pack/plugins/cases/server/config.ts +++ b/x-pack/plugins/cases/server/config.ts @@ -20,6 +20,9 @@ export const ConfigSchema = schema.object({ // intentionally not setting a default here so that we can determine if the user set it maxSize: schema.maybe(schema.number({ min: 0 })), }), + stack: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), }); export type ConfigType = TypeOf; diff --git a/x-pack/plugins/cases/server/index.ts b/x-pack/plugins/cases/server/index.ts index 62748905f6b82..b1cadb43f147c 100644 --- a/x-pack/plugins/cases/server/index.ts +++ b/x-pack/plugins/cases/server/index.ts @@ -16,6 +16,7 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { markdownPlugins: true, files: { maxSize: true, allowedMimeTypes: true }, + stack: { enabled: true }, }, deprecations: ({ renameFromRoot }) => [ renameFromRoot('xpack.case.enabled', 'xpack.cases.enabled', { level: 'critical' }), diff --git a/x-pack/plugins/cases/server/plugin.test.ts b/x-pack/plugins/cases/server/plugin.test.ts new file mode 100644 index 0000000000000..255e6ce42af68 --- /dev/null +++ b/x-pack/plugins/cases/server/plugin.test.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PluginInitializerContext } from '@kbn/core/server'; +import {} from '@kbn/core/server'; +import { coreMock } from '@kbn/core/server/mocks'; +import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/server/mocks'; +import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; +import { createFilesSetupMock } from '@kbn/files-plugin/server/mocks'; +import { securityMock } from '@kbn/security-plugin/server/mocks'; +import { makeLensEmbeddableFactory } from '@kbn/lens-plugin/server/embeddable/make_lens_embeddable_factory'; +import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; +import { actionsMock } from '@kbn/actions-plugin/server/mocks'; +import { notificationsMock } from '@kbn/notifications-plugin/server/mocks'; +import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import type { PluginsSetup, PluginsStart } from './plugin'; +import { CasePlugin } from './plugin'; +import type { ConfigType } from './config'; +import { ALLOWED_MIME_TYPES } from '../common/constants/mime_types'; + +function getConfig(overrides = {}) { + return { + markdownPlugins: { lens: true }, + files: { maxSize: 1, allowedMimeTypes: ALLOWED_MIME_TYPES }, + stack: { enabled: true }, + ...overrides, + }; +} + +describe('Cases Plugin', () => { + let context: PluginInitializerContext; + let plugin: CasePlugin; + let coreSetup: ReturnType; + let coreStart: ReturnType; + let pluginsSetup: jest.Mocked; + let pluginsStart: jest.Mocked; + + beforeEach(() => { + context = coreMock.createPluginInitializerContext(getConfig()); + + plugin = new CasePlugin(context); + coreSetup = coreMock.createSetup(); + coreStart = coreMock.createStart(); + + pluginsSetup = { + taskManager: taskManagerMock.createSetup(), + actions: actionsMock.createSetup(), + files: createFilesSetupMock(), + lens: { + lensEmbeddableFactory: makeLensEmbeddableFactory( + () => ({}), + () => ({}), + {} + ), + registerVisualizationMigration: jest.fn(), + }, + security: securityMock.createSetup(), + licensing: licensingMock.createSetup(), + usageCollection: usageCollectionPluginMock.createSetupContract(), + features: featuresPluginMock.createSetup(), + }; + + pluginsStart = { + licensing: licensingMock.createStart(), + actions: actionsMock.createStart(), + files: { fileServiceFactory: { asScoped: jest.fn(), asInternal: jest.fn() } }, + features: featuresPluginMock.createStart(), + security: securityMock.createStart(), + notifications: notificationsMock.createStart(), + ruleRegistry: { getRacClientWithRequest: jest.fn(), alerting: alertsMock.createStart() }, + }; + }); + + describe('setup()', () => { + it('should start setup cases plugin correctly', async () => { + plugin.setup(coreSetup, pluginsSetup); + + expect(context.logger.get().debug).toHaveBeenCalledWith( + `Setting up Case Workflow with core contract [${Object.keys( + coreSetup + )}] and plugins [${Object.keys(pluginsSetup)}]` + ); + }); + + it('should register kibana feature when stack is enabled', async () => { + plugin.setup(coreSetup, pluginsSetup); + + expect(pluginsSetup.features.registerKibanaFeature).toHaveBeenCalled(); + }); + + it('should not register kibana feature when stack is disabled', async () => { + context = coreMock.createPluginInitializerContext( + getConfig({ stack: { enabled: false } }) + ); + const pluginWithStackDisabled = new CasePlugin(context); + + pluginWithStackDisabled.setup(coreSetup, pluginsSetup); + + expect(pluginsSetup.features.registerKibanaFeature).not.toHaveBeenCalled(); + }); + }); + + describe('start', () => { + it('should start cases plugin correctly', async () => { + const pluginStart = plugin.start(coreStart, pluginsStart); + + expect(context.logger.get().debug).toHaveBeenCalledWith(`Starting Case Workflow`); + + expect(pluginStart).toMatchInlineSnapshot(` + Object { + "getCasesClientWithRequest": [Function], + "getExternalReferenceAttachmentTypeRegistry": [Function], + "getPersistableStateAttachmentTypeRegistry": [Function], + } + `); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 0ae005e09b188..510686f1a98bd 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -121,7 +121,9 @@ export class CasePlugin { this.securityPluginSetup = plugins.security; this.lensEmbeddableFactory = plugins.lens.lensEmbeddableFactory; - plugins.features.registerKibanaFeature(getCasesKibanaFeature()); + if (this.caseConfig.stack.enabled) { + plugins.features.registerKibanaFeature(getCasesKibanaFeature()); + } core.savedObjects.registerType( createCaseCommentSavedObjectType({ diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json index cb82b27d5334d..ccd6e228d6aff 100644 --- a/x-pack/plugins/cases/tsconfig.json +++ b/x-pack/plugins/cases/tsconfig.json @@ -68,6 +68,7 @@ "@kbn/core-theme-browser", "@kbn/serverless", "@kbn/core-http-server", + "@kbn/alerting-plugin", "@kbn/content-management-plugin", ], "exclude": [ diff --git a/x-pack/plugins/cloud_integrations/cloud_links/kibana.jsonc b/x-pack/plugins/cloud_integrations/cloud_links/kibana.jsonc index b9b9a0f629b64..4b6625f842f79 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/kibana.jsonc +++ b/x-pack/plugins/cloud_integrations/cloud_links/kibana.jsonc @@ -11,6 +11,9 @@ "cloud", "security", "guidedOnboarding" + ], + "requiredBundles": [ + "kibanaReact" ] } } diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/__snapshots__/maybe_add_cloud_links.test.ts.snap b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/__snapshots__/maybe_add_cloud_links.test.ts.snap new file mode 100644 index 0000000000000..7a957943fe2f0 --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/__snapshots__/maybe_add_cloud_links.test.ts.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`maybeAddCloudLinks when cloud enabled and it fails to fetch the user, it sets the links 2`] = ` +Array [ + Object { + "href": "profile-url", + "iconType": "user", + "label": "Profile", + "order": 100, + "setAsProfile": true, + }, + Object { + "href": "billing-url", + "iconType": "visGauge", + "label": "Billing", + "order": 200, + }, + Object { + "href": "organization-url", + "iconType": "gear", + "label": "Organization", + "order": 300, + }, + Any, +] +`; + +exports[`maybeAddCloudLinks when cloud enabled and the user is an Elastic Cloud user, it sets the links 2`] = ` +Array [ + Object { + "href": "profile-url", + "iconType": "user", + "label": "Profile", + "order": 100, + "setAsProfile": true, + }, + Object { + "href": "billing-url", + "iconType": "visGauge", + "label": "Billing", + "order": 200, + }, + Object { + "href": "organization-url", + "iconType": "gear", + "label": "Organization", + "order": 300, + }, + Any, +] +`; diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.test.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.test.ts index f2e14d1a08526..b9045fdc9a59f 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.test.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.test.ts @@ -14,12 +14,11 @@ import { maybeAddCloudLinks } from './maybe_add_cloud_links'; describe('maybeAddCloudLinks', () => { it('should skip if cloud is disabled', async () => { const security = securityMock.createStart(); + const core = coreMock.createStart(); maybeAddCloudLinks({ + core, security, - chrome: coreMock.createStart().chrome, cloud: { ...cloudMock.createStart(), isCloudEnabled: false }, - docLinks: coreMock.createStart().docLinks, - uiSettingsClient: coreMock.createStart().uiSettings, }); // Since there's a promise, let's wait for the next tick await new Promise((resolve) => process.nextTick(resolve)); @@ -31,13 +30,12 @@ describe('maybeAddCloudLinks', () => { security.authc.getCurrentUser.mockResolvedValue( securityMock.createMockAuthenticatedUser({ elastic_cloud_user: true }) ); - const { chrome, docLinks, uiSettings } = coreMock.createStart(); + const core = coreMock.createStart(); + const { chrome } = core; maybeAddCloudLinks({ security, - chrome, + core, cloud: { ...cloudMock.createStart(), isCloudEnabled: true }, - docLinks, - uiSettingsClient: uiSettings, }); // Since there's a promise, let's wait for the next tick await new Promise((resolve) => process.nextTick(resolve)); @@ -53,104 +51,28 @@ describe('maybeAddCloudLinks', () => { ] `); expect(security.navControlService.addUserMenuLinks).toHaveBeenCalledTimes(1); - expect(security.navControlService.addUserMenuLinks.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "href": "profile-url", - "iconType": "user", - "label": "Profile", - "order": 100, - "setAsProfile": true, - }, - Object { - "href": "billing-url", - "iconType": "visGauge", - "label": "Billing", - "order": 200, - }, - Object { - "href": "organization-url", - "iconType": "gear", - "label": "Organization", - "order": 300, - }, - Object { - "content": , - "href": "", - "iconType": "", - "label": "", - "order": 400, - }, - ], - ] - `); + expect(security.navControlService.addUserMenuLinks.mock.calls[0][0]).toMatchSnapshot([ + { + href: 'profile-url', + iconType: 'user', + label: 'Profile', + order: 100, + setAsProfile: true, + }, + { + href: 'billing-url', + iconType: 'visGauge', + label: 'Billing', + order: 200, + }, + { + href: 'organization-url', + iconType: 'gear', + label: 'Organization', + order: 300, + }, + expect.any(Object), + ]); expect(chrome.setHelpMenuLinks).toHaveBeenCalledTimes(1); expect(chrome.setHelpMenuLinks.mock.calls[0]).toMatchInlineSnapshot(` @@ -176,13 +98,12 @@ describe('maybeAddCloudLinks', () => { it('when cloud enabled and it fails to fetch the user, it sets the links', async () => { const security = securityMock.createStart(); security.authc.getCurrentUser.mockRejectedValue(new Error('Something went terribly wrong')); - const { chrome, docLinks, uiSettings } = coreMock.createStart(); + const core = coreMock.createStart(); + const { chrome } = core; maybeAddCloudLinks({ security, - chrome, + core, cloud: { ...cloudMock.createStart(), isCloudEnabled: true }, - docLinks, - uiSettingsClient: uiSettings, }); // Since there's a promise, let's wait for the next tick await new Promise((resolve) => process.nextTick(resolve)); @@ -198,104 +119,28 @@ describe('maybeAddCloudLinks', () => { ] `); expect(security.navControlService.addUserMenuLinks).toHaveBeenCalledTimes(1); - expect(security.navControlService.addUserMenuLinks.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "href": "profile-url", - "iconType": "user", - "label": "Profile", - "order": 100, - "setAsProfile": true, - }, - Object { - "href": "billing-url", - "iconType": "visGauge", - "label": "Billing", - "order": 200, - }, - Object { - "href": "organization-url", - "iconType": "gear", - "label": "Organization", - "order": 300, - }, - Object { - "content": , - "href": "", - "iconType": "", - "label": "", - "order": 400, - }, - ], - ] - `); + expect(security.navControlService.addUserMenuLinks.mock.calls[0][0]).toMatchSnapshot([ + { + href: 'profile-url', + iconType: 'user', + label: 'Profile', + order: 100, + setAsProfile: true, + }, + { + href: 'billing-url', + iconType: 'visGauge', + label: 'Billing', + order: 200, + }, + { + href: 'organization-url', + iconType: 'gear', + label: 'Organization', + order: 300, + }, + expect.any(Object), + ]); expect(chrome.setHelpMenuLinks).toHaveBeenCalledTimes(1); expect(chrome.setHelpMenuLinks.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -322,13 +167,12 @@ describe('maybeAddCloudLinks', () => { security.authc.getCurrentUser.mockResolvedValue( securityMock.createMockAuthenticatedUser({ elastic_cloud_user: false }) ); - const { chrome, docLinks, uiSettings } = coreMock.createStart(); + const core = coreMock.createStart(); + const { chrome } = core; maybeAddCloudLinks({ security, - chrome, + core, cloud: { ...cloudMock.createStart(), isCloudEnabled: true }, - docLinks, - uiSettingsClient: uiSettings, }); // Since there's a promise, let's wait for the next tick await new Promise((resolve) => process.nextTick(resolve)); diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.ts index 1becd2cdf7254..33fb4df7bfce2 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.ts @@ -9,28 +9,18 @@ import { catchError, defer, filter, map, of, combineLatest } from 'rxjs'; import { i18n } from '@kbn/i18n'; import type { CloudStart } from '@kbn/cloud-plugin/public'; -import type { ChromeStart } from '@kbn/core/public'; +import type { CoreStart } from '@kbn/core/public'; import type { SecurityPluginStart } from '@kbn/security-plugin/public'; -import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; -import type { DocLinksStart } from '@kbn/core-doc-links-browser'; import { createUserMenuLinks } from './user_menu_links'; import { createHelpMenuLinks } from './help_menu_links'; export interface MaybeAddCloudLinksDeps { + core: CoreStart; security: SecurityPluginStart; - chrome: ChromeStart; cloud: CloudStart; - docLinks: DocLinksStart; - uiSettingsClient: IUiSettingsClient; } -export function maybeAddCloudLinks({ - security, - chrome, - cloud, - docLinks, - uiSettingsClient, -}: MaybeAddCloudLinksDeps): void { +export function maybeAddCloudLinks({ core, security, cloud }: MaybeAddCloudLinksDeps): void { const userObservable = defer(() => security.authc.getCurrentUser()).pipe( // Check if user is a cloud user. map((user) => user.elastic_cloud_user), @@ -39,7 +29,7 @@ export function maybeAddCloudLinks({ filter((isElasticCloudUser) => isElasticCloudUser === true), map(() => { if (cloud.deploymentUrl) { - chrome.setCustomNavLink({ + core.chrome.setCustomNavLink({ title: i18n.translate('xpack.cloudLinks.deploymentLinkLabel', { defaultMessage: 'Manage this deployment', }), @@ -47,22 +37,26 @@ export function maybeAddCloudLinks({ href: cloud.deploymentUrl, }); } - const userMenuLinks = createUserMenuLinks({ cloud, security, uiSettingsClient }); + const userMenuLinks = createUserMenuLinks({ + core, + cloud, + security, + }); security.navControlService.addUserMenuLinks(userMenuLinks); }) ); - const helpObservable = chrome.getHelpSupportUrl$(); + const helpObservable = core.chrome.getHelpSupportUrl$(); if (cloud.isCloudEnabled) { combineLatest({ user: userObservable, helpSupportUrl: helpObservable }).subscribe( ({ helpSupportUrl }) => { const helpMenuLinks = createHelpMenuLinks({ - docLinks, + docLinks: core.docLinks, helpSupportUrl, }); - chrome.setHelpMenuLinks(helpMenuLinks); + core.chrome.setHelpMenuLinks(helpMenuLinks); } ); } diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_hook.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_hook.ts index 75d6ae4d1d329..c0a4fcc3a09ac 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_hook.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_hook.ts @@ -7,21 +7,20 @@ import { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import type { SecurityPluginStart } from '@kbn/security-plugin/public'; import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import { useUpdateUserProfile } from '@kbn/user-profile-components'; interface Deps { uiSettingsClient: IUiSettingsClient; - security: SecurityPluginStart; } -export const useThemeDarkmodeToggle = ({ uiSettingsClient, security }: Deps) => { +export const useThemeDarkmodeToggle = ({ uiSettingsClient }: Deps) => { const [isDarkModeOn, setIsDarkModeOn] = useState(false); // If a value is set in kibana.yml (uiSettings.overrides.theme:darkMode) // we don't allow the user to change the theme color. const valueSetInKibanaConfig = uiSettingsClient.isOverridden('theme:darkMode'); - const { userProfileData, isLoading, update } = security.hooks.useUpdateUserProfile({ + const { userProfileData, isLoading, update } = useUpdateUserProfile({ notificationSuccess: { title: i18n.translate('xpack.cloudLinks.userMenuLinks.darkMode.successNotificationTitle', { defaultMessage: 'Color theme updated', diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_toggle.test.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_toggle.test.tsx index 2bebdad488498..6b06cd64b9e23 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_toggle.test.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_toggle.test.tsx @@ -11,18 +11,22 @@ import '@testing-library/jest-dom'; import { coreMock } from '@kbn/core/public/mocks'; import { securityMock } from '@kbn/security-plugin/public/mocks'; -import { ThemDarkModeToggle } from './theme_darkmode_toggle'; +import { ThemeDarkModeToggle } from './theme_darkmode_toggle'; -describe('ThemDarkModeToggle', () => { - const mockUseUpdateUserProfile = jest.fn(); - const mockGetSpaceDarkModeValue = jest.fn(); +const mockUseUpdateUserProfile = jest.fn(); +jest.mock('@kbn/user-profile-components', () => { + const original = jest.requireActual('@kbn/user-profile-components'); + return { + ...original, + useUpdateUserProfile: () => mockUseUpdateUserProfile(), + }; +}); + +describe('ThemeDarkModeToggle', () => { it('renders correctly and toggles dark mode', () => { - const security = { - ...securityMock.createStart(), - hooks: { useUpdateUserProfile: mockUseUpdateUserProfile }, - }; - const { uiSettings } = coreMock.createStart(); + const security = securityMock.createStart(); + const core = coreMock.createStart(); const mockUpdate = jest.fn(); mockUseUpdateUserProfile.mockReturnValue({ @@ -31,10 +35,8 @@ describe('ThemDarkModeToggle', () => { update: mockUpdate, }); - mockGetSpaceDarkModeValue.mockReturnValue(false); - const { getByTestId, rerender } = render( - + ); const toggleSwitch = getByTestId('darkModeToggleSwitch'); @@ -49,7 +51,7 @@ describe('ThemDarkModeToggle', () => { }); // Rerender the component to apply the new props - rerender(); + rerender(); fireEvent.click(toggleSwitch); expect(mockUpdate).toHaveBeenLastCalledWith({ userSettings: { darkMode: 'light' } }); diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_toggle.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_toggle.tsx index 3f85c2ed63a76..20085172b2c68 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_toggle.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/theme_darkmode_toggle.tsx @@ -17,19 +17,30 @@ import { import { i18n } from '@kbn/i18n'; import type { SecurityPluginStart } from '@kbn/security-plugin/public'; import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import { UserProfilesKibanaProvider } from '@kbn/user-profile-components'; +import { CoreStart } from '@kbn/core-lifecycle-browser'; +import { toMountPoint } from '@kbn/kibana-react-plugin/public'; + import { useThemeDarkmodeToggle } from './theme_darkmode_hook'; interface Props { - uiSettingsClient: IUiSettingsClient; security: SecurityPluginStart; + core: CoreStart; } -export const ThemDarkModeToggle = ({ security, uiSettingsClient }: Props) => { +export const ThemeDarkModeToggle = ({ security, core }: Props) => { + return ( + + + + ); +}; + +function ThemeDarkModeToggleUi({ uiSettingsClient }: { uiSettingsClient: IUiSettingsClient }) { const toggleTextSwitchId = useGeneratedHtmlId({ prefix: 'toggleTextSwitch' }); const { euiTheme } = useEuiTheme(); const { isVisible, toggle, isDarkModeOn, colorScheme } = useThemeDarkmodeToggle({ - security, uiSettingsClient, }); @@ -77,4 +88,4 @@ export const ThemDarkModeToggle = ({ security, uiSettingsClient }: Props) => { ); -}; +} diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/user_menu_links.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/user_menu_links.tsx index 1eae5d6ed0c58..16ffa32360f25 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/user_menu_links.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/user_menu_links.tsx @@ -9,17 +9,17 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import type { CloudStart } from '@kbn/cloud-plugin/public'; import type { SecurityPluginStart, UserMenuLink } from '@kbn/security-plugin/public'; -import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; -import { ThemDarkModeToggle } from './theme_darkmode_toggle'; +import type { CoreStart } from '@kbn/core/public'; +import { ThemeDarkModeToggle } from './theme_darkmode_toggle'; export const createUserMenuLinks = ({ + core, cloud, security, - uiSettingsClient, }: { + core: CoreStart; cloud: CloudStart; security: SecurityPluginStart; - uiSettingsClient: IUiSettingsClient; }): UserMenuLink[] => { const { profileUrl, billingUrl, organizationUrl } = cloud; @@ -60,7 +60,7 @@ export const createUserMenuLinks = ({ } userMenuLinks.push({ - content: , + content: , order: 400, label: '', iconType: '', diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.tsx index 7ee3f0969251d..38b568791b70b 100755 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.tsx @@ -44,11 +44,9 @@ export class CloudLinksPlugin } if (security) { maybeAddCloudLinks({ + core, security, - chrome: core.chrome, cloud, - docLinks: core.docLinks, - uiSettingsClient: core.uiSettings, }); } } diff --git a/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json b/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json index 0dfa7ce42858d..f1a67895cdd5e 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json +++ b/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json @@ -20,6 +20,9 @@ "@kbn/core-chrome-browser", "@kbn/core-doc-links-browser", "@kbn/core-ui-settings-browser", + "@kbn/user-profile-components", + "@kbn/core-lifecycle-browser", + "@kbn/kibana-react-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 91d42c2544191..058b23e3477a5 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -123,5 +123,3 @@ export const VULNERABILITIES_SEVERITY: Record = { CRITICAL: 'CRITICAL', UNKNOWN: 'UNKNOWN', }; - -export const VULNERABILITIES_ENUMERATION = 'CVE'; diff --git a/x-pack/plugins/cloud_security_posture/common/utils/get_safe_vulnerabilities_query_filter.ts b/x-pack/plugins/cloud_security_posture/common/utils/get_safe_vulnerabilities_query_filter.ts index 5cbbf6dd054a7..fb2bbd1c51273 100644 --- a/x-pack/plugins/cloud_security_posture/common/utils/get_safe_vulnerabilities_query_filter.ts +++ b/x-pack/plugins/cloud_security_posture/common/utils/get_safe_vulnerabilities_query_filter.ts @@ -5,7 +5,7 @@ * 2.0. */ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; -import { VULNERABILITIES_ENUMERATION, VULNERABILITIES_SEVERITY } from '../constants'; +import { VULNERABILITIES_SEVERITY } from '../constants'; export const getSafeVulnerabilitiesQueryFilter = (query?: QueryDslQueryContainer) => ({ ...query, @@ -29,7 +29,6 @@ export const getSafeVulnerabilitiesQueryFilter = (query?: QueryDslQueryContainer { exists: { field: 'vulnerability.severity' } }, { exists: { field: 'resource.id' } }, { exists: { field: 'resource.name' } }, - { match_phrase: { 'vulnerability.enumeration': VULNERABILITIES_ENUMERATION } }, ], }, }); diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/create_detection_rule.ts b/x-pack/plugins/cloud_security_posture/public/common/api/create_detection_rule.ts new file mode 100644 index 0000000000000..ef0aa3321f35e --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/api/create_detection_rule.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from '@kbn/core/public'; + +const DETECTION_ENGINE_URL = '/api/detection_engine' as const; +const DETECTION_ENGINE_RULES_URL = `${DETECTION_ENGINE_URL}/rules` as const; + +interface RuleCreateProps { + type: string; + language: string; + license: string; + author: string[]; + filters: any[]; + false_positives: any[]; + risk_score: number; + risk_score_mapping: any[]; + severity: string; + severity_mapping: any[]; + threat: any[]; + interval: string; + from: string; + to: string; + timestamp_override: string; + timestamp_override_fallback_disabled: boolean; + actions: any[]; + enabled: boolean; + alert_suppression: { + group_by: string[]; + missing_fields_strategy: string; + }; + index: string[]; + query: string; + references: string[]; + name: string; + description: string; + tags: string[]; +} + +export interface RuleResponse extends RuleCreateProps { + id: string; +} + +export const createDetectionRule = async ({ + http, + rule, +}: { + http: HttpSetup; + rule: RuleCreateProps; +}): Promise => { + const res = await http.post(DETECTION_ENGINE_RULES_URL, { + body: JSON.stringify(rule), + }); + + return res as RuleResponse; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx b/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx new file mode 100644 index 0000000000000..57684d02fd157 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { + EuiButton, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiText, + useGeneratedHtmlId, +} from '@elastic/eui'; +import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import type { HttpSetup } from '@kbn/core/public'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { CREATE_RULE_ACTION_SUBJ, TAKE_ACTION_SUBJ } from './test_subjects'; +import { useKibana } from '../common/hooks/use_kibana'; +import type { RuleResponse } from '../common/api/create_detection_rule'; + +const RULE_PAGE_PATH = '/app/security/rules/id/'; + +interface TakeActionProps { + createRuleFn: (http: HttpSetup) => Promise; +} +/* + * This component is used to create a detection rule from Flyout. + * It accepts a createRuleFn parameter which is used to create a rule in a generic way. + */ +export const TakeAction = ({ createRuleFn }: TakeActionProps) => { + const [isPopoverOpen, setPopoverOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const closePopover = () => { + setPopoverOpen(false); + }; + + const smallContextMenuPopoverId = useGeneratedHtmlId({ + prefix: 'smallContextMenuPopover', + }); + + const { http, notifications } = useKibana().services; + + const showSuccessToast = (ruleResponse: RuleResponse) => { + return notifications.toasts.addSuccess({ + toastLifeTimeMs: 10000, + color: 'success', + iconType: '', + text: toMountPoint( +
+ + {ruleResponse.name} + {` `} + + + + + + + + + + + + +
+ ), + }); + }; + + const button = ( + setPopoverOpen(!isPopoverOpen)} + > + + + ); + + return ( + + { + closePopover(); + setIsLoading(true); + const ruleResponse = await createRuleFn(http); + setIsLoading(false); + showSuccessToast(ruleResponse); + }} + data-test-subj={CREATE_RULE_ACTION_SUBJ} + > + + , + ]} + /> + + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts index 638076818d3f0..a1fa5d985df3c 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts @@ -32,4 +32,7 @@ export const NO_VULNERABILITIES_STATUS_TEST_SUBJ = { export const VULNERABILITIES_CONTAINER_TEST_SUBJ = 'vulnerabilities_container'; -export const VULNERABILITIES_CVSS_SCORE_BADGE_SUBJ = 'vuknerabilities_cvss_score_badge'; +export const VULNERABILITIES_CVSS_SCORE_BADGE_SUBJ = 'vulnerabilities_cvss_score_badge'; + +export const TAKE_ACTION_SUBJ = 'csp:take_action'; +export const CREATE_RULE_ACTION_SUBJ = 'csp:create_rule'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_flyout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_flyout.tsx index f59463e00125f..2c59f360850d8 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_flyout.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_flyout.tsx @@ -25,9 +25,11 @@ import { } from '@elastic/eui'; import { assertNever } from '@kbn/std'; import { i18n } from '@kbn/i18n'; +import type { HttpSetup } from '@kbn/core/public'; import cisLogoIcon from '../../../assets/icons/cis_logo.svg'; import { CspFinding } from '../../../../common/schemas/csp_finding'; import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge'; +import { TakeAction } from '../../../components/take_action'; import { TableTab } from './table_tab'; import { JsonTab } from './json_tab'; import { OverviewTab } from './overview_tab'; @@ -36,6 +38,7 @@ import type { BenchmarkId } from '../../../../common/types'; import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon'; import { BenchmarkName } from '../../../../common/types'; import { FINDINGS_FLYOUT } from '../test_subjects'; +import { createDetectionRuleFromFinding } from '../utils/create_detection_rule_from_finding'; const tabs = [ { @@ -127,6 +130,9 @@ export const FindingsRuleFlyout = ({ }: FindingFlyoutProps) => { const [tab, setTab] = useState(tabs[0]); + const createMisconfigurationRuleFn = async (http: HttpSetup) => + await createDetectionRuleFromFinding(http, findings); + return ( @@ -160,7 +166,7 @@ export const FindingsRuleFlyout = ({ - + + + + diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/create_detection_rule_from_finding.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/create_detection_rule_from_finding.ts new file mode 100644 index 0000000000000..179ac6e27713c --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/create_detection_rule_from_finding.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpSetup } from '@kbn/core/public'; +import type { CspFinding } from '../../../../common/schemas/csp_finding'; +import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '../../../../common/constants'; +import { createDetectionRule } from '../../../common/api/create_detection_rule'; + +const DEFAULT_RULE_RISK_SCORE = 0; +const DEFAULT_RULE_SEVERITY = 'low'; +const DEFAULT_RULE_ENABLED = true; +const DEFAULT_RULE_AUTHOR = 'Elastic'; +const DEFAULT_RULE_LICENSE = 'Elastic License v2'; +const ALERT_SUPPRESSION_FIELD = 'resource.id'; +const ALERT_TIMESTAMP_FIELD = 'event.ingested'; + +enum AlertSuppressionMissingFieldsStrategy { + // per each document a separate alert will be created + DoNotSuppress = 'doNotSuppress', + // only one alert will be created per suppress by bucket + Suppress = 'suppress', +} + +const convertReferencesLinksToArray = (input: string | undefined) => { + if (!input) { + return []; + } + // Match all URLs in the input string using a regular expression + const matches = input.match(/(https?:\/\/\S+)/g); + + if (!matches) { + return []; + } + + // Remove the numbers and new lines + return matches.map((link) => link.replace(/^\d+\. /, '').replace(/\n/g, '')); +}; + +const STATIC_RULE_TAGS = ['Elastic', 'Cloud Security']; + +const generateMisconfigurationsTags = (finding: CspFinding) => { + return [STATIC_RULE_TAGS] + .concat(finding.rule.tags) + .concat( + finding.rule.benchmark.posture_type ? [finding.rule.benchmark.posture_type.toUpperCase()] : [] + ) + .flat(); +}; + +const generateMisconfigurationsRuleQuery = (finding: CspFinding) => { + return ` + rule.benchmark.rule_number: "${finding.rule.benchmark.rule_number}" + AND rule.benchmark.id: "${finding.rule.benchmark.id}" + AND result.evaluation: "failed" + `; +}; + +/* + * Creates a detection rule from a CspFinding + */ +export const createDetectionRuleFromFinding = async (http: HttpSetup, finding: CspFinding) => { + return await createDetectionRule({ + http, + rule: { + type: 'query', + language: 'kuery', + license: DEFAULT_RULE_LICENSE, + author: [DEFAULT_RULE_AUTHOR], + filters: [], + false_positives: [], + risk_score: DEFAULT_RULE_RISK_SCORE, + risk_score_mapping: [], + severity: DEFAULT_RULE_SEVERITY, + severity_mapping: [], + threat: [], + interval: '1h', + from: 'now-7200s', + to: 'now', + timestamp_override: ALERT_TIMESTAMP_FIELD, + timestamp_override_fallback_disabled: false, + actions: [], + enabled: DEFAULT_RULE_ENABLED, + alert_suppression: { + group_by: [ALERT_SUPPRESSION_FIELD], + missing_fields_strategy: AlertSuppressionMissingFieldsStrategy.Suppress, + }, + index: [LATEST_FINDINGS_INDEX_DEFAULT_NS], + query: generateMisconfigurationsRuleQuery(finding), + references: convertReferencesLinksToArray(finding.rule.references), + name: finding.rule.name, + description: finding.rule.rationale, + tags: generateMisconfigurationsTags(finding), + }, + }); +}; diff --git a/x-pack/plugins/cloud_security_posture/tsconfig.json b/x-pack/plugins/cloud_security_posture/tsconfig.json index ae8f7d610002b..a88bbf2bd0995 100755 --- a/x-pack/plugins/cloud_security_posture/tsconfig.json +++ b/x-pack/plugins/cloud_security_posture/tsconfig.json @@ -48,7 +48,7 @@ "@kbn/shared-ux-router", "@kbn/core-saved-objects-server", "@kbn/share-plugin", - "@kbn/core-http-server", + "@kbn/core-http-server" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/enterprise_search/jest.config.dev.js b/x-pack/plugins/enterprise_search/jest.config.dev.js new file mode 100644 index 0000000000000..638235a6deef5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/jest.config.dev.js @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../', + projects: [ + '/x-pack/plugins/enterprise_search/public/**/jest.config.js', + '/x-pack/plugins/enterprise_search/common/**/jest.config.js', + '/x-pack/plugins/enterprise_search/server/**/jest.config.js', + ], +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/jest.config.js b/x-pack/plugins/enterprise_search/public/applications/analytics/jest.config.js new file mode 100644 index 0000000000000..58b5334165ec5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/jest.config.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/x-pack/plugins/enterprise_search/public/applications/analytics'], + collectCoverage: true, + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/enterprise_search/public/applications/**/*.{ts,tsx}', + '!/x-pack/plugins/enterprise_search/public/*.ts', + '!/x-pack/plugins/enterprise_search/server/*.ts', + '!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}', + ], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/enterprise_search/public/applications/analytics', + modulePathIgnorePatterns: [ + '/x-pack/plugins/enterprise_search/public/applications/app_search/cypress', + '/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress', + ], +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/jest.config.js b/x-pack/plugins/enterprise_search/public/applications/app_search/jest.config.js new file mode 100644 index 0000000000000..7d591c369c18b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/jest.config.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/x-pack/plugins/enterprise_search/public/applications/app_search'], + collectCoverage: true, + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/enterprise_search/public/applications/**/*.{ts,tsx}', + '!/x-pack/plugins/enterprise_search/public/*.ts', + '!/x-pack/plugins/enterprise_search/server/*.ts', + '!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}', + ], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/enterprise_search/public/applications/app_search', + modulePathIgnorePatterns: [ + '/x-pack/plugins/enterprise_search/public/applications/app_search/cypress', + '/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress', + ], +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/applications/jest.config.js b/x-pack/plugins/enterprise_search/public/applications/applications/jest.config.js new file mode 100644 index 0000000000000..1e04c0845ec9f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/applications/jest.config.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/x-pack/plugins/enterprise_search/public/applications/applications'], + collectCoverage: true, + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/enterprise_search/public/applications/**/*.{ts,tsx}', + '!/x-pack/plugins/enterprise_search/public/*.ts', + '!/x-pack/plugins/enterprise_search/server/*.ts', + '!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}', + ], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/enterprise_search/public/applications/applications', + modulePathIgnorePatterns: [ + '/x-pack/plugins/enterprise_search/public/applications/app_search/cypress', + '/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress', + ], +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/elasticsearch/jest.config.js b/x-pack/plugins/enterprise_search/public/applications/elasticsearch/jest.config.js new file mode 100644 index 0000000000000..ab90da605f2b3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/elasticsearch/jest.config.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/x-pack/plugins/enterprise_search/public/applications/elasticsearch'], + collectCoverage: true, + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/enterprise_search/public/applications/**/*.{ts,tsx}', + '!/x-pack/plugins/enterprise_search/public/*.ts', + '!/x-pack/plugins/enterprise_search/server/*.ts', + '!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}', + ], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/enterprise_search/public/applications/elasticsearch', + modulePathIgnorePatterns: [ + '/x-pack/plugins/enterprise_search/public/applications/app_search/cypress', + '/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress', + ], +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/jest.config.js b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/jest.config.js new file mode 100644 index 0000000000000..a55b8bbc715f4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/jest.config.js @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: [ + '/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content', + ], + collectCoverage: true, + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/enterprise_search/public/applications/**/*.{ts,tsx}', + '!/x-pack/plugins/enterprise_search/public/*.ts', + '!/x-pack/plugins/enterprise_search/server/*.ts', + '!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}', + ], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content', + modulePathIgnorePatterns: [ + '/x-pack/plugins/enterprise_search/public/applications/app_search/cypress', + '/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress', + ], +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/jest.config.js b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/jest.config.js new file mode 100644 index 0000000000000..fd5a6db3b8e0c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/jest.config.js @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: [ + '/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview', + ], + collectCoverage: true, + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/enterprise_search/public/applications/**/*.{ts,tsx}', + '!/x-pack/plugins/enterprise_search/public/*.ts', + '!/x-pack/plugins/enterprise_search/server/*.ts', + '!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}', + ], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview', + modulePathIgnorePatterns: [ + '/x-pack/plugins/enterprise_search/public/applications/app_search/cypress', + '/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress', + ], +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/esre/jest.config.js b/x-pack/plugins/enterprise_search/public/applications/esre/jest.config.js new file mode 100644 index 0000000000000..e6ef9e507cf27 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/esre/jest.config.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/x-pack/plugins/enterprise_search/public/applications/esre'], + collectCoverage: true, + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/enterprise_search/public/applications/**/*.{ts,tsx}', + '!/x-pack/plugins/enterprise_search/public/*.ts', + '!/x-pack/plugins/enterprise_search/server/*.ts', + '!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}', + ], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/enterprise_search/public/applications/esre', + modulePathIgnorePatterns: [ + '/x-pack/plugins/enterprise_search/public/applications/app_search/cypress', + '/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress', + ], +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/search_experiences/jest.config.js b/x-pack/plugins/enterprise_search/public/applications/search_experiences/jest.config.js new file mode 100644 index 0000000000000..1e39c00ae9893 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/search_experiences/jest.config.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/x-pack/plugins/enterprise_search/public/applications/search_experiences'], + collectCoverage: true, + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/enterprise_search/public/applications/**/*.{ts,tsx}', + '!/x-pack/plugins/enterprise_search/public/*.ts', + '!/x-pack/plugins/enterprise_search/server/*.ts', + '!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}', + ], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/enterprise_search/public/applications/search_experiences', + modulePathIgnorePatterns: [ + '/x-pack/plugins/enterprise_search/public/applications/app_search/cypress', + '/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress', + ], +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/jest.config.js b/x-pack/plugins/enterprise_search/public/applications/shared/jest.config.js new file mode 100644 index 0000000000000..5ee13cc30aeaf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/jest.config.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/x-pack/plugins/enterprise_search/public/applications/shared'], + collectCoverage: true, + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/enterprise_search/public/applications/**/*.{ts,tsx}', + '!/x-pack/plugins/enterprise_search/public/*.ts', + '!/x-pack/plugins/enterprise_search/server/*.ts', + '!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}', + ], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/enterprise_search/public/applications/shared', + modulePathIgnorePatterns: [ + '/x-pack/plugins/enterprise_search/public/applications/app_search/cypress', + '/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress', + ], +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/vector_search/jest.config.js b/x-pack/plugins/enterprise_search/public/applications/vector_search/jest.config.js new file mode 100644 index 0000000000000..24158650aa75f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/vector_search/jest.config.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/x-pack/plugins/enterprise_search/public/applications/vector_search'], + collectCoverage: true, + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/enterprise_search/public/applications/**/*.{ts,tsx}', + '!/x-pack/plugins/enterprise_search/public/*.ts', + '!/x-pack/plugins/enterprise_search/server/*.ts', + '!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}', + ], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/enterprise_search/public/applications/vector_search', + modulePathIgnorePatterns: [ + '/x-pack/plugins/enterprise_search/public/applications/app_search/cypress', + '/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress', + ], +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/jest.config.js b/x-pack/plugins/enterprise_search/public/applications/workplace_search/jest.config.js new file mode 100644 index 0000000000000..6ae1b5b9b1a84 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/jest.config.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/x-pack/plugins/enterprise_search/public/applications/workplace_search'], + collectCoverage: true, + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/enterprise_search/public/applications/**/*.{ts,tsx}', + '!/x-pack/plugins/enterprise_search/public/*.ts', + '!/x-pack/plugins/enterprise_search/server/*.ts', + '!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}', + ], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/enterprise_search/public/applications/workplace_search', + modulePathIgnorePatterns: [ + '/x-pack/plugins/enterprise_search/public/applications/app_search/cypress', + '/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress', + ], +}; diff --git a/x-pack/plugins/enterprise_search/public/jest.config.js b/x-pack/plugins/enterprise_search/public/jest.config.js index c527b85707b42..fec5a831f2fee 100644 --- a/x-pack/plugins/enterprise_search/public/jest.config.js +++ b/x-pack/plugins/enterprise_search/public/jest.config.js @@ -8,11 +8,15 @@ module.exports = { preset: '@kbn/test', rootDir: '../../../..', + /** all nested directories have their own Jest config file */ + testMatch: [ + '/x-pack/plugins/enterprise_search/public/applications/*.test.{js,mjs,ts,tsx}', + ], roots: ['/x-pack/plugins/enterprise_search/public'], collectCoverage: true, coverageReporters: ['text', 'html'], collectCoverageFrom: [ - '/x-pack/plugins/enterprise_search/**/*.{ts,tsx}', + '/x-pack/plugins/enterprise_search/public/applications/*.{ts,tsx}', '!/x-pack/plugins/enterprise_search/public/*.ts', '!/x-pack/plugins/enterprise_search/server/*.ts', '!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}', diff --git a/x-pack/plugins/fleet/common/constants/output.ts b/x-pack/plugins/fleet/common/constants/output.ts index b23fbc3cc2c2a..59f3ceff36a0f 100644 --- a/x-pack/plugins/fleet/common/constants/output.ts +++ b/x-pack/plugins/fleet/common/constants/output.ts @@ -42,6 +42,12 @@ export const kafkaAuthType = { Userpass: 'user_pass', Ssl: 'ssl', Kerberos: 'kerberos', + None: 'none', +} as const; + +export const kafkaConnectionType = { + Plaintext: 'plaintext', + Encryption: 'encryption', } as const; export const kafkaSaslMechanism = { @@ -60,18 +66,19 @@ export const kafkaTopicWhenType = { Equals: 'equals', Contains: 'contains', Regexp: 'regexp', - Range: 'range', - Network: 'network', - HasFields: 'has_fields', - Or: 'or', - And: 'and', - Not: 'not', } as const; export const kafkaAcknowledgeReliabilityLevel = { - Commit: 'Wait for local commit', - Replica: 'Wait for all replicas to commit', - DoNotWait: 'Do not wait', + Commit: 1, + Replica: -1, + DoNotWait: 0, +} as const; + +export const kafkaVerificationModes = { + Full: 'full', + None: 'none', + Strict: 'strict', + Certificate: 'certificate', } as const; export const kafkaSupportedVersions = [ diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index cc1ad5c98c32a..200cebba48cc7 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -7701,6 +7701,15 @@ }, "key": { "type": "string" + }, + "verification_mode": { + "type": "string", + "enum": [ + "none", + "full", + "certificate", + "strict" + ] } } }, @@ -7751,6 +7760,13 @@ "auth_type": { "type": "string" }, + "connection_type": { + "type": "string", + "enum": [ + "plaintext", + "encryption" + ] + }, "username": { "type": "string" }, @@ -7826,11 +7842,8 @@ "broker_timeout": { "type": "number" }, - "broker_buffer_size": { + "required_acks": { "type": "number" - }, - "broker_ack_reliability": { - "type": "string" } }, "required": [ @@ -8103,6 +8116,15 @@ }, "key": { "type": "string" + }, + "verification_mode": { + "type": "string", + "enum": [ + "none", + "full", + "certificate", + "strict" + ] } } }, @@ -8153,6 +8175,13 @@ "auth_type": { "type": "string" }, + "connection_type": { + "type": "string", + "enum": [ + "plaintext", + "encryption" + ] + }, "username": { "type": "string" }, @@ -8228,10 +8257,7 @@ "broker_timeout": { "type": "number" }, - "broker_ack_reliability": { - "type": "string" - }, - "broker_buffer_size": { + "required_acks": { "type": "number" } }, diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 0d72245fa2d99..6c216e464a104 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -4959,6 +4959,13 @@ components: type: string key: type: string + verification_mode: + type: string + enum: + - none + - full + - certificate + - strict proxy_id: type: string shipper: @@ -4990,6 +4997,11 @@ components: type: string auth_type: type: string + connection_type: + type: string + enum: + - plaintext + - encryption username: type: string password: @@ -5038,10 +5050,8 @@ components: type: number broker_timeout: type: number - broker_buffer_size: + required_acks: type: number - broker_ack_reliability: - type: string required: - name - type @@ -5223,6 +5233,13 @@ components: type: string key: type: string + verification_mode: + type: string + enum: + - none + - full + - certificate + - strict proxy_id: type: string shipper: @@ -5254,6 +5271,11 @@ components: type: string auth_type: type: string + connection_type: + type: string + enum: + - plaintext + - encryption username: type: string password: @@ -5302,9 +5324,7 @@ components: type: number broker_timeout: type: number - broker_ack_reliability: - type: string - broker_buffer_size: + required_acks: type: number required: - name diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/output_create_request_kafka.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/output_create_request_kafka.yaml index dbed2b44dc08a..fa76c2301ed94 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/output_create_request_kafka.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/output_create_request_kafka.yaml @@ -35,6 +35,9 @@ properties: type: string key: type: string + verification_mode: + type: string + enum: ['none', 'full', 'certificate', 'strict'] proxy_id: type: string shipper: @@ -66,6 +69,9 @@ properties: type: string auth_type: type: string + connection_type: + type: string + enum: ['plaintext', 'encryption'] username: type: string password: @@ -114,10 +120,8 @@ properties: type: number broker_timeout: type: number - broker_buffer_size: + required_acks: type: number - broker_ack_reliability: - type: string required: - name - type diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/output_update_request_kafka.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/output_update_request_kafka.yaml index bb1e76fa70a55..2ce5525a1a9f4 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/output_update_request_kafka.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/output_update_request_kafka.yaml @@ -35,6 +35,9 @@ properties: type: string key: type: string + verification_mode: + type: string + enum: ['none', 'full', 'certificate', 'strict'] proxy_id: type: string shipper: @@ -66,6 +69,9 @@ properties: type: string auth_type: type: string + connection_type: + type: string + enum: ['plaintext', 'encryption'] username: type: string password: @@ -114,9 +120,7 @@ properties: type: number broker_timeout: type: number - broker_ack_reliability: - type: string - broker_buffer_size: + required_acks: type: number required: - name diff --git a/x-pack/plugins/fleet/common/types/models/output.ts b/x-pack/plugins/fleet/common/types/models/output.ts index 95f353575afd6..a537e0ab0233b 100644 --- a/x-pack/plugins/fleet/common/types/models/output.ts +++ b/x-pack/plugins/fleet/common/types/models/output.ts @@ -11,14 +11,18 @@ import type { kafkaAuthType, kafkaCompressionType, kafkaSaslMechanism } from '.. import type { kafkaPartitionType } from '../../constants'; import type { kafkaTopicWhenType } from '../../constants'; import type { kafkaAcknowledgeReliabilityLevel } from '../../constants'; +import type { kafkaVerificationModes } from '../../constants'; +import type { kafkaConnectionType } from '../../constants'; export type OutputType = typeof outputType; export type KafkaCompressionType = typeof kafkaCompressionType; export type KafkaAuthType = typeof kafkaAuthType; +export type KafkaConnectionTypeType = typeof kafkaConnectionType; export type KafkaSaslMechanism = typeof kafkaSaslMechanism; export type KafkaPartitionType = typeof kafkaPartitionType; export type KafkaTopicWhenType = typeof kafkaTopicWhenType; export type KafkaAcknowledgeReliabilityLevel = typeof kafkaAcknowledgeReliabilityLevel; +export type KafkaVerificationMode = typeof kafkaVerificationModes; interface NewBaseOutput { is_default: boolean; @@ -34,6 +38,7 @@ interface NewBaseOutput { certificate_authorities?: string[]; certificate?: string; key?: string; + verification_mode?: ValueOf; } | null; proxy_id?: string | null; shipper?: ShipperOutput | null; @@ -76,6 +81,7 @@ export interface KafkaOutput extends NewBaseOutput { compression?: ValueOf; compression_level?: number; auth_type?: ValueOf; + connection_type?: ValueOf; username?: string; password?: string; sasl?: { @@ -105,6 +111,5 @@ export interface KafkaOutput extends NewBaseOutput { }>; timeout?: number; broker_timeout?: number; - broker_ack_reliability?: ValueOf; - broker_buffer_size?: number; + required_acks?: ValueOf; } diff --git a/x-pack/plugins/fleet/cypress/e2e/fleet_settings_outputs.cy.ts b/x-pack/plugins/fleet/cypress/e2e/fleet_settings_outputs.cy.ts index 45ff5257fc7c2..2d9f33d8e1efa 100644 --- a/x-pack/plugins/fleet/cypress/e2e/fleet_settings_outputs.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/fleet_settings_outputs.cy.ts @@ -5,10 +5,6 @@ * 2.0. */ -import { allowedExperimentalValues } from '../../common/experimental_features'; - -import { ExperimentalFeaturesService } from '../../public/services'; - import { getSpecificSelectorId, SETTINGS_CONFIRM_MODAL_BTN, @@ -42,323 +38,358 @@ describe('Outputs', () => { }); describe('Kafka', () => { - ExperimentalFeaturesService.init(allowedExperimentalValues); - - const { kafkaOutput: isKafkaOutputEnabled } = ExperimentalFeaturesService.get(); - - // TODO: Remove IF statement once Kafka is GA - if (!isKafkaOutputEnabled) { - it('is not available', () => { - visit('/app/fleet/settings'); - }); - } else { - describe('Form validation', () => { - it('renders all form fields', () => { - selectKafkaOutput(); - - cy.getBySel(SETTINGS_OUTPUTS.NAME_INPUT); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.VERSION_SELECT); - cy.get('[placeholder="Specify host"'); - cy.getBySel(SETTINGS_OUTPUTS.ADD_HOST_ROW_BTN); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SELECT).within(() => { - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_USERNAME_PASSWORD_OPTION); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SSL_OPTION); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_KERBEROS_OPTION); - }); - - // Verify user/pass fields - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_USERNAME_INPUT); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_PASSWORD_INPUT); - - // Verify SSL fields - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SSL_OPTION).click(); - cy.get('[placeholder="Specify certificate authority"]'); - cy.get('[placeholder="Specify ssl certificate"]'); - cy.get('[placeholder="Specify certificate key"]'); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_USERNAME_PASSWORD_OPTION).click(); - - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SASL_SELECT).within(() => { - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SASL_PLAIN_OPTION); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SASL_SCRAM_256_OPTION); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SASL_SCRAM_512_OPTION); - }); + describe('Form validation', () => { + it('renders all form fields', () => { + selectKafkaOutput(); + + cy.getBySel(SETTINGS_OUTPUTS.NAME_INPUT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.VERSION_SELECT); + cy.get('[placeholder="Specify host"'); + cy.getBySel(SETTINGS_OUTPUTS.ADD_HOST_ROW_BTN); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SELECT).within(() => { + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_NONE_OPTION); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_USERNAME_PASSWORD_OPTION); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SSL_OPTION); + }); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_PANEL).within(() => { - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_SELECT); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_RANDOM_OPTION); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_HASH_OPTION); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_ROUND_ROBIN_OPTION); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_EVENTS_INPUT); - }); + // Verify user/pass fields + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_USERNAME_INPUT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_PASSWORD_INPUT); + cy.get('[placeholder="Specify certificate authority"]'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_VERIFICATION_MODE_INPUT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_CONNECTION_TYPE_SELECT).should( + 'not.exist' + ); + + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SASL_SELECT).within(() => { + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SASL_PLAIN_OPTION); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SASL_SCRAM_256_OPTION); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SASL_SCRAM_512_OPTION); + }); - // Verify Round Robin fields - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_RANDOM_OPTION).click(); + // Verify SSL fields + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SSL_OPTION).click(); + cy.get('[placeholder="Specify certificate authority"]'); + cy.get('[placeholder="Specify ssl certificate"]'); + cy.get('[placeholder="Specify certificate key"]'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_VERIFICATION_MODE_INPUT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_CONNECTION_TYPE_SELECT).should( + 'not.exist' + ); + + // Verify None fields + + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_NONE_OPTION).click(); + + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SASL_SELECT).should('not.exist'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_USERNAME_INPUT).should('not.exist'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_PASSWORD_INPUT).should('not.exist'); + cy.get('[placeholder="Specify ssl certificate"]').should('not.exist'); + cy.get('[placeholder="Specify certificate key"]').should('not.exist'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_VERIFICATION_MODE_INPUT).should( + 'not.exist' + ); + cy.get('[placeholder="Specify certificate authority"]').should('not.exist'); + + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_CONNECTION_TYPE_SELECT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_CONNECTION_TYPE_PLAIN_OPTION); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_CONNECTION_TYPE_ENCRYPTION_OPTION); + + cy.getBySel( + SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_CONNECTION_TYPE_ENCRYPTION_OPTION + ).click(); + + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_VERIFICATION_MODE_INPUT); + cy.get('[placeholder="Specify certificate authority"]'); + + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_USERNAME_PASSWORD_OPTION).click(); + + // Verify Partitioning fields + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_PANEL).within(() => { + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_SELECT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_RANDOM_OPTION); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_HASH_OPTION); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_ROUND_ROBIN_OPTION); cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_EVENTS_INPUT); + }); - // Verify Hash fields - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_HASH_OPTION).click(); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_HASH_INPUT); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_RANDOM_OPTION).click(); - - // Topics - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_PANEL).within(() => { - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_DEFAULT_TOPIC_INPUT); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_ADD_ROW_BUTTON); - }); + // Verify Round Robin fields + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_RANDOM_OPTION).click(); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_EVENTS_INPUT); - // Verify one topic processor fields - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_ADD_ROW_BUTTON).click(); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_TOPIC_INPUT); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_CONDITION_INPUT); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_WHEN_INPUT); - - // Verify additional topic processor fields - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_ADD_ROW_BUTTON).click(); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_TOPIC_INPUT); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_CONDITION_INPUT); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_WHEN_INPUT); - cy.getBySel(getSpecificSelectorId(SETTINGS_OUTPUTS_KAFKA.TOPICS_TOPIC_INPUT, 1)); - cy.getBySel(getSpecificSelectorId(SETTINGS_OUTPUTS_KAFKA.TOPICS_CONDITION_INPUT, 1)); - cy.getBySel(getSpecificSelectorId(SETTINGS_OUTPUTS_KAFKA.TOPICS_WHEN_INPUT, 1)); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_DRAG_HANDLE_ICON); - - // Verify remove topic processors - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_REMOVE_ROW_BUTTON).click(); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_REMOVE_ROW_BUTTON).click(); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_TOPIC_INPUT).should('not.exist'); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_CONDITION_INPUT).should('not.exist'); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_WHEN_INPUT).should('not.exist'); - - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_PANEL).within(() => { - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_KEY_INPUT); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_VALUE_INPUT); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_ADD_ROW_BUTTON).should('be.disabled'); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_REMOVE_ROW_BUTTON).should('be.disabled'); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_CLIENT_ID_INPUT); - }); + // Verify Hash fields + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_HASH_OPTION).click(); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_HASH_INPUT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_RANDOM_OPTION).click(); - // Verify add header - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_KEY_INPUT).type('key'); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_VALUE_INPUT).type('value'); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_ADD_ROW_BUTTON).should('be.enabled'); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_REMOVE_ROW_BUTTON).should('be.disabled'); + // Topics + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_PANEL).within(() => { + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_DEFAULT_TOPIC_INPUT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_ADD_ROW_BUTTON); + }); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_ADD_ROW_BUTTON).click(); - cy.getBySel(getSpecificSelectorId(SETTINGS_OUTPUTS_KAFKA.HEADERS_KEY_INPUT, 1)); - cy.getBySel(getSpecificSelectorId(SETTINGS_OUTPUTS_KAFKA.HEADERS_VALUE_INPUT, 1)); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_ADD_ROW_BUTTON).should('be.enabled'); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_REMOVE_ROW_BUTTON).should('be.enabled'); - - // Verify remove header - cy.getBySel( - getSpecificSelectorId(SETTINGS_OUTPUTS_KAFKA.HEADERS_REMOVE_ROW_BUTTON, 1) - ).click(); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_ADD_ROW_BUTTON).should('be.enabled'); + // Verify one topic processor fields + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_ADD_ROW_BUTTON).click(); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_TOPIC_INPUT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_CONDITION_INPUT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_WHEN_INPUT); + + // Verify additional topic processor fields + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_ADD_ROW_BUTTON).click(); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_TOPIC_INPUT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_CONDITION_INPUT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_WHEN_INPUT); + cy.getBySel(getSpecificSelectorId(SETTINGS_OUTPUTS_KAFKA.TOPICS_TOPIC_INPUT, 1)); + cy.getBySel(getSpecificSelectorId(SETTINGS_OUTPUTS_KAFKA.TOPICS_CONDITION_INPUT, 1)); + cy.getBySel(getSpecificSelectorId(SETTINGS_OUTPUTS_KAFKA.TOPICS_WHEN_INPUT, 1)); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_DRAG_HANDLE_ICON); + + // Verify remove topic processors + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_REMOVE_ROW_BUTTON).click(); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_REMOVE_ROW_BUTTON).click(); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_TOPIC_INPUT).should('not.exist'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_CONDITION_INPUT).should('not.exist'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_WHEN_INPUT).should('not.exist'); + + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_PANEL).within(() => { + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_KEY_INPUT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_VALUE_INPUT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_ADD_ROW_BUTTON).should('be.disabled'); cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_REMOVE_ROW_BUTTON).should('be.disabled'); - - // Compression - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_CODEC_INPUT).should('not.exist'); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_SWITCH).click(); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_LEVEL_INPUT).should('not.exist'); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_CODEC_INPUT).select('gzip'); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_LEVEL_INPUT).should('exist'); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_LEVEL_INPUT).select('1'); - - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.BROKER_PANEL).within(() => { - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.BROKER_ACK_RELIABILITY_SELECT); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.BROKER_CHANNEL_BUFFER_SIZE_SELECT); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.BROKER_TIMEOUT_SELECT); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.BROKER_REACHABILITY_TIMEOUT_SELECT); - }); - cy.getBySel(SETTINGS_OUTPUTS_KAFKA.KEY_INPUT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_CLIENT_ID_INPUT).should( + 'have.value', + 'Elastic' + ); }); - it('displays proper error messages', () => { - selectKafkaOutput(); - cy.getBySel(SETTINGS_SAVE_BTN).click(); - - cy.contains('Name is required'); - cy.contains('URL is required'); - cy.contains('Username is required'); - cy.contains('Password is required'); - cy.contains('Default topic is required'); - shouldDisplayError(SETTINGS_OUTPUTS.NAME_INPUT); - shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_USERNAME_INPUT); - shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_PASSWORD_INPUT); - shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.TOPICS_DEFAULT_TOPIC_INPUT); + // Verify add header + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_KEY_INPUT).type('key'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_VALUE_INPUT).type('value'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_ADD_ROW_BUTTON).should('be.enabled'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_REMOVE_ROW_BUTTON).should('be.disabled'); + + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_ADD_ROW_BUTTON).click(); + cy.getBySel(getSpecificSelectorId(SETTINGS_OUTPUTS_KAFKA.HEADERS_KEY_INPUT, 1)); + cy.getBySel(getSpecificSelectorId(SETTINGS_OUTPUTS_KAFKA.HEADERS_VALUE_INPUT, 1)); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_ADD_ROW_BUTTON).should('be.enabled'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_REMOVE_ROW_BUTTON).should('be.enabled'); + + // Verify remove header + cy.getBySel( + getSpecificSelectorId(SETTINGS_OUTPUTS_KAFKA.HEADERS_REMOVE_ROW_BUTTON, 1) + ).click(); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_ADD_ROW_BUTTON).should('be.enabled'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_REMOVE_ROW_BUTTON).should('be.disabled'); + + // Compression + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_CODEC_INPUT).should('not.exist'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_SWITCH).click(); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_LEVEL_INPUT).should('not.exist'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_CODEC_INPUT).select('gzip'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_LEVEL_INPUT).should('exist'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_LEVEL_INPUT).select('1'); + + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.BROKER_PANEL).within(() => { + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.BROKER_ACK_RELIABILITY_SELECT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.BROKER_TIMEOUT_SELECT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.BROKER_REACHABILITY_TIMEOUT_SELECT); }); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.KEY_INPUT); }); - // Test buttons presence before accessing output directly via url and delete via api - describe('Output operations', () => { - let kafkaOutputId: string; + it('displays proper error messages', () => { + selectKafkaOutput(); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_CLIENT_ID_INPUT).clear(); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.TOPICS_ADD_ROW_BUTTON).click(); + cy.getBySel(SETTINGS_SAVE_BTN).click(); + + cy.contains('Name is required'); + cy.contains('Host is required'); + cy.contains('Username is required'); + cy.contains('Password is required'); + cy.contains('Default topic is required'); + cy.contains('Topic is required'); + cy.contains( + 'Client ID is invalid. Only letters, numbers, dots, underscores, and dashes are allowed.' + ); + cy.contains('Must be a key, value pair i.e. "http.response.code: 200"'); + shouldDisplayError(SETTINGS_OUTPUTS.NAME_INPUT); + shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_USERNAME_INPUT); + shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_PASSWORD_INPUT); + shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.TOPICS_DEFAULT_TOPIC_INPUT); + shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.TOPICS_CONDITION_INPUT); + shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.TOPICS_TOPIC_INPUT); + shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.HEADERS_CLIENT_ID_INPUT); + }); + }); - before(() => { - loadKafkaOutput().then((data) => { - kafkaOutputId = data.item.id; - }); - }); + // Test buttons presence before accessing output directly via url and delete via api + describe('Output operations', () => { + let kafkaOutputId: string; - it('opens edit modal', () => { - visit('/app/fleet/settings'); - cy.get(`a[href="/app/fleet/settings/outputs/${kafkaOutputId}"]`) - .parents('tr') - .within(() => { - cy.contains(kafkaOutputBody.name); - cy.contains(kafkaOutputBody.type); - cy.contains(kafkaOutputBody.hosts[0]); - cy.getBySel('editOutputBtn').click(); - cy.url().should('include', `/app/fleet/settings/outputs/${kafkaOutputId}`); - }); - }); - it('delete output', () => { - visit('/app/fleet/settings'); - cy.get(`a[href="/app/fleet/settings/outputs/${kafkaOutputId}"]`) - .parents('tr') - .within(() => { - cy.get('[title="Delete"]').click(); - }); - cy.getBySel(SETTINGS_CONFIRM_MODAL_BTN).click(); - cy.get(`a[href="app/fleet/settings/outputs/${kafkaOutputId}"]`).should('not.exist'); + before(() => { + loadKafkaOutput().then((data) => { + kafkaOutputId = data.item.id; }); }); - describe('Form submit', () => { - let kafkaOutputId: string; - - before(() => { - interceptOutputId((id) => { - kafkaOutputId = id; + it('opens edit modal', () => { + visit('/app/fleet/settings'); + cy.get(`a[href="/app/fleet/settings/outputs/${kafkaOutputId}"]`) + .parents('tr') + .within(() => { + cy.contains(kafkaOutputBody.name); + cy.contains(kafkaOutputBody.type); + cy.contains(kafkaOutputBody.hosts[0]); + cy.getBySel('editOutputBtn').click(); + cy.url().should('include', `/app/fleet/settings/outputs/${kafkaOutputId}`); }); - }); + }); + it('delete output', () => { + visit('/app/fleet/settings'); + cy.get(`a[href="/app/fleet/settings/outputs/${kafkaOutputId}"]`) + .parents('tr') + .within(() => { + cy.get('[title="Delete"]').click(); + }); + cy.getBySel(SETTINGS_CONFIRM_MODAL_BTN).click(); + cy.get(`a[href="app/fleet/settings/outputs/${kafkaOutputId}"]`).should('not.exist'); + }); + }); - after(() => { - cleanupOutput(kafkaOutputId); + describe('Form submit', () => { + let kafkaOutputId: string; + + before(() => { + interceptOutputId((id) => { + kafkaOutputId = id; }); + }); - it('saves the output', () => { - selectKafkaOutput(); + after(() => { + cleanupOutput(kafkaOutputId); + }); - fillInKafkaOutputForm(); + it('saves the output', () => { + selectKafkaOutput(); - cy.intercept('POST', '**/api/fleet/outputs').as('saveOutput'); + fillInKafkaOutputForm(); - cy.getBySel(SETTINGS_SAVE_BTN).click(); + cy.intercept('POST', '**/api/fleet/outputs').as('saveOutput'); - cy.wait('@saveOutput').then((interception) => { - const responseBody = interception.response?.body; - cy.visit(`/app/fleet/settings/outputs/${responseBody?.item?.id}`); - }); + cy.getBySel(SETTINGS_SAVE_BTN).click(); - validateSavedKafkaOutputForm(); + cy.wait('@saveOutput').then((interception) => { + const responseBody = interception.response?.body; + cy.visit(`/app/fleet/settings/outputs/${responseBody?.item?.id}`); }); + + validateSavedKafkaOutputForm(); }); + }); - describe('Form edit', () => { - let kafkaOutputId: string; + describe('Form edit', () => { + let kafkaOutputId: string; - before(() => { - loadKafkaOutput().then((data) => { - kafkaOutputId = data.item.id; - }); - }); - after(() => { - cleanupOutput(kafkaOutputId); + before(() => { + loadKafkaOutput().then((data) => { + kafkaOutputId = data.item.id; }); + }); + after(() => { + cleanupOutput(kafkaOutputId); + }); - it('edits the output', () => { - visit(`/app/fleet/settings/outputs/${kafkaOutputId}`); + it('edits the output', () => { + visit(`/app/fleet/settings/outputs/${kafkaOutputId}`); - resetKafkaOutputForm(); + resetKafkaOutputForm(); - fillInKafkaOutputForm(); + fillInKafkaOutputForm(); - cy.getBySel(SETTINGS_SAVE_BTN).click(); - cy.getBySel(SETTINGS_CONFIRM_MODAL_BTN).click(); - visit(`/app/fleet/settings/outputs/${kafkaOutputId}`); + cy.getBySel(SETTINGS_SAVE_BTN).click(); + cy.getBySel(SETTINGS_CONFIRM_MODAL_BTN).click(); + visit(`/app/fleet/settings/outputs/${kafkaOutputId}`); - validateSavedKafkaOutputForm(); - }); + validateSavedKafkaOutputForm(); }); + }); - describe('Form output type change', () => { - let kafkaOutputToESId: string; - let kafkaOutputToLogstashId: string; - let logstashOutputToKafkaId: string; - let esOutputToKafkaId: string; + describe('Form output type change', () => { + let kafkaOutputToESId: string; + let kafkaOutputToLogstashId: string; + let logstashOutputToKafkaId: string; + let esOutputToKafkaId: string; - before(() => { - loadKafkaOutput().then((data) => { - kafkaOutputToESId = data.item.id; - }); - loadKafkaOutput().then((data) => { - kafkaOutputToLogstashId = data.item.id; - }); - loadESOutput().then((data) => { - esOutputToKafkaId = data.item.id; - }); - loadLogstashOutput().then((data) => { - logstashOutputToKafkaId = data.item.id; - }); + before(() => { + loadKafkaOutput().then((data) => { + kafkaOutputToESId = data.item.id; }); - after(() => { - cleanupOutput(kafkaOutputToESId); - cleanupOutput(kafkaOutputToLogstashId); - cleanupOutput(logstashOutputToKafkaId); - cleanupOutput(esOutputToKafkaId); + loadKafkaOutput().then((data) => { + kafkaOutputToLogstashId = data.item.id; }); - it('changes output type from es to kafka', () => { - validateOutputTypeChangeToKafka(esOutputToKafkaId); + loadESOutput().then((data) => { + esOutputToKafkaId = data.item.id; }); - - it('changes output type from logstash to kafka', () => { - validateOutputTypeChangeToKafka(logstashOutputToKafkaId); + loadLogstashOutput().then((data) => { + logstashOutputToKafkaId = data.item.id; }); + }); + after(() => { + cleanupOutput(kafkaOutputToESId); + cleanupOutput(kafkaOutputToLogstashId); + cleanupOutput(logstashOutputToKafkaId); + cleanupOutput(esOutputToKafkaId); + }); + it('changes output type from es to kafka', () => { + validateOutputTypeChangeToKafka(esOutputToKafkaId); + }); - it('changes output type from kafka to es', () => { - visit(`/app/fleet/settings/outputs/${kafkaOutputToESId}`); - cy.getBySel(SETTINGS_OUTPUTS.TYPE_INPUT).select('elasticsearch'); - cy.getBySel(kafkaOutputFormValues.name.selector).clear().type('kafka_to_es'); + it('changes output type from logstash to kafka', () => { + validateOutputTypeChangeToKafka(logstashOutputToKafkaId); + }); - cy.intercept('PUT', '**/api/fleet/outputs/**').as('saveOutput'); + it('changes output type from kafka to es', () => { + visit(`/app/fleet/settings/outputs/${kafkaOutputToESId}`); + cy.getBySel(SETTINGS_OUTPUTS.TYPE_INPUT).select('elasticsearch'); + cy.getBySel(kafkaOutputFormValues.name.selector).clear().type('kafka_to_es'); + cy.get('[placeholder="Specify host URL"').clear().type('https://localhost:5000'); - cy.getBySel(SETTINGS_SAVE_BTN).click(); - cy.getBySel(SETTINGS_CONFIRM_MODAL_BTN).click(); + cy.intercept('PUT', '**/api/fleet/outputs/**').as('saveOutput'); - // wait for the save request to finish to avoid race condition - cy.wait('@saveOutput').then(() => { - visit(`/app/fleet/settings/outputs/${kafkaOutputToESId}`); - }); + cy.getBySel(SETTINGS_SAVE_BTN).click(); + cy.getBySel(SETTINGS_CONFIRM_MODAL_BTN).click(); - cy.getBySel(SETTINGS_OUTPUTS.TYPE_INPUT).should('have.value', 'elasticsearch'); - cy.getBySel(kafkaOutputFormValues.name.selector).should('have.value', 'kafka_to_es'); + // wait for the save request to finish to avoid race condition + cy.wait('@saveOutput').then(() => { + visit(`/app/fleet/settings/outputs/${kafkaOutputToESId}`); }); - it('changes output type from kafka to logstash', () => { - visit(`/app/fleet/settings/outputs/${kafkaOutputToLogstashId}`); - cy.getBySel(SETTINGS_OUTPUTS.TYPE_INPUT).select('logstash'); - cy.getBySel(kafkaOutputFormValues.name.selector).clear().type('kafka_to_logstash'); - cy.get('[placeholder="Specify host"').clear().type('localhost:5000'); - cy.get('[placeholder="Specify ssl certificate"]').clear().type('SSL CERTIFICATE'); - cy.get('[placeholder="Specify certificate key"]').clear().type('SSL KEY'); + cy.getBySel(SETTINGS_OUTPUTS.TYPE_INPUT).should('have.value', 'elasticsearch'); + cy.getBySel(kafkaOutputFormValues.name.selector).should('have.value', 'kafka_to_es'); + }); - cy.intercept('PUT', '**/api/fleet/outputs/**').as('saveOutput'); + it('changes output type from kafka to logstash', () => { + visit(`/app/fleet/settings/outputs/${kafkaOutputToLogstashId}`); + cy.getBySel(SETTINGS_OUTPUTS.TYPE_INPUT).select('logstash'); + cy.getBySel(kafkaOutputFormValues.name.selector).clear().type('kafka_to_logstash'); + cy.get('[placeholder="Specify host"').clear().type('localhost:5000'); + cy.get('[placeholder="Specify ssl certificate"]').clear().type('SSL CERTIFICATE'); + cy.get('[placeholder="Specify certificate key"]').clear().type('SSL KEY'); - cy.getBySel(SETTINGS_SAVE_BTN).click(); - cy.getBySel(SETTINGS_CONFIRM_MODAL_BTN).click(); + cy.intercept('PUT', '**/api/fleet/outputs/**').as('saveOutput'); - // wait for the save request to finish to avoid race condition - cy.wait('@saveOutput').then(() => { - visit(`/app/fleet/settings/outputs/${kafkaOutputToLogstashId}`); - }); + cy.getBySel(SETTINGS_SAVE_BTN).click(); + cy.getBySel(SETTINGS_CONFIRM_MODAL_BTN).click(); - cy.getBySel(SETTINGS_OUTPUTS.TYPE_INPUT).should('have.value', 'logstash'); - cy.getBySel(kafkaOutputFormValues.name.selector).should( - 'have.value', - 'kafka_to_logstash' - ); + // wait for the save request to finish to avoid race condition + cy.wait('@saveOutput').then(() => { + visit(`/app/fleet/settings/outputs/${kafkaOutputToLogstashId}`); }); + + cy.getBySel(SETTINGS_OUTPUTS.TYPE_INPUT).should('have.value', 'logstash'); + cy.getBySel(kafkaOutputFormValues.name.selector).should('have.value', 'kafka_to_logstash'); }); - } + }); }); }); diff --git a/x-pack/plugins/fleet/cypress/screens/fleet.ts b/x-pack/plugins/fleet/cypress/screens/fleet.ts index b21dfa7c4e66c..3b8ffcc63b6f6 100644 --- a/x-pack/plugins/fleet/cypress/screens/fleet.ts +++ b/x-pack/plugins/fleet/cypress/screens/fleet.ts @@ -121,6 +121,8 @@ export const SETTINGS_OUTPUTS = { NAME_INPUT: 'settingsOutputsFlyout.nameInput', TYPE_INPUT: 'settingsOutputsFlyout.typeInput', ADD_HOST_ROW_BTN: 'fleetServerHosts.multiRowInput.addRowButton', + WARNING_KAFKA_CALLOUT: 'settingsOutputsFlyout.kafkaOutputTypeCallout', + WARNING_ELASTICSEARCH_CALLOUT: 'settingsOutputsFlyout.elasticsearchOutputTypeCallout', }; export const getSpecificSelectorId = (selector: string, id: number) => { @@ -136,11 +138,16 @@ export const getSpecificSelectorId = (selector: string, id: number) => { export const SETTINGS_OUTPUTS_KAFKA = { VERSION_SELECT: 'settingsOutputsFlyout.kafkaVersionInput', AUTHENTICATION_SELECT: 'settingsOutputsFlyout.kafkaAuthenticationRadioInput', + AUTHENTICATION_NONE_OPTION: 'kafkaAuthenticationNoneRadioButton', AUTHENTICATION_USERNAME_PASSWORD_OPTION: 'kafkaAuthenticationUsernamePasswordRadioButton', AUTHENTICATION_SSL_OPTION: 'kafkaAuthenticationSSLRadioButton', AUTHENTICATION_KERBEROS_OPTION: 'kafkaAuthenticationKerberosRadioButton', AUTHENTICATION_USERNAME_INPUT: 'settingsOutputsFlyout.kafkaUsernameInput', AUTHENTICATION_PASSWORD_INPUT: 'settingsOutputsFlyout.kafkaPasswordInput', + AUTHENTICATION_VERIFICATION_MODE_INPUT: 'settingsOutputsFlyout.kafkaVerificationModeInput', + AUTHENTICATION_CONNECTION_TYPE_SELECT: 'settingsOutputsFlyout.kafkaConnectionTypeRadioInput', + AUTHENTICATION_CONNECTION_TYPE_PLAIN_OPTION: 'kafkaConnectionTypePlaintextRadioButton', + AUTHENTICATION_CONNECTION_TYPE_ENCRYPTION_OPTION: 'kafkaConnectionTypeEncryptionRadioButton', AUTHENTICATION_SASL_SELECT: 'settingsOutputsFlyout.kafkaSaslInput', AUTHENTICATION_SASL_PLAIN_OPTION: 'kafkaSaslPlainRadioButton', AUTHENTICATION_SASL_SCRAM_256_OPTION: 'kafkaSaslScramSha256RadioButton', @@ -173,7 +180,6 @@ export const SETTINGS_OUTPUTS_KAFKA = { BROKER_PANEL: 'settingsOutputsFlyout.kafkaBrokerSettingsPanel', BROKER_TIMEOUT_SELECT: 'settingsOutputsFlyout.kafkaBrokerTimeoutInput', BROKER_REACHABILITY_TIMEOUT_SELECT: 'settingsOutputsFlyout.kafkaBrokerReachabilityTimeoutInput', - BROKER_CHANNEL_BUFFER_SIZE_SELECT: 'settingsOutputsFlyout.kafkaBrokerChannelBufferSizeInput', BROKER_ACK_RELIABILITY_SELECT: 'settingsOutputsFlyout.kafkaBrokerAckReliabilityInputLabel', KEY_INPUT: 'settingsOutputsFlyout.kafkaKeyInput', }; diff --git a/x-pack/plugins/fleet/cypress/screens/fleet_outputs.ts b/x-pack/plugins/fleet/cypress/screens/fleet_outputs.ts index c5f7a852197c1..0e018cd301d1b 100644 --- a/x-pack/plugins/fleet/cypress/screens/fleet_outputs.ts +++ b/x-pack/plugins/fleet/cypress/screens/fleet_outputs.ts @@ -21,6 +21,8 @@ export const selectKafkaOutput = () => { visit('/app/fleet/settings'); cy.getBySel(SETTINGS_OUTPUTS.ADD_BTN).click(); cy.getBySel(SETTINGS_OUTPUTS.TYPE_INPUT).select('kafka'); + cy.getBySel(SETTINGS_OUTPUTS.WARNING_KAFKA_CALLOUT); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_USERNAME_PASSWORD_OPTION).click(); }; export const shouldDisplayError = (handler: string) => { @@ -56,7 +58,7 @@ export const kafkaOutputBody = { name: 'kafka_test1', type: 'kafka', is_default: false, - hosts: ['https://example.com'], + hosts: ['example.com:2000'], topics: [{ topic: 'test' }], auth_type: 'user_pass', username: 'kafka', @@ -96,6 +98,10 @@ export const kafkaOutputFormValues = { selector: SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_PASSWORD_INPUT, value: 'test_password', }, + verificationMode: { + selector: SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_VERIFICATION_MODE_INPUT, + value: 'certificate', + }, hash: { selector: SETTINGS_OUTPUTS_KAFKA.PARTITIONING_HASH_INPUT, value: 'testHash', @@ -110,7 +116,7 @@ export const kafkaOutputFormValues = { }, firstTopicCondition: { selector: SETTINGS_OUTPUTS_KAFKA.TOPICS_CONDITION_INPUT, - value: 'testCondition', + value: 'testCondition: abc', }, firstTopicWhen: { selector: SETTINGS_OUTPUTS_KAFKA.TOPICS_WHEN_INPUT, @@ -122,7 +128,7 @@ export const kafkaOutputFormValues = { }, secondTopicCondition: { selector: getSpecificSelectorId(SETTINGS_OUTPUTS_KAFKA.TOPICS_CONDITION_INPUT, 1), - value: 'testCondition1', + value: 'testCondition1: dca', }, secondTopicWhen: { selector: getSpecificSelectorId(SETTINGS_OUTPUTS_KAFKA.TOPICS_WHEN_INPUT, 1), @@ -154,11 +160,7 @@ export const kafkaOutputFormValues = { }, brokerAckReliability: { selector: SETTINGS_OUTPUTS_KAFKA.BROKER_ACK_RELIABILITY_SELECT, - value: 'Do not wait', - }, - brokerChannelBufferSize: { - selector: SETTINGS_OUTPUTS_KAFKA.BROKER_CHANNEL_BUFFER_SIZE_SELECT, - value: '512', + value: '0', }, brokerTimeout: { selector: SETTINGS_OUTPUTS_KAFKA.BROKER_TIMEOUT_SELECT, @@ -185,9 +187,13 @@ export const resetKafkaOutputForm = () => { export const fillInKafkaOutputForm = () => { cy.getBySel(kafkaOutputFormValues.name.selector).type(kafkaOutputFormValues.name.value); - cy.get('[placeholder="Specify host"').clear().type('http://localhost:5000'); + cy.get('[placeholder="Specify host"').clear().type('localhost:5000'); cy.getBySel(kafkaOutputFormValues.username.selector).type(kafkaOutputFormValues.username.value); cy.getBySel(kafkaOutputFormValues.password.selector).type(kafkaOutputFormValues.password.value); + cy.getBySel(kafkaOutputFormValues.verificationMode.selector).select( + kafkaOutputFormValues.verificationMode.value + ); + cy.get('[placeholder="Specify certificate authority"]').clear().type('testCA'); cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SASL_SCRAM_256_OPTION).click(); cy.getBySel(SETTINGS_OUTPUTS_KAFKA.PARTITIONING_HASH_OPTION).click(); @@ -249,9 +255,6 @@ export const fillInKafkaOutputForm = () => { cy.getBySel(kafkaOutputFormValues.brokerAckReliability.selector).select( kafkaOutputFormValues.brokerAckReliability.value ); - cy.getBySel(kafkaOutputFormValues.brokerChannelBufferSize.selector).select( - kafkaOutputFormValues.brokerChannelBufferSize.value - ); cy.getBySel(kafkaOutputFormValues.brokerTimeout.selector).select( kafkaOutputFormValues.brokerTimeout.value ); @@ -267,6 +270,9 @@ export const validateSavedKafkaOutputForm = () => { cy.getBySel(selector).should('have.value', value); }); + cy.get('[placeholder="Specify host"').should('have.value', 'localhost:5000'); + cy.get('[placeholder="Specify certificate authority"]').should('have.value', 'testCA'); + cy.getBySel(SETTINGS_OUTPUTS.TYPE_INPUT).should('have.value', 'kafka'); cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_SASL_SCRAM_256_OPTION) @@ -285,6 +291,8 @@ export const validateOutputTypeChangeToKafka = (outputId: string) => { visit(`/app/fleet/settings/outputs/${outputId}`); cy.getBySel(kafkaOutputFormValues.name.selector).clear(); cy.getBySel(SETTINGS_OUTPUTS.TYPE_INPUT).select('kafka'); + cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_USERNAME_PASSWORD_OPTION).click(); + fillInKafkaOutputForm(); cy.intercept('PUT', '**/api/fleet/outputs/**').as('saveOutput'); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_cloud_formation_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_cloud_formation_modal.tsx index e8300e35d0862..7ed959e40efa0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_cloud_formation_modal.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_cloud_formation_modal.tsx @@ -22,7 +22,7 @@ import { useQuery } from '@tanstack/react-query'; import type { AgentPolicy, PackagePolicy } from '../../../../../types'; import { sendGetEnrollmentAPIKeys, useCreateCloudFormationUrl } from '../../../../../hooks'; -import { getCloudFormationTemplateUrlFromPackagePolicy } from '../../../../../services'; +import { getCloudFormationPropsFromPackagePolicy } from '../../../../../services'; import { CloudFormationGuide } from '../../../../../components'; export const PostInstallCloudFormationModal: React.FunctionComponent<{ @@ -39,13 +39,11 @@ export const PostInstallCloudFormationModal: React.FunctionComponent<{ }) ); - const cloudFormationTemplateUrl = - getCloudFormationTemplateUrlFromPackagePolicy(packagePolicy) || ''; + const cloudFormationProps = getCloudFormationPropsFromPackagePolicy(packagePolicy); const { cloudFormationUrl, error, isError, isLoading } = useCreateCloudFormationUrl({ - cloudFormationTemplateUrl, enrollmentAPIKey: apyKeysData?.data?.items[0]?.api_key, - packagePolicy, + cloudFormationProps, }); return ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx index 52d02d93c8097..dbb901316cece 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx @@ -39,7 +39,7 @@ import type { PackagePolicyFormState } from '../../types'; import { SelectedPolicyTab } from '../../components'; import { useOnSaveNavigate } from '../../hooks'; import { prepareInputPackagePolicyDataset } from '../../services/prepare_input_pkg_policy_dataset'; -import { getCloudFormationTemplateUrlFromPackagePolicy } from '../../../../../services'; +import { getCloudFormationPropsFromPackagePolicy } from '../../../../../services'; async function createAgentPolicy({ packagePolicy, @@ -301,7 +301,7 @@ export function useOnSubmit({ }); const hasCloudFormation = data?.item - ? getCloudFormationTemplateUrlFromPackagePolicy(data.item) + ? getCloudFormationPropsFromPackagePolicy(data.item).templateUrl : false; if (hasCloudFormation) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx index d577a1643ac0c..46b367011395c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx @@ -43,9 +43,6 @@ const logstashInputsLabels = [ ]; const kafkaInputsLabels = [ - 'Username', - 'Password', - 'SASL Mechanism', 'Partitioning strategy', 'Number of events', 'Default topic', @@ -53,7 +50,6 @@ const kafkaInputsLabels = [ 'Value', 'Broker timeout', 'Broker reachability timeout', - 'Channel buffer size', 'ACK Reliability', 'Key (optional)', ]; @@ -143,7 +139,7 @@ describe('EditOutputFlyout', () => { }); // Does not show logstash inputs - logstashInputsLabels.forEach((label) => { + ['Client SSL certificate key', 'Client SSL certificate'].forEach((label) => { expect(utils.queryByLabelText(label)).toBeNull(); }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx index 3f2055e999914..e764e93527b34 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx @@ -73,7 +73,6 @@ export const EditOutputFlyout: React.FunctionComponent = [proxies] ); - const isESOutput = inputs.typeInput.value === outputType.Elasticsearch; const { kafkaOutput: isKafkaOutputEnabled } = ExperimentalFeaturesService.get(); const OUTPUT_TYPE_OPTIONS = [ @@ -249,6 +248,43 @@ export const EditOutputFlyout: React.FunctionComponent = } }; + const renderTypeSpecificWarning = () => { + const isESOutput = inputs.typeInput.value === outputType.Elasticsearch; + const isKafkaOutput = inputs.typeInput.value === outputType.Kafka; + if (!isKafkaOutput && !isESOutput) { + return null; + } + + const generateWarningMessage = () => { + switch (inputs.typeInput.value) { + case outputType.Kafka: + return i18n.translate('xpack.fleet.settings.editOutputFlyout.kafkaOutputTypeCallout', { + defaultMessage: + 'Kafka output is currently not supported on Agents using the Elastic Defend integration.', + }); + default: + case outputType.Elasticsearch: + return i18n.translate('xpack.fleet.settings.editOutputFlyout.esOutputTypeCallout', { + defaultMessage: + 'This output type currently does not support connectivity to a remote Elasticsearch cluster.', + }); + } + }; + return ( + <> + + + + ); + }; + return ( @@ -350,24 +386,7 @@ export const EditOutputFlyout: React.FunctionComponent = } )} /> - {isESOutput && ( - <> - - - - )} + {renderTypeSpecificWarning()} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka.tsx index 222def40b5a75..f6af3270487be 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka.tsx @@ -84,13 +84,13 @@ export const OutputFormKafkaSection: React.FunctionComponent = (props) => helpText={ ), @@ -105,6 +105,7 @@ export const OutputFormKafkaSection: React.FunctionComponent = (props) => + diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_authentication.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_authentication.tsx index 271f3fe144aca..42a7b66727597 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_authentication.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_authentication.tsx @@ -5,14 +5,16 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { + EuiFieldPassword, EuiFieldText, EuiFormRow, EuiPanel, EuiRadioGroup, + EuiSelect, EuiSpacer, EuiTextArea, EuiTitle, @@ -20,7 +22,13 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { MultiRowInput } from '../multi_row_input'; -import { kafkaAuthType, kafkaSaslMechanism } from '../../../../../../../common/constants'; + +import { + kafkaAuthType, + kafkaConnectionType, + kafkaSaslMechanism, + kafkaVerificationModes, +} from '../../../../../../../common/constants'; import type { OutputFormInputsType } from './use_output_form'; @@ -43,6 +51,11 @@ const kafkaSaslOptions = [ ]; const kafkaAuthenticationsOptions = [ + { + id: kafkaAuthType.None, + label: 'None', + 'data-test-subj': 'kafkaAuthenticationNoneRadioButton', + }, { id: kafkaAuthType.Userpass, label: 'Username / Password', @@ -53,11 +66,6 @@ const kafkaAuthenticationsOptions = [ label: 'SSL', 'data-test-subj': 'kafkaAuthenticationSSLRadioButton', }, - { - id: kafkaAuthType.Kerberos, - label: 'Kerberos', - 'data-test-subj': 'kafkaAuthenticationKerberosRadioButton', - }, ]; export const OutputFormKafkaAuthentication: React.FunctionComponent<{ @@ -65,28 +73,56 @@ export const OutputFormKafkaAuthentication: React.FunctionComponent<{ }> = (props) => { const { inputs } = props; + const kafkaVerificationModeOptions = useMemo( + () => + (Object.keys(kafkaVerificationModes) as Array).map( + (key) => { + return { + text: kafkaVerificationModes[key], + label: key, + }; + } + ), + [] + ); + + const kafkaConnectionTypeOptions = useMemo( + () => + (Object.keys(kafkaConnectionType) as Array).map((key) => { + return { + id: kafkaConnectionType[key], + label: key, + 'data-test-subj': `kafkaConnectionType${key}RadioButton`, + }; + }), + [] + ); + const renderAuthentication = () => { switch (inputs.kafkaAuthMethodInput.value) { + case kafkaAuthType.None: + return ( + + } + > + + + ); case kafkaAuthType.Ssl: return ( <> - ); - case kafkaAuthType.Kerberos: - return null; default: case kafkaAuthType.Userpass: return ( @@ -165,7 +199,8 @@ export const OutputFormKafkaAuthentication: React.FunctionComponent<{ } {...inputs.kafkaAuthPasswordInput.formRowProps} > - { + const displayEncryptionSection = + inputs.kafkaConnectionTypeInput.value !== kafkaConnectionType.Plaintext || + inputs.kafkaAuthMethodInput.value !== kafkaAuthType.None; + + if (!displayEncryptionSection) { + return null; + } + + return ( + <> + + + + + + } + > + + + + ); + }; + return ( - - -

- + + +

+ +

+
+ + + -

-
- - - - - {renderAuthentication()} -
+
+ {renderAuthentication()} + + {renderEncryptionSection()} + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_broker.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_broker.tsx index f6b0477c81515..2fb40f3e5e376 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_broker.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_broker.tsx @@ -27,14 +27,18 @@ export const OutputFormKafkaBroker: React.FunctionComponent<{ inputs: OutputForm [] ); - const kafkaBrokerChannelBufferSizeOptions = useMemo( - () => - Array.from({ length: 4 }, (_, i) => Math.pow(2, i + 7)).map((buffer) => ({ - text: buffer, - label: `${buffer}`, - })), - [] - ); + const getAckReliabilityLabel = (value: number) => { + switch (value) { + case kafkaAcknowledgeReliabilityLevel.DoNotWait: + return 'No response'; + case kafkaAcknowledgeReliabilityLevel.Replica: + return 'Wait for all replicas to commit'; + default: + case kafkaAcknowledgeReliabilityLevel.Commit: + return 'Wait for local commit'; + } + }; + const kafkaBrokerAckReliabilityOptions = useMemo( () => ( @@ -44,7 +48,7 @@ export const OutputFormKafkaBroker: React.FunctionComponent<{ inputs: OutputForm ).map((key) => { return { text: kafkaAcknowledgeReliabilityLevel[key], - label: kafkaAcknowledgeReliabilityLevel[key], + label: getAckReliabilityLabel(kafkaAcknowledgeReliabilityLevel[key]), }; }), [] @@ -111,28 +115,6 @@ export const OutputFormKafkaBroker: React.FunctionComponent<{ inputs: OutputForm options={kafkaBrokerTimeoutOptions} /> - - } - helpText={ - - } - > - - } > diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_headers.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_headers.tsx index 3f605463a65bc..89ff4b5d0de7a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_headers.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_headers.tsx @@ -114,7 +114,7 @@ export const OutputFormKafkaHeaders: React.FunctionComponent<{ inputs: OutputFor const keyErrors = matchErrorsByIndex(index, 'key'); const valueErrors = matchErrorsByIndex(index, 'value'); return ( - <> +
{index > 0 && } @@ -173,7 +173,7 @@ export const OutputFormKafkaHeaders: React.FunctionComponent<{ inputs: OutputFor /> - +
); })} {displayErrors(globalErrors)} @@ -198,6 +198,7 @@ export const OutputFormKafkaHeaders: React.FunctionComponent<{ inputs: OutputFor defaultMessage="Client ID" /> } + {...inputs.kafkaClientIdInput.formRowProps} > { @@ -64,7 +63,30 @@ export const OutputFormKafkaTopics: React.FunctionComponent<{ inputs: OutputForm acc[err.index] = []; } - acc[err.index].push(err.message); + if (!err.condition) { + acc[err.index].push(err.message); + } + + return acc; + }, []); + }, [errors]); + + const indexedConditionErrors = useMemo(() => { + if (!errors) { + return []; + } + return errors.reduce((acc, err) => { + if (err.index === undefined) { + return acc; + } + + if (!acc[err.index]) { + acc[err.index] = []; + } + + if (err.condition) { + acc[err.index].push(err.message); + } return acc; }, []); @@ -97,9 +119,10 @@ export const OutputFormKafkaTopics: React.FunctionComponent<{ inputs: OutputForm (index: number) => { const updatedTopics = topics.filter((_, i) => i !== index); indexedErrors.splice(index, 1); + indexedConditionErrors.splice(index, 1); onChange(updatedTopics); }, - [topics, indexedErrors, onChange] + [topics, indexedErrors, indexedConditionErrors, onChange] ); const displayErrors = (errorMessages?: string[]) => { @@ -132,10 +155,13 @@ export const OutputFormKafkaTopics: React.FunctionComponent<{ inputs: OutputForm const sourceErrors = indexedErrors[source.index]; indexedErrors.splice(source.index, 1); indexedErrors.splice(destination.index, 0, sourceErrors); + const sourceConditionErrors = indexedConditionErrors[source.index]; + indexedConditionErrors.splice(source.index, 1); + indexedConditionErrors.splice(destination.index, 0, sourceConditionErrors); onChange(items); } }, - [topics, indexedErrors, onChange] + [topics, indexedErrors, indexedConditionErrors, onChange] ); return ( @@ -187,6 +213,7 @@ export const OutputFormKafkaTopics: React.FunctionComponent<{ inputs: OutputForm {topics.map((topic, index) => { const topicErrors = indexedErrors[index]; + const topicConditionErrors = indexedConditionErrors[index]; return ( - + 0} + > 0} onChange={(e) => handleTopicProcessorChange(index, 'condition', e.target.value) } @@ -301,6 +333,7 @@ export const OutputFormKafkaTopics: React.FunctionComponent<{ inputs: OutputForm <> {topics.map((topic, index) => { const topicErrors = indexedErrors[index]; + const topicConditionErrors = indexedConditionErrors[index]; return ( <> @@ -320,10 +353,15 @@ export const OutputFormKafkaTopics: React.FunctionComponent<{ inputs: OutputForm - + 0} + > 0} onChange={(e) => handleTopicProcessorChange(index, 'condition', e.target.value) } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.test.tsx index 04b07ac9c8b4a..d5da6f553cf89 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.test.tsx @@ -11,9 +11,71 @@ import { validateYamlConfig, validateCATrustedFingerPrint, validateKafkaHeaders, + validateKafkaHosts, } from './output_form_validators'; describe('Output form validation', () => { + describe('validateKafkaHosts', () => { + it('should not work without any urls', () => { + const res = validateKafkaHosts([]); + + expect(res).toEqual([{ message: 'Host is required' }]); + }); + + it('should work with valid url', () => { + const res = validateKafkaHosts(['test.fr:9200']); + + expect(res).toBeUndefined(); + }); + + it('should work with multiple valid urls', () => { + const res = validateKafkaHosts(['test.fr:9200', 'test2.fr:9200', 'test.fr:9999']); + + expect(res).toBeUndefined(); + }); + + it('should return an error with invalid url', () => { + const res = validateKafkaHosts(['toto']); + + expect(res).toEqual([ + { index: 0, message: 'Invalid format. Expected "host:port" without protocol.' }, + ]); + }); + + it('should return an error with url with defined protocol', () => { + const res = validateKafkaHosts(['https://test.fr:9200']); + + expect(res).toEqual([ + { index: 0, message: 'Invalid format. Expected "host:port" without protocol.' }, + ]); + }); + + it('should return an error with url with invalid port', () => { + const res = validateKafkaHosts(['test.fr:qwerty9200']); + + expect(res).toEqual([ + { index: 0, message: 'Invalid port number. Expected a number between 1 and 65535' }, + ]); + }); + + it('should return an error with multiple invalid urls', () => { + const res = validateKafkaHosts(['toto', 'tata']); + + expect(res).toEqual([ + { index: 0, message: 'Invalid format. Expected "host:port" without protocol.' }, + { index: 1, message: 'Invalid format. Expected "host:port" without protocol.' }, + ]); + }); + it('should return an error with duplicate urls', () => { + const res = validateKafkaHosts(['test.fr:2000', 'test.fr:2000']); + + expect(res).toEqual([ + { index: 0, message: 'Duplicate URL' }, + { index: 1, message: 'Duplicate URL' }, + ]); + }); + }); + describe('validateESHosts', () => { it('should not work without any urls', () => { const res = validateESHosts([]); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx index 8d658e545a854..bbb5b5dda3e91 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx @@ -8,6 +8,73 @@ import { i18n } from '@kbn/i18n'; import { safeLoad } from 'js-yaml'; +export function validateKafkaHosts(value: string[]) { + const res: Array<{ message: string; index?: number }> = []; + const urlIndexes: { [key: string]: number[] } = {}; + + value.forEach((val, idx) => { + if (!val) { + res.push({ + message: i18n.translate('xpack.fleet.settings.outputForm.kafkaHostFieldRequiredError', { + defaultMessage: 'Host is required', + }), + }); + return; + } + + // Split the URL into parts based on ":" + const urlParts = val.split(':'); + if (urlParts.length !== 2 || !urlParts[0] || !urlParts[1]) { + res.push({ + message: i18n.translate('xpack.fleet.settings.outputForm.kafkaHostPortError', { + defaultMessage: 'Invalid format. Expected "host:port" without protocol.', + }), + index: idx, + }); + return; + } + + // Validate that the port is a valid number + const port = parseInt(urlParts[1], 10); + if (isNaN(port) || port < 1 || port > 65535) { + res.push({ + message: i18n.translate('xpack.fleet.settings.outputForm.kafkaPortError', { + defaultMessage: 'Invalid port number. Expected a number between 1 and 65535', + }), + index: idx, + }); + } + + const curIndexes = urlIndexes[val] || []; + urlIndexes[val] = [...curIndexes, idx]; + }); + + Object.values(urlIndexes) + .filter(({ length }) => length > 1) + .forEach((indexes) => { + indexes.forEach((index) => + res.push({ + message: i18n.translate('xpack.fleet.settings.outputForm.kafkaHostDuplicateError', { + defaultMessage: 'Duplicate URL', + }), + index, + }) + ); + }); + + if (value.length === 0) { + res.push({ + message: i18n.translate('xpack.fleet.settings.outputForm.kafkaHostRequiredError', { + defaultMessage: 'Host is required', + }), + }); + } + + if (res.length) { + return res; + } +} + export function validateESHosts(value: string[]) { const res: Array<{ message: string; index?: number }> = []; const urlIndexes: { [key: string]: number[] } = {}; @@ -211,14 +278,31 @@ export function validateKafkaDefaultTopic(value: string) { } } +export function validateKafkaClientId(value: string) { + const regex = /^[A-Za-z0-9._-]+$/; + return regex.test(value) + ? undefined + : [ + i18n.translate('xpack.fleet.settings.outputForm.kafkaClientIdFormattingMessage', { + defaultMessage: + 'Client ID is invalid. Only letters, numbers, dots, underscores, and dashes are allowed.', + }), + ]; +} + export function validateKafkaTopics( topics: Array<{ topic: string; + when?: { + condition?: string; + type?: string; + }; }> ) { const errors: Array<{ message: string; index: number; + condition?: boolean; }> = []; topics.forEach((topic, index) => { @@ -230,6 +314,19 @@ export function validateKafkaTopics( index, }); } + if ( + !topic.when?.condition || + topic.when.condition === '' || + topic.when.condition.split(':').length - 1 !== 1 + ) { + errors.push({ + message: i18n.translate('xpack.fleet.settings.outputForm.kafkaTopicConditionRequired', { + defaultMessage: 'Must be a key, value pair i.e. "http.response.code: 200"', + }), + index, + condition: true, + }); + } }); if (errors.length) { return errors; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx index dbd1b4b0c7a30..bd1891f3ebc12 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx @@ -21,8 +21,10 @@ import { kafkaAcknowledgeReliabilityLevel, kafkaAuthType, kafkaCompressionType, + kafkaConnectionType, kafkaPartitionType, kafkaSaslMechanism, + kafkaVerificationModes, outputType, } from '../../../../../../../common/constants'; @@ -57,6 +59,8 @@ import { validateKafkaHeaders, validateKafkaDefaultTopic, validateKafkaTopics, + validateKafkaClientId, + validateKafkaHosts, } from './output_form_validators'; import { confirmUpdate } from './confirm_update'; @@ -87,7 +91,9 @@ export interface OutputFormInputsType { maxBatchBytes: ReturnType; kafkaHostsInput: ReturnType; kafkaVersionInput: ReturnType; + kafkaVerificationModeInput: ReturnType; kafkaAuthMethodInput: ReturnType; + kafkaConnectionTypeInput: ReturnType; kafkaSaslMechanismInput: ReturnType; kafkaAuthUsernameInput: ReturnType; kafkaAuthPasswordInput: ReturnType; @@ -104,7 +110,6 @@ export interface OutputFormInputsType { kafkaCompressionCodecInput: ReturnType; kafkaBrokerTimeoutInput: ReturnType; kafkaBrokerReachabilityTimeoutInput: ReturnType; - kafkaBrokerChannelBufferSizeInput: ReturnType; kafkaBrokerAckReliabilityInput: ReturnType; kafkaKeyInput: ReturnType; kafkaSslCertificateInput: ReturnType; @@ -278,15 +283,20 @@ export function useOutputForm(onSucess: () => void, output?: Output) { const kafkaHostsInput = useComboInput( 'kafkaHostsComboBox', output?.hosts ?? [], - validateESHosts, + validateKafkaHosts, isDisabled('hosts') ); const kafkaAuthMethodInput = useRadioInput( - kafkaOutput?.auth_type ?? kafkaAuthType.Userpass, + kafkaOutput?.auth_type ?? kafkaAuthType.None, isDisabled('auth_type') ); + const kafkaConnectionTypeInput = useRadioInput( + kafkaOutput?.connection_type ?? kafkaConnectionType.Plaintext, + isDisabled('connection_type') + ); + const kafkaAuthUsernameInput = useInput( kafkaOutput?.username, kafkaAuthMethodInput.value === kafkaAuthType.Userpass ? validateKafkaUsername : undefined, @@ -315,6 +325,12 @@ export function useOutputForm(onSucess: () => void, output?: Output) { isSSLEditable ); + const kafkaVerificationModeInput = useInput( + kafkaOutput?.ssl?.verification_mode ?? kafkaVerificationModes.Full, + undefined, + isSSLEditable + ); + const kafkaSaslMechanismInput = useRadioInput( kafkaOutput?.sasl?.mechanism ?? kafkaSaslMechanism.Plain, isDisabled('sasl') @@ -360,8 +376,8 @@ export function useOutputForm(onSucess: () => void, output?: Output) { ); const kafkaClientIdInput = useInput( - kafkaOutput?.client_id ?? 'Elastic agent', - undefined, + kafkaOutput?.client_id ?? 'Elastic', + validateKafkaClientId, isDisabled('client_id') ); @@ -392,16 +408,10 @@ export function useOutputForm(onSucess: () => void, output?: Output) { isDisabled('timeout') ); - const kafkaBrokerChannelBufferSizeInput = useInput( - `${kafkaOutput?.broker_buffer_size ?? 256}`, - undefined, - isDisabled('broker_buffer_size') - ); - const kafkaBrokerAckReliabilityInput = useInput( - kafkaOutput?.broker_ack_reliability ?? kafkaAcknowledgeReliabilityLevel.Commit, + `${kafkaOutput?.required_acks ?? kafkaAcknowledgeReliabilityLevel.Commit}`, undefined, - isDisabled('broker_ack_reliability') + isDisabled('required_acks') ); const kafkaKeyInput = useInput(kafkaOutput?.key, undefined, isDisabled('key')); @@ -434,7 +444,9 @@ export function useOutputForm(onSucess: () => void, output?: Output) { maxBatchBytes, kafkaVersionInput, kafkaHostsInput, + kafkaVerificationModeInput, kafkaAuthMethodInput, + kafkaConnectionTypeInput, kafkaAuthUsernameInput, kafkaAuthPasswordInput, kafkaSaslMechanismInput, @@ -449,7 +461,6 @@ export function useOutputForm(onSucess: () => void, output?: Output) { kafkaCompressionCodecInput, kafkaBrokerTimeoutInput, kafkaBrokerReachabilityTimeoutInput, - kafkaBrokerChannelBufferSizeInput, kafkaBrokerAckReliabilityInput, kafkaKeyInput, kafkaSslCertificateAuthoritiesInput, @@ -467,6 +478,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) { const kafkaHostsValid = kafkaHostsInput.validate(); const kafkaUsernameValid = kafkaAuthUsernameInput.validate(); const kafkaPasswordValid = kafkaAuthPasswordInput.validate(); + const kafkaClientIDValid = kafkaClientIdInput.validate(); const kafkaSslCertificateValid = kafkaSslCertificateInput.validate(); const kafkaSslKeyValid = kafkaSslKeyInput.validate(); const kafkaDefaultTopicValid = kafkaDefaultTopicInput.validate(); @@ -501,7 +513,8 @@ export function useOutputForm(onSucess: () => void, output?: Output) { kafkaHeadersValid && kafkaDefaultTopicValid && kafkaTopicsValid && - additionalYamlConfigValid + additionalYamlConfigValid && + kafkaClientIDValid ); } else { // validate ES @@ -519,6 +532,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) { kafkaHostsInput, kafkaAuthUsernameInput, kafkaAuthPasswordInput, + kafkaClientIdInput, kafkaSslCertificateInput, kafkaSslKeyInput, kafkaDefaultTopicInput, @@ -582,7 +596,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) { const payload: NewOutput = (() => { const parseIntegerIfStringDefined = (value: string | undefined): number | undefined => { if (value !== undefined) { - const parsedInt = parseInt(value, 10); // Specify the base as 10 for decimal numbers + const parsedInt = parseInt(value, 10); if (!isNaN(parsedInt)) { return parsedInt; } @@ -592,6 +606,10 @@ export function useOutputForm(onSucess: () => void, output?: Output) { switch (typeInput.value) { case outputType.Kafka: + const definedCA = kafkaSslCertificateAuthoritiesInput.value.filter( + (val) => val !== '' + ).length; + return { name: nameInput.value, type: outputType.Kafka, @@ -599,18 +617,26 @@ export function useOutputForm(onSucess: () => void, output?: Output) { is_default: defaultOutputInput.value, is_default_monitoring: defaultMonitoringOutputInput.value, config_yaml: additionalYamlConfigInput.value, - ...(kafkaAuthMethodInput.value === kafkaAuthType.Ssl + ...(kafkaConnectionTypeInput.value !== kafkaConnectionType.Plaintext || + kafkaAuthMethodInput.value !== kafkaAuthType.None ? { ssl: { - certificate: kafkaSslCertificateInput.value, - key: kafkaSslKeyInput.value, - certificate_authorities: kafkaSslCertificateAuthoritiesInput.value.filter( - (val) => val !== '' - ), + ...(definedCA + ? { + certificate_authorities: + kafkaSslCertificateAuthoritiesInput.value.filter((val) => val !== ''), + } + : {}), + ...(kafkaAuthMethodInput.value === kafkaAuthType.Ssl + ? { + certificate: kafkaSslCertificateInput.value, + key: kafkaSslKeyInput.value, + } + : {}), + verification_mode: kafkaVerificationModeInput.value, }, } : {}), - proxy_id: proxyIdValue, client_id: kafkaClientIdInput.value || undefined, @@ -626,8 +652,17 @@ export function useOutputForm(onSucess: () => void, output?: Output) { : {}), auth_type: kafkaAuthMethodInput.value, - ...(kafkaAuthUsernameInput.value ? { username: kafkaAuthUsernameInput.value } : {}), - ...(kafkaAuthPasswordInput.value ? { password: kafkaAuthPasswordInput.value } : {}), + ...(kafkaAuthMethodInput.value === kafkaAuthType.None + ? { connection_type: kafkaConnectionTypeInput.value } + : {}), + ...(kafkaAuthMethodInput.value === kafkaAuthType.Userpass && + kafkaAuthUsernameInput.value + ? { username: kafkaAuthUsernameInput.value } + : {}), + ...(kafkaAuthMethodInput.value === kafkaAuthType.Userpass && + kafkaAuthPasswordInput.value + ? { password: kafkaAuthPasswordInput.value } + : {}), ...(kafkaAuthMethodInput.value === kafkaAuthType.Userpass && kafkaSaslMechanismInput.value ? { sasl: { mechanism: kafkaSaslMechanismInput.value } } @@ -665,10 +700,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) { broker_timeout: parseIntegerIfStringDefined( kafkaBrokerReachabilityTimeoutInput.value ), - broker_ack_reliability: kafkaBrokerAckReliabilityInput.value, - broker_buffer_size: parseIntegerIfStringDefined( - kafkaBrokerChannelBufferSizeInput.value - ), + required_acks: parseIntegerIfStringDefined(kafkaBrokerAckReliabilityInput.value), ...shipperParams, } as KafkaOutput; case outputType.Logstash: @@ -752,6 +784,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) { compressionLevelInput.value, loadBalanceEnabledInput.value, typeInput.value, + kafkaSslCertificateAuthoritiesInput.value, nameInput.value, kafkaHostsInput.value, defaultOutputInput.value, @@ -760,12 +793,13 @@ export function useOutputForm(onSucess: () => void, output?: Output) { kafkaAuthMethodInput.value, kafkaSslCertificateInput.value, kafkaSslKeyInput.value, - kafkaSslCertificateAuthoritiesInput.value, + kafkaVerificationModeInput.value, kafkaClientIdInput.value, kafkaVersionInput.value, kafkaKeyInput.value, kafkaCompressionCodecInput.value, kafkaCompressionLevelInput.value, + kafkaConnectionTypeInput.value, kafkaAuthUsernameInput.value, kafkaAuthPasswordInput.value, kafkaSaslMechanismInput.value, @@ -779,7 +813,6 @@ export function useOutputForm(onSucess: () => void, output?: Output) { kafkaBrokerTimeoutInput.value, kafkaBrokerReachabilityTimeoutInput.value, kafkaBrokerAckReliabilityInput.value, - kafkaBrokerChannelBufferSizeInput.value, logstashHostsInput.value, sslCertificateInput.value, sslKeyInput.value, diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx index 0edbe5316409b..83031548293f7 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx @@ -10,26 +10,23 @@ import { EuiButton, EuiSpacer, EuiCallOut, EuiSkeletonText } from '@elastic/eui' import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import type { PackagePolicy } from '../../../common'; - import { useCreateCloudFormationUrl } from '../../hooks'; import { CloudFormationGuide } from '../cloud_formation_guide'; +import type { CloudSecurityIntegration } from './types'; + interface Props { enrollmentAPIKey?: string; - cloudFormationTemplateUrl: string; - packagePolicy?: PackagePolicy; + cloudSecurityIntegration: CloudSecurityIntegration; } export const CloudFormationInstructions: React.FunctionComponent = ({ enrollmentAPIKey, - cloudFormationTemplateUrl, - packagePolicy, + cloudSecurityIntegration, }) => { const { isLoading, cloudFormationUrl, error, isError } = useCreateCloudFormationUrl({ enrollmentAPIKey, - cloudFormationTemplateUrl, - packagePolicy, + cloudFormationProps: cloudSecurityIntegration?.cloudFormationProps, }); if (error && isError) { @@ -45,7 +42,7 @@ export const CloudFormationInstructions: React.FunctionComponent = ({ { - if (!agentPolicy) { + const cloudSecurityPackagePolicy = useMemo(() => { + return getCloudSecurityPackagePolicyFromAgentPolicy(agentPolicy); + }, [agentPolicy]); + + const integrationVersion = cloudSecurityPackagePolicy?.package?.version; + + // Fetch the package info to get the CloudFormation template URL only + // if the package policy is a Cloud Security policy + const { data: packageInfoData, isLoading } = useGetPackageInfoByKeyQuery( + FLEET_CLOUD_SECURITY_POSTURE_PACKAGE, + integrationVersion, + { full: true }, + { enabled: Boolean(cloudSecurityPackagePolicy) } + ); + + const cloudSecurityIntegration: CloudSecurityIntegration | undefined = useMemo(() => { + if (!agentPolicy || !cloudSecurityPackagePolicy) { return undefined; } - const integrationType = getCloudSecurityIntegrationTypeFromPackagePolicy(agentPolicy); - const cloudformationUrl = getCloudFormationTemplateUrlFromAgentPolicy(agentPolicy); + const integrationType = cloudSecurityPackagePolicy.inputs?.find((input) => input.enabled) + ?.policy_template as CloudSecurityIntegrationType; + + if (!integrationType) return undefined; + + const cloudFormationTemplateFromAgentPolicy = + getCloudFormationTemplateUrlFromAgentPolicy(agentPolicy); + + // Use the latest CloudFormation template for the current version + // So it guarantee that the template version matches the integration version + // when the integration is upgraded. + // In case it can't find the template for the current version, + // it will fallback to the one from the agent policy. + const cloudFormationTemplateUrl = packageInfoData?.item + ? getCloudFormationTemplateUrlFromPackageInfo(packageInfoData.item, integrationType) + : cloudFormationTemplateFromAgentPolicy; + + const AWS_ACCOUNT_TYPE = 'aws.account_type'; + + const cloudFormationAwsAccountType: CloudSecurityIntegrationAwsAccountType | undefined = + cloudSecurityPackagePolicy?.inputs?.find((input) => input.enabled)?.streams?.[0]?.vars?.[ + AWS_ACCOUNT_TYPE + ]?.value; return { + isLoading, integrationType, - cloudformationUrl, + isCloudFormation: Boolean(cloudFormationTemplateFromAgentPolicy), + cloudFormationProps: { + awsAccountType: cloudFormationAwsAccountType, + templateUrl: cloudFormationTemplateUrl, + }, }; - }, [agentPolicy]); + }, [agentPolicy, packageInfoData?.item, isLoading, cloudSecurityPackagePolicy]); return { cloudSecurityIntegration }; } @@ -97,13 +147,10 @@ const isK8sPackage = (pkg: PackagePolicy) => { return K8S_PACKAGES.has(name); }; -const getCloudSecurityIntegrationTypeFromPackagePolicy = ( - agentPolicy: AgentPolicy -): CloudSecurityIntegrationType | undefined => { - const packagePolicy = agentPolicy?.package_policies?.find( +const getCloudSecurityPackagePolicyFromAgentPolicy = ( + agentPolicy?: AgentPolicy +): PackagePolicy | undefined => { + return agentPolicy?.package_policies?.find( (input) => input.package?.name === FLEET_CLOUD_SECURITY_POSTURE_PACKAGE ); - if (!packagePolicy) return undefined; - return packagePolicy?.inputs?.find((input) => input.enabled) - ?.policy_template as CloudSecurityIntegrationType; }; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/instructions.tsx index b602b9fd8931d..d88c9cedb4bcd 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/instructions.tsx @@ -80,8 +80,8 @@ export const Instructions = (props: InstructionProps) => { (fleetStatus.missingRequirements ?? []).some((r) => r === FLEET_SERVER_PACKAGE)); useEffect(() => { - // If we have a cloudFormationTemplateUrl, we want to hide the selection type - if (props.cloudSecurityIntegration?.cloudformationUrl) { + // If we detect a CloudFormation integration, we want to hide the selection type + if (props.cloudSecurityIntegration?.isCloudFormation) { setSelectionType(undefined); } else if (!isIntegrationFlow && showAgentEnrollment) { setSelectionType('radio'); @@ -117,10 +117,7 @@ export const Instructions = (props: InstructionProps) => { {isFleetServerPolicySelected ? ( undefined} /> ) : ( - + )} ); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/compute_steps.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/compute_steps.tsx index 3977cdd5db576..a750fa48aae7f 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/compute_steps.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/compute_steps.tsx @@ -200,7 +200,6 @@ export const ManagedSteps: React.FunctionComponent = ({ isK8s, cloudSecurityIntegration, installedPackagePolicy, - cloudFormationTemplateUrl, }) => { const kibanaVersion = useKibanaVersion(); const core = useStartServices(); @@ -247,14 +246,13 @@ export const ManagedSteps: React.FunctionComponent = ({ ); } - if (cloudFormationTemplateUrl) { + if (cloudSecurityIntegration?.isCloudFormation) { steps.push( InstallCloudFormationManagedAgentStep({ apiKeyData, selectedApiKeyId, enrollToken, - cloudFormationTemplateUrl, - agentPolicy, + cloudSecurityIntegration, }) ); } else { @@ -314,7 +312,6 @@ export const ManagedSteps: React.FunctionComponent = ({ link, agentDataConfirmed, installedPackagePolicy, - cloudFormationTemplateUrl, ]); return ; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_cloud_formation_managed_agent_step.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_cloud_formation_managed_agent_step.tsx index 75fec5be125f5..7826d1648ae64 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_cloud_formation_managed_agent_step.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_cloud_formation_managed_agent_step.tsx @@ -11,46 +11,38 @@ import { i18n } from '@kbn/i18n'; import type { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps'; -import type { AgentPolicy } from '../../../../common'; - import type { GetOneEnrollmentAPIKeyResponse } from '../../../../common/types/rest_spec/enrollment_api_key'; import { CloudFormationInstructions } from '../cloud_formation_instructions'; -import { FLEET_CLOUD_SECURITY_POSTURE_PACKAGE } from '../../../../common'; + +import type { CloudSecurityIntegration } from '../types'; export const InstallCloudFormationManagedAgentStep = ({ selectedApiKeyId, apiKeyData, enrollToken, isComplete, - cloudFormationTemplateUrl, - agentPolicy, + cloudSecurityIntegration, }: { selectedApiKeyId?: string; apiKeyData?: GetOneEnrollmentAPIKeyResponse | null; enrollToken?: string; isComplete?: boolean; - cloudFormationTemplateUrl: string; - agentPolicy?: AgentPolicy; + cloudSecurityIntegration?: CloudSecurityIntegration | undefined; }): EuiContainedStepProps => { const nonCompleteStatus = selectedApiKeyId ? undefined : 'disabled'; const status = isComplete ? 'complete' : nonCompleteStatus; - const cloudSecurityPackagePolicy = agentPolicy?.package_policies?.find( - (p) => p.package?.name === FLEET_CLOUD_SECURITY_POSTURE_PACKAGE - ); - return { status, title: i18n.translate('xpack.fleet.agentEnrollment.cloudFormation.stepEnrollAndRunAgentTitle', { defaultMessage: 'Install Elastic Agent on your cloud', }), children: - selectedApiKeyId && apiKeyData ? ( + selectedApiKeyId && apiKeyData && cloudSecurityIntegration ? ( ) : ( diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts index 79c8029e4ec01..f1cd44ea5348b 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts @@ -16,13 +16,21 @@ export type K8sMode = | 'IS_KUBERNETES_MULTIPAGE'; export type CloudSecurityIntegrationType = 'kspm' | 'vuln_mgmt' | 'cspm'; +export type CloudSecurityIntegrationAwsAccountType = 'single-account' | 'organization-account'; export type FlyoutMode = 'managed' | 'standalone'; export type SelectionType = 'tabs' | 'radio' | undefined; +export interface CloudFormationProps { + templateUrl: string | undefined; + awsAccountType: CloudSecurityIntegrationAwsAccountType | undefined; +} + export interface CloudSecurityIntegration { integrationType: CloudSecurityIntegrationType | undefined; - cloudformationUrl: string | undefined; + isLoading: boolean; + isCloudFormation: boolean; + cloudFormationProps?: CloudFormationProps; } export interface BaseProps { @@ -65,5 +73,4 @@ export interface InstructionProps extends BaseProps { setSelectedAPIKeyId: (key?: string) => void; fleetServerHosts: string[]; fleetProxy?: FleetProxy; - cloudFormationTemplateUrl?: string; } diff --git a/x-pack/plugins/fleet/public/hooks/use_create_cloud_formation_url.ts b/x-pack/plugins/fleet/public/hooks/use_create_cloud_formation_url.ts index a28b0f46adb41..45e5b259afd15 100644 --- a/x-pack/plugins/fleet/public/hooks/use_create_cloud_formation_url.ts +++ b/x-pack/plugins/fleet/public/hooks/use_create_cloud_formation_url.ts @@ -7,34 +7,27 @@ import { i18n } from '@kbn/i18n'; -import type { PackagePolicy, PackagePolicyInput } from '../../common'; +import type { + CloudFormationProps, + CloudSecurityIntegrationAwsAccountType, +} from '../components/agent_enrollment_flyout/types'; import { useKibanaVersion } from './use_kibana_version'; import { useGetSettings } from './use_request'; -type AwsAccountType = 'single_account' | 'organization_account'; - -const CLOUDBEAT_AWS = 'cloudbeat/cis_aws'; - -const getAwsAccountType = (input?: PackagePolicyInput): AwsAccountType | undefined => - input?.streams[0].vars?.['aws.account_type']?.value; +const CLOUD_FORMATION_DEFAULT_ACCOUNT_TYPE = 'single-account'; export const useCreateCloudFormationUrl = ({ enrollmentAPIKey, - cloudFormationTemplateUrl, - packagePolicy, + cloudFormationProps, }: { enrollmentAPIKey: string | undefined; - cloudFormationTemplateUrl: string; - packagePolicy?: PackagePolicy; + cloudFormationProps: CloudFormationProps | undefined; }) => { const { data, isLoading } = useGetSettings(); const kibanaVersion = useKibanaVersion(); - const awsInput = packagePolicy?.inputs?.find((input) => input.type === CLOUDBEAT_AWS); - const awsAccountType = getAwsAccountType(awsInput) || ''; - let isError = false; let error: string | undefined; @@ -56,13 +49,13 @@ export const useCreateCloudFormationUrl = ({ } const cloudFormationUrl = - enrollmentAPIKey && fleetServerHost && cloudFormationTemplateUrl + enrollmentAPIKey && fleetServerHost && cloudFormationProps?.templateUrl ? createCloudFormationUrl( - cloudFormationTemplateUrl, + cloudFormationProps?.templateUrl, enrollmentAPIKey, fleetServerHost, kibanaVersion, - awsAccountType + cloudFormationProps?.awsAccountType ) : undefined; @@ -79,7 +72,7 @@ const createCloudFormationUrl = ( enrollmentToken: string, fleetUrl: string, kibanaVersion: string, - awsAccountType: string + awsAccountType: CloudSecurityIntegrationAwsAccountType | undefined ) => { let cloudFormationUrl; @@ -89,8 +82,15 @@ const createCloudFormationUrl = ( .replace('KIBANA_VERSION', kibanaVersion); if (cloudFormationUrl.includes('ACCOUNT_TYPE')) { - cloudFormationUrl = cloudFormationUrl.replace('ACCOUNT_TYPE', awsAccountType); + cloudFormationUrl = cloudFormationUrl.replace( + 'ACCOUNT_TYPE', + getAwsAccountType(awsAccountType) + ); } return new URL(cloudFormationUrl).toString(); }; + +const getAwsAccountType = (awsAccountType: CloudSecurityIntegrationAwsAccountType | undefined) => { + return awsAccountType ? awsAccountType : CLOUD_FORMATION_DEFAULT_ACCOUNT_TYPE; +}; diff --git a/x-pack/plugins/fleet/public/hooks/use_input.ts b/x-pack/plugins/fleet/public/hooks/use_input.ts index a48e3075d7a42..dbc60eafda833 100644 --- a/x-pack/plugins/fleet/public/hooks/use_input.ts +++ b/x-pack/plugins/fleet/public/hooks/use_input.ts @@ -86,6 +86,16 @@ export function useInput( export function useRadioInput(defaultValue: string, disabled = false) { const [value, setValue] = useState(defaultValue); + const [hasChanged, setHasChanged] = useState(false); + + useEffect(() => { + if (hasChanged) { + return; + } + if (value !== defaultValue) { + setHasChanged(true); + } + }, [hasChanged, value, defaultValue]); const onChange = useCallback(setValue, [setValue]); @@ -97,6 +107,7 @@ export function useRadioInput(defaultValue: string, disabled = false) { }, setValue, value, + hasChanged, }; } @@ -137,11 +148,15 @@ export function useSwitchInput(defaultValue = false, disabled = false) { function useCustomInput( id: string, defaultValue: T, - validate?: (value: T) => Array<{ message: string; index?: number }> | undefined, + validate?: ( + value: T + ) => Array<{ message: string; index?: number; condition?: boolean }> | undefined, disabled = false ) { const [value, setValue] = useState(defaultValue); - const [errors, setErrors] = useState | undefined>(); + const [errors, setErrors] = useState< + Array<{ message: string; index?: number; condition?: boolean }> | undefined + >(); const [hasChanged, setHasChanged] = useState(false); useEffect(() => { @@ -237,7 +252,9 @@ type Topic = Array<{ export function useTopicsInput( id: string, defaultValue: Topic = [], - validate?: (value: Topic) => Array<{ message: string; index: number }> | undefined, + validate?: ( + value: Topic + ) => Array<{ message: string; index: number; condition?: boolean }> | undefined, disabled = false ) { return useCustomInput(id, defaultValue, validate, disabled); diff --git a/x-pack/plugins/fleet/public/hooks/use_request/epm.ts b/x-pack/plugins/fleet/public/hooks/use_request/epm.ts index cc381c20ecd97..df8ceb8a4c2e2 100644 --- a/x-pack/plugins/fleet/public/hooks/use_request/epm.ts +++ b/x-pack/plugins/fleet/public/hooks/use_request/epm.ts @@ -105,6 +105,13 @@ export const useGetPackageInfoByKeyQuery = ( ignoreUnverified?: boolean; prerelease?: boolean; full?: boolean; + }, + // Additional options for the useQuery hook + queryOptions: { + // If enabled is false, the query will not be fetched + enabled?: boolean; + } = { + enabled: true, } ) => { const confirmOpenUnverified = useConfirmOpenUnverified(); @@ -112,15 +119,18 @@ export const useGetPackageInfoByKeyQuery = ( options?.ignoreUnverified ); - const response = useQuery([pkgName, pkgVersion, options], () => - sendRequestForRq({ - path: epmRouteService.getInfoPath(pkgName, pkgVersion), - method: 'get', - query: { - ...options, - ...(ignoreUnverifiedQueryParam && { ignoreUnverified: ignoreUnverifiedQueryParam }), - }, - }) + const response = useQuery( + [pkgName, pkgVersion, options], + () => + sendRequestForRq({ + path: epmRouteService.getInfoPath(pkgName, pkgVersion), + method: 'get', + query: { + ...options, + ...(ignoreUnverifiedQueryParam && { ignoreUnverified: ignoreUnverifiedQueryParam }), + }, + }), + { enabled: queryOptions.enabled } ); const confirm = async () => { diff --git a/x-pack/plugins/fleet/public/services/get_cloud_formation_props_from_package_policy.test.ts b/x-pack/plugins/fleet/public/services/get_cloud_formation_props_from_package_policy.test.ts new file mode 100644 index 0000000000000..f84d5c839dd40 --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_cloud_formation_props_from_package_policy.test.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { getCloudFormationPropsFromPackagePolicy } from './get_cloud_formation_props_from_package_policy'; + +describe('getCloudFormationPropsFromPackagePolicy', () => { + test('returns empty CloudFormationProps when packagePolicy is undefined', () => { + const result = getCloudFormationPropsFromPackagePolicy(undefined); + expect(result).toEqual({ + templateUrl: undefined, + awsAccountType: undefined, + }); + }); + + test('returns empty CloudFormationProps when packagePolicy has no inputs', () => { + const packagePolicy = { otherProperty: 'value' }; + // @ts-expect-error + const result = getCloudFormationPropsFromPackagePolicy(packagePolicy); + expect(result).toEqual({ + templateUrl: undefined, + awsAccountType: undefined, + }); + }); + + test('returns empty CloudFormationProps when no enabled input has a cloudFormationTemplateUrl', () => { + const packagePolicy = { + inputs: [ + { enabled: false, config: { cloud_formation_template_url: { value: 'template1' } } }, + { enabled: false, config: { cloud_formation_template_url: { value: 'template2' } } }, + ], + }; + // @ts-expect-error + const result = getCloudFormationPropsFromPackagePolicy(packagePolicy); + expect(result).toEqual({ + templateUrl: undefined, + awsAccountType: undefined, + }); + }); + + test('returns the cloudFormationTemplateUrl and awsAccountType when found in the enabled input', () => { + const packagePolicy = { + inputs: [ + { + enabled: true, + config: { cloud_formation_template_url: { value: 'template1' } }, + streams: [ + { + vars: { + ['aws.account_type']: { value: 'aws_account_type_value' }, + }, + }, + ], + }, + { + enabled: false, + config: { cloud_formation_template_url: { value: 'template2' } }, + streams: [ + { + vars: { + ['aws.account_type']: { value: 'aws_account_type_value2' }, + }, + }, + ], + }, + ], + }; + // @ts-expect-error + const result = getCloudFormationPropsFromPackagePolicy(packagePolicy); + expect(result).toEqual({ + templateUrl: 'template1', + awsAccountType: 'aws_account_type_value', + }); + }); + + test('returns the first cloudFormationTemplateUrl and awsAccountType when multiple enabled inputs have them', () => { + const packagePolicy = { + inputs: [ + { + enabled: true, + config: { + cloud_formation_template_url: { value: 'template1' }, + }, + streams: [ + { + vars: { + ['aws.account_type']: { value: 'aws_account_type_value1' }, + }, + }, + { + vars: { + ['aws.account_type']: { value: 'aws_account_type_value2' }, + }, + }, + ], + }, + { + enabled: true, + config: { + cloud_formation_template_url: { value: 'template2' }, + }, + streams: [ + { + vars: { + ['aws.account_type']: { value: 'aws_account_type_value1' }, + }, + }, + { + vars: { + ['aws.account_type']: { value: 'aws_account_type_value2' }, + }, + }, + ], + }, + ], + }; + // @ts-expect-error + const result = getCloudFormationPropsFromPackagePolicy(packagePolicy); + expect(result).toEqual({ + templateUrl: 'template1', + awsAccountType: 'aws_account_type_value1', + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_policy.ts b/x-pack/plugins/fleet/public/services/get_cloud_formation_props_from_package_policy.ts similarity index 52% rename from x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_policy.ts rename to x-pack/plugins/fleet/public/services/get_cloud_formation_props_from_package_policy.ts index 598e71709fdc7..b56659b21db99 100644 --- a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_policy.ts +++ b/x-pack/plugins/fleet/public/services/get_cloud_formation_props_from_package_policy.ts @@ -5,15 +5,23 @@ * 2.0. */ +import type { + CloudFormationProps, + CloudSecurityIntegrationAwsAccountType, +} from '../components/agent_enrollment_flyout/types'; import type { PackagePolicy } from '../types'; +const AWS_ACCOUNT_TYPE = 'aws.account_type'; + /** * Get the cloud formation template url from a package policy * It looks for a config with a cloud_formation_template_url object present in * the enabled inputs of the package policy */ -export const getCloudFormationTemplateUrlFromPackagePolicy = (packagePolicy?: PackagePolicy) => { - const cloudFormationTemplateUrl = packagePolicy?.inputs?.reduce((accInput, input) => { +export const getCloudFormationPropsFromPackagePolicy = ( + packagePolicy?: PackagePolicy +): CloudFormationProps => { + const templateUrl = packagePolicy?.inputs?.reduce((accInput, input) => { if (accInput !== '') { return accInput; } @@ -23,5 +31,12 @@ export const getCloudFormationTemplateUrlFromPackagePolicy = (packagePolicy?: Pa return accInput; }, ''); - return cloudFormationTemplateUrl !== '' ? cloudFormationTemplateUrl : undefined; + const awsAccountType: CloudSecurityIntegrationAwsAccountType | undefined = + packagePolicy?.inputs?.find((input) => input.enabled)?.streams?.[0]?.vars?.[AWS_ACCOUNT_TYPE] + ?.value; + + return { + templateUrl: templateUrl !== '' ? templateUrl : undefined, + awsAccountType, + }; }; diff --git a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_info.test.ts b/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_info.test.ts new file mode 100644 index 0000000000000..8ed2fb3ae389a --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_info.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getCloudFormationTemplateUrlFromPackageInfo } from './get_cloud_formation_template_url_from_package_info'; + +describe('getCloudFormationTemplateUrlFromPackageInfo', () => { + test('returns undefined when packageInfo is undefined', () => { + const result = getCloudFormationTemplateUrlFromPackageInfo(undefined, 'test'); + expect(result).toBeUndefined(); + }); + + test('returns undefined when packageInfo has no policy_templates', () => { + const packageInfo = { inputs: [] }; + // @ts-expect-error + const result = getCloudFormationTemplateUrlFromPackageInfo(packageInfo, 'test'); + expect(result).toBeUndefined(); + }); + + test('returns undefined when integrationType is not found in policy_templates', () => { + const packageInfo = { policy_templates: [{ name: 'template1' }, { name: 'template2' }] }; + // @ts-expect-error + const result = getCloudFormationTemplateUrlFromPackageInfo(packageInfo, 'nonExistentTemplate'); + expect(result).toBeUndefined(); + }); + + test('returns undefined when no input in the policy template has a cloudFormationTemplate', () => { + const packageInfo = { + policy_templates: [ + { + name: 'template1', + inputs: [ + { name: 'input1', vars: [] }, + { name: 'input2', vars: [{ name: 'var1', default: 'value1' }] }, + ], + }, + ], + }; + // @ts-expect-error + const result = getCloudFormationTemplateUrlFromPackageInfo(packageInfo, 'template1'); + expect(result).toBeUndefined(); + }); + + test('returns the cloudFormationTemplate from the policy template', () => { + const packageInfo = { + policy_templates: [ + { + name: 'template1', + inputs: [ + { name: 'input1', vars: [] }, + { + name: 'input2', + vars: [{ name: 'cloud_formation_template', default: 'cloud_formation_template_url' }], + }, + ], + }, + ], + }; + // @ts-expect-error + const result = getCloudFormationTemplateUrlFromPackageInfo(packageInfo, 'template1'); + expect(result).toBe('cloud_formation_template_url'); + }); +}); diff --git a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_info.ts b/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_info.ts new file mode 100644 index 0000000000000..4f5381ccedb3f --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_info.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PackageInfo } from '../types'; + +/** + * Get the cloud formation template url from the PackageInfo + * It looks for a input var with a object containing cloud_formation_template_url present in + * the package_policies inputs of the given integration type + */ +export const getCloudFormationTemplateUrlFromPackageInfo = ( + packageInfo: PackageInfo | undefined, + integrationType: string +): string | undefined => { + if (!packageInfo?.policy_templates) return undefined; + + const policyTemplate = packageInfo.policy_templates.find((p) => p.name === integrationType); + if (!policyTemplate) return undefined; + + if ('inputs' in policyTemplate) { + const cloudFormationTemplate = policyTemplate.inputs?.reduce((acc, input): string => { + if (!input.vars) return acc; + const template = input.vars.find((v) => v.name === 'cloud_formation_template')?.default; + return template ? String(template) : acc; + }, ''); + return cloudFormationTemplate !== '' ? cloudFormationTemplate : undefined; + } +}; diff --git a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_policy.test.ts b/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_policy.test.ts deleted file mode 100644 index 523641b10eb1b..0000000000000 --- a/x-pack/plugins/fleet/public/services/get_cloud_formation_template_url_from_package_policy.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { getCloudFormationTemplateUrlFromPackagePolicy } from './get_cloud_formation_template_url_from_package_policy'; - -describe('getCloudFormationTemplateUrlFromPackagePolicy', () => { - test('returns undefined when packagePolicy is undefined', () => { - const result = getCloudFormationTemplateUrlFromPackagePolicy(undefined); - expect(result).toBeUndefined(); - }); - - test('returns undefined when packagePolicy is defined but inputs are empty', () => { - const packagePolicy = { inputs: [] }; - // @ts-expect-error - const result = getCloudFormationTemplateUrlFromPackagePolicy(packagePolicy); - expect(result).toBeUndefined(); - }); - - test('returns undefined when no enabled input has a cloudFormationTemplateUrl', () => { - const packagePolicy = { - inputs: [ - { enabled: false, config: { cloud_formation_template_url: { value: 'template1' } } }, - { enabled: false, config: { cloud_formation_template_url: { value: 'template2' } } }, - ], - }; - // @ts-expect-error - const result = getCloudFormationTemplateUrlFromPackagePolicy(packagePolicy); - expect(result).toBeUndefined(); - }); - - test('returns the cloudFormationTemplateUrl of the first enabled input', () => { - const packagePolicy = { - inputs: [ - { enabled: false, config: { cloud_formation_template_url: { value: 'template1' } } }, - { enabled: true, config: { cloud_formation_template_url: { value: 'template2' } } }, - { enabled: true, config: { cloud_formation_template_url: { value: 'template3' } } }, - ], - }; - // @ts-expect-error - const result = getCloudFormationTemplateUrlFromPackagePolicy(packagePolicy); - expect(result).toBe('template2'); - }); - - test('returns the cloudFormationTemplateUrl of the first enabled input and ignores subsequent inputs', () => { - const packagePolicy = { - inputs: [ - { enabled: true, config: { cloud_formation_template_url: { value: 'template1' } } }, - { enabled: true, config: { cloud_formation_template_url: { value: 'template2' } } }, - { enabled: true, config: { cloud_formation_template_url: { value: 'template3' } } }, - ], - }; - // @ts-expect-error - const result = getCloudFormationTemplateUrlFromPackagePolicy(packagePolicy); - expect(result).toBe('template1'); - }); - - // Add more test cases as needed -}); diff --git a/x-pack/plugins/fleet/public/services/index.ts b/x-pack/plugins/fleet/public/services/index.ts index 1da10c7384cdc..44bf6b965742c 100644 --- a/x-pack/plugins/fleet/public/services/index.ts +++ b/x-pack/plugins/fleet/public/services/index.ts @@ -48,5 +48,6 @@ export { isPackageUpdatable } from './is_package_updatable'; export { pkgKeyFromPackageInfo } from './pkg_key_from_package_info'; export { createExtensionRegistrationCallback } from './ui_extensions'; export { incrementPolicyName } from './increment_policy_name'; -export { getCloudFormationTemplateUrlFromPackagePolicy } from './get_cloud_formation_template_url_from_package_policy'; +export { getCloudFormationPropsFromPackagePolicy } from './get_cloud_formation_props_from_package_policy'; export { getCloudFormationTemplateUrlFromAgentPolicy } from './get_cloud_formation_template_url_from_agent_policy'; +export { getCloudFormationTemplateUrlFromPackageInfo } from './get_cloud_formation_template_url_from_package_info'; diff --git a/x-pack/plugins/fleet/server/config.ts b/x-pack/plugins/fleet/server/config.ts index 14e5a86aa73ad..9726837375eed 100644 --- a/x-pack/plugins/fleet/server/config.ts +++ b/x-pack/plugins/fleet/server/config.ts @@ -139,7 +139,6 @@ export const config: PluginConfigDescriptor = { disableRegistryVersionCheck: schema.boolean({ defaultValue: false }), allowAgentUpgradeSourceUri: schema.boolean({ defaultValue: false }), bundledPackageLocation: schema.string({ defaultValue: DEFAULT_BUNDLED_PACKAGE_LOCATION }), - testSecretsIndex: schema.maybe(schema.string()), }), packageVerification: schema.object({ gpgKeyPath: schema.string({ defaultValue: DEFAULT_GPG_KEY_PATH }), diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 43e0069d3be48..b7013bf43ff84 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -25,6 +25,8 @@ import { UNINSTALL_TOKENS_SAVED_OBJECT_TYPE, } from '../constants'; +import { migrateOutputEvictionsFromV8100, migrateOutputToV8100 } from './migrations/to_v8_10_0'; + import { migrateSyntheticsPackagePolicyToV8100 } from './migrations/synthetics/to_v8_10_0'; import { migratePackagePolicyEvictionsFromV8100 } from './migrations/security_solution/to_v8_10_0'; @@ -177,6 +179,7 @@ const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({ compression_level: { type: 'integer' }, client_id: { type: 'keyword' }, auth_type: { type: 'keyword' }, + connection_type: { type: 'keyword' }, username: { type: 'keyword' }, password: { type: 'text', index: false }, sasl: { @@ -229,6 +232,29 @@ const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({ broker_timeout: { type: 'integer' }, broker_ack_reliability: { type: 'text' }, broker_buffer_size: { type: 'integer' }, + required_acks: { type: 'integer' }, + channel_buffer_size: { type: 'integer' }, + }, + }, + modelVersions: { + '1': { + changes: [ + { + type: 'mappings_deprecation', + deprecatedMappings: [ + 'broker_ack_reliability', + 'broker_buffer_size', + 'channel_buffer_size', + ], + }, + { + type: 'data_backfill', + backfillFn: migrateOutputToV8100, + }, + ], + schemas: { + forwardCompatibility: migrateOutputEvictionsFromV8100, + }, }, }, migrations: { @@ -528,6 +554,24 @@ export function registerEncryptedSavedObjects( 'config_yaml', 'is_preconfigured', 'proxy_id', + 'version', + 'key', + 'compression', + 'compression_level', + 'client_id', + 'auth_type', + 'connection_type', + 'username', + 'sasl', + 'partition', + 'random', + 'round_robin', + 'hash', + 'topics', + 'headers', + 'timeout', + 'broker_timeout', + 'required_acks', ]), }); // Encrypted saved objects diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_10_0.test.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_10_0.test.ts new file mode 100644 index 0000000000000..92b8c2c2d34b6 --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_10_0.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectModelTransformationContext } from '@kbn/core-saved-objects-server'; + +import { + migrateOutputToV8100 as migration, + migrateOutputEvictionsFromV8100 as eviction, +} from './to_v8_10_0'; + +describe('8.10.0 migration', () => { + describe('Migrate output to v8.10.0', () => { + const outputDoc = (connectionType = {}) => ({ + id: 'mock-saved-object-id', + attributes: { + id: 'id', + name: 'Test', + type: 'kafka' as const, + is_default: false, + is_default_monitoring: false, + hosts: ['localhost:9092'], + ca_sha256: 'sha', + ca_trusted_fingerprint: 'fingerprint', + version: '7.10.0', + key: 'key', + compression: 'gzip' as const, + compression_level: 4, + client_id: 'Elastic', + auth_type: 'none' as const, + ...connectionType, + topics: [{ topic: 'topic' }], + }, + type: 'nested', + }); + + it('adds connection type field to output and sets it to plaintext', () => { + const initialDoc = outputDoc({}); + + const migratedDoc = outputDoc({ + connection_type: 'plaintext', + }); + + expect(migration(initialDoc, {} as SavedObjectModelTransformationContext)).toEqual({ + attributes: migratedDoc.attributes, + }); + }); + + it('removes connection type field from output', () => { + const initialDoc = outputDoc({ + connection_type: 'plaintext', + }); + + const migratedDoc = outputDoc({}); + + expect(eviction(initialDoc.attributes)).toEqual(migratedDoc.attributes); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_10_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_10_0.ts new file mode 100644 index 0000000000000..fc0ebda76086b --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_10_0.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + SavedObjectModelDataBackfillFn, + SavedObjectUnsanitizedDoc, +} from '@kbn/core-saved-objects-server'; + +import type { SavedObjectModelVersionForwardCompatibilityFn } from '@kbn/core-saved-objects-server'; + +import { omit } from 'lodash'; + +import type { Output } from '../../../common'; + +export const migrateOutputToV8100: SavedObjectModelDataBackfillFn = (outputDoc) => { + const updatedOutputDoc: SavedObjectUnsanitizedDoc = outputDoc; + + if (updatedOutputDoc.attributes.type === 'kafka') { + updatedOutputDoc.attributes.connection_type = 'plaintext'; + } + + return { + attributes: updatedOutputDoc.attributes, + }; +}; + +export const migrateOutputEvictionsFromV8100: SavedObjectModelVersionForwardCompatibilityFn = ( + unknownAttributes +) => { + const attributes = unknownAttributes as Output; + if (attributes.type !== 'kafka') { + return attributes; + } + + let updatedAttributes = attributes; + + updatedAttributes = omit(updatedAttributes, ['connection_type']); + + return updatedAttributes; +}; diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts index 1da8187744091..d50e12541063a 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts @@ -291,7 +291,6 @@ export function transformOutputToFullPolicyOutput( key, compression, compression_level, - auth_type, username, password, sasl, @@ -303,31 +302,69 @@ export function transformOutputToFullPolicyOutput( headers, timeout, broker_timeout, - broker_buffer_size, - broker_ack_reliability, + required_acks, } = output; - /* eslint-enable @typescript-eslint/naming-convention */ + const transformPartition = () => { + if (!partition) return {}; + switch (partition) { + case 'random': + return { + random: { + ...(random?.group_events + ? { group_events: random.group_events } + : { group_events: 1 }), + }, + }; + case 'round_robin': + return { + round_robin: { + ...(round_robin?.group_events + ? { group_events: round_robin.group_events } + : { group_events: 1 }), + }, + }; + case 'hash': + default: + return { hash: { ...(hash?.hash ? { hash: hash.hash } : { hash: '' }) } }; + } + }; + + /* eslint-enable @typescript-eslint/naming-convention */ kafkaData = { client_id, version, key, compression, compression_level, - auth_type, - username, - password, - sasl, - partition, - random, - round_robin, - hash, - topics, - headers, + ...(username ? { username } : {}), + ...(password ? { password } : {}), + ...(sasl ? { sasl } : {}), + partition: transformPartition(), + topics: (topics ?? []).map((topic) => { + const { topic: topicName, ...rest } = topic; + const whenKeys = Object.keys(rest); + + if (whenKeys.length === 0) { + return { topic: topicName }; + } + if (rest.when && rest.when.condition) { + const [keyName, value] = rest.when.condition.split(':'); + + return { + topic: topicName, + when: { + [rest.when.type as string]: { + [keyName.replace(/\s/g, '')]: value.replace(/\s/g, ''), + }, + }, + }; + } + }), + headers: (headers ?? []).filter((item) => item.key !== '' || item.value !== ''), timeout, broker_timeout, - broker_buffer_size, - broker_ack_reliability, + required_acks, }; } diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index e9a4e255e9ea1..ec0cbab539bcd 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -320,6 +320,7 @@ export async function installKibanaSavedObjects({ readStream: createListStream(toBeSavedObjects), createNewCopies: false, refresh: false, + managed: true, }) ); @@ -371,6 +372,7 @@ export async function installKibanaSavedObjects({ await savedObjectsImporter.resolveImportErrors({ readStream: createListStream(toBeSavedObjects), createNewCopies: false, + managed: true, retries, }); diff --git a/x-pack/plugins/fleet/server/services/output.test.ts b/x-pack/plugins/fleet/server/services/output.test.ts index 31efb22834cdb..12abc7c0bec70 100644 --- a/x-pack/plugins/fleet/server/services/output.test.ts +++ b/x-pack/plugins/fleet/server/services/output.test.ts @@ -916,9 +916,9 @@ describe('Output Service', () => { type: 'elasticsearch', hosts: ['http://test:4343'], auth_type: null, + connection_type: null, broker_timeout: null, - broker_ack_reliability: null, - broker_buffer_size: null, + required_acks: null, client_id: null, compression: null, compression_level: null, @@ -1033,13 +1033,14 @@ describe('Output Service', () => { ca_sha256: null, ca_trusted_fingerprint: null, auth_type: null, + connection_type: null, broker_timeout: null, - broker_ack_reliability: null, - broker_buffer_size: null, + required_acks: null, client_id: null, compression: null, compression_level: null, hash: null, + ssl: null, key: null, partition: null, password: null, @@ -1257,10 +1258,13 @@ describe('Output Service', () => { hosts: ['test:4343'], ca_sha256: null, ca_trusted_fingerprint: null, + password: null, + username: null, + ssl: null, + sasl: null, broker_timeout: 10, - broker_ack_reliability: 'Wait for local commit', - broker_buffer_size: 256, - client_id: 'Elastic Agent', + required_acks: 1, + client_id: 'Elastic', compression: 'gzip', compression_level: 4, partition: 'hash', @@ -1285,11 +1289,14 @@ describe('Output Service', () => { expect(soClient.update).toBeCalledWith(expect.anything(), expect.anything(), { hosts: ['test:4343'], broker_timeout: 10, - broker_ack_reliability: 'Wait for local commit', - broker_buffer_size: 256, + required_acks: 1, ca_sha256: null, ca_trusted_fingerprint: null, - client_id: 'Elastic Agent', + password: null, + username: null, + ssl: null, + sasl: null, + client_id: 'Elastic', compression: 'gzip', compression_level: 4, partition: 'hash', @@ -1310,25 +1317,28 @@ describe('Output Service', () => { await outputService.update(soClient, esClientMock, 'output-test', { type: 'kafka', - hosts: ['http://test:4343'], + hosts: ['test:4343'], is_default: true, }); expect(soClient.update).toBeCalledWith(expect.anything(), expect.anything(), { type: 'kafka', - hosts: ['http://test:4343'], + hosts: ['test:4343'], is_default: true, ca_sha256: null, ca_trusted_fingerprint: null, - client_id: 'Elastic Agent', + password: null, + username: null, + ssl: null, + sasl: null, + client_id: 'Elastic', compression: 'gzip', compression_level: 4, partition: 'hash', timeout: 30, version: '1.0.0', broker_timeout: 10, - broker_ack_reliability: 'Wait for local commit', - broker_buffer_size: 256, + required_acks: 1, }); expect(mockedAgentPolicyService.update).toBeCalledWith( expect.anything(), @@ -1354,7 +1364,7 @@ describe('Output Service', () => { 'output-test', { type: 'kafka', - hosts: ['http://test:4343'], + hosts: ['test:4343'], is_default: true, }, { @@ -1364,19 +1374,22 @@ describe('Output Service', () => { expect(soClient.update).toBeCalledWith(expect.anything(), expect.anything(), { type: 'kafka', - hosts: ['http://test:4343'], + hosts: ['test:4343'], is_default: true, ca_sha256: null, ca_trusted_fingerprint: null, - client_id: 'Elastic Agent', + password: null, + username: null, + ssl: null, + sasl: null, + client_id: 'Elastic', compression: 'gzip', compression_level: 4, partition: 'hash', timeout: 30, version: '1.0.0', broker_timeout: 10, - broker_ack_reliability: 'Wait for local commit', - broker_buffer_size: 256, + required_acks: 1, }); expect(mockedAgentPolicyService.update).toBeCalledWith( expect.anything(), @@ -1396,25 +1409,28 @@ describe('Output Service', () => { await outputService.update(soClient, esClientMock, 'output-test', { type: 'kafka', - hosts: ['http://test:4343'], + hosts: ['test:4343'], is_default: true, }); expect(soClient.update).toBeCalledWith(expect.anything(), expect.anything(), { type: 'kafka', - hosts: ['http://test:4343'], + hosts: ['test:4343'], is_default: true, ca_sha256: null, ca_trusted_fingerprint: null, - client_id: 'Elastic Agent', + password: null, + username: null, + ssl: null, + sasl: null, + client_id: 'Elastic', compression: 'gzip', compression_level: 4, partition: 'hash', timeout: 30, version: '1.0.0', broker_timeout: 10, - broker_ack_reliability: 'Wait for local commit', - broker_buffer_size: 256, + required_acks: 1, }); expect(mockedAgentPolicyService.update).toBeCalledWith( expect.anything(), @@ -1438,7 +1454,7 @@ describe('Output Service', () => { 'output-test', { type: 'kafka', - hosts: ['http://test:4343'], + hosts: ['test:4343'], is_default: true, }, { @@ -1448,19 +1464,22 @@ describe('Output Service', () => { expect(soClient.update).toBeCalledWith(expect.anything(), expect.anything(), { type: 'kafka', - hosts: ['http://test:4343'], + hosts: ['test:4343'], is_default: true, ca_sha256: null, ca_trusted_fingerprint: null, - client_id: 'Elastic Agent', + password: null, + username: null, + ssl: null, + sasl: null, + client_id: 'Elastic', compression: 'gzip', compression_level: 4, partition: 'hash', timeout: 30, version: '1.0.0', broker_timeout: 10, - broker_ack_reliability: 'Wait for local commit', - broker_buffer_size: 256, + required_acks: 1, }); expect(mockedAgentPolicyService.update).toBeCalledWith( expect.anything(), diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts index d95771eb48238..2cf1764a78f59 100644 --- a/x-pack/plugins/fleet/server/services/output.ts +++ b/x-pack/plugins/fleet/server/services/output.ts @@ -493,7 +493,7 @@ class OutputService { data.compression_level = 4; } if (!output.client_id) { - data.client_id = 'Elastic Agent'; + data.client_id = 'Elastic'; } if (output.username && output.password && !output.sasl?.mechanism) { data.sasl = { @@ -519,11 +519,9 @@ class OutputService { if (!output.broker_timeout) { data.broker_timeout = 10; } - if (!output.broker_ack_reliability) { - data.broker_ack_reliability = kafkaAcknowledgeReliabilityLevel.Commit; - } - if (!output.broker_buffer_size) { - data.broker_buffer_size = 256; + if (output.required_acks === null || output.required_acks === undefined) { + // required_acks can be 0 + data.required_acks = kafkaAcknowledgeReliabilityLevel.Commit; } } @@ -712,6 +710,7 @@ class OutputService { target.key = null; target.compression = null; target.compression_level = null; + target.connection_type = null; target.client_id = null; target.auth_type = null; target.username = null; @@ -725,8 +724,8 @@ class OutputService { target.headers = null; target.timeout = null; target.broker_timeout = null; - target.broker_ack_reliability = null; - target.broker_buffer_size = null; + target.required_acks = null; + target.ssl = null; }; // If the output type changed @@ -766,7 +765,7 @@ class OutputService { updateData.compression_level = 4; } if (!data.client_id) { - updateData.client_id = 'Elastic Agent'; + updateData.client_id = 'Elastic'; } if (data.username && data.password && !data.sasl?.mechanism) { updateData.sasl = { @@ -792,11 +791,9 @@ class OutputService { if (!data.broker_timeout) { updateData.broker_timeout = 10; } - if (!data.broker_ack_reliability) { - updateData.broker_ack_reliability = kafkaAcknowledgeReliabilityLevel.Commit; - } - if (!data.broker_buffer_size) { - updateData.broker_buffer_size = 256; + if (updateData.required_acks === null || updateData.required_acks === undefined) { + // required_acks can be 0 + updateData.required_acks = kafkaAcknowledgeReliabilityLevel.Commit; } } } @@ -808,6 +805,21 @@ class OutputService { updateData.ssl = null; } + if (data.type === outputType.Kafka && updateData.type === outputType.Kafka) { + if (!data.password) { + updateData.password = null; + } + if (!data.username) { + updateData.username = null; + } + if (!data.ssl) { + updateData.ssl = null; + } + if (!data.sasl) { + updateData.sasl = null; + } + } + // ensure only default output exists if (data.is_default) { if (defaultDataOutputId && defaultDataOutputId !== id) { diff --git a/x-pack/plugins/fleet/server/types/models/output.test.ts b/x-pack/plugins/fleet/server/types/models/output.test.ts index 4441630653a99..06edd900fec2a 100644 --- a/x-pack/plugins/fleet/server/types/models/output.test.ts +++ b/x-pack/plugins/fleet/server/types/models/output.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { validateLogstashHost } from './output'; +import { validateKafkaHost, validateLogstashHost } from './output'; describe('Output model', () => { describe('validateLogstashHost', () => { @@ -23,4 +23,22 @@ describe('Output model', () => { ); }); }); + + describe('validateKafkaHost', () => { + it('should support valid host', () => { + expect(validateKafkaHost('test.fr:5044')).toBeUndefined(); + }); + + it('should return an error for an invalid host', () => { + expect(validateKafkaHost('!@#%&!#!@')).toBe( + 'Invalid format. Expected "host:port" without protocol' + ); + }); + + it('should return an error for an invalid host with http scheme', () => { + expect(validateKafkaHost('https://test.fr:5044')).toBe( + 'Invalid format. Expected "host:port" without protocol' + ); + }); + }); }); diff --git a/x-pack/plugins/fleet/server/types/models/output.ts b/x-pack/plugins/fleet/server/types/models/output.ts index b18d2baa25103..845361116d732 100644 --- a/x-pack/plugins/fleet/server/types/models/output.ts +++ b/x-pack/plugins/fleet/server/types/models/output.ts @@ -8,12 +8,13 @@ import { schema } from '@kbn/config-schema'; import { - kafkaAcknowledgeReliabilityLevel, kafkaAuthType, kafkaCompressionType, + kafkaConnectionType, kafkaPartitionType, kafkaSaslMechanism, kafkaTopicWhenType, + kafkaVerificationModes, outputType, } from '../../../common/constants'; @@ -33,6 +34,21 @@ export function validateLogstashHost(val: string) { } } +export const validateKafkaHost = (input: string): string | undefined => { + const parts = input.split(':'); + + if (parts.length !== 2 || !parts[0] || parts[0].includes('://')) { + return 'Invalid format. Expected "host:port" without protocol'; + } + + const port = parseInt(parts[1], 10); + if (isNaN(port) || port < 1 || port > 65535) { + return 'Invalid port number. Expected a number between 1 and 65535'; + } + + return undefined; +}; + /** * Base schemas */ @@ -50,6 +66,14 @@ const BaseSchema = { certificate_authorities: schema.maybe(schema.arrayOf(schema.string())), certificate: schema.maybe(schema.string()), key: schema.maybe(schema.string()), + verification_mode: schema.maybe( + schema.oneOf([ + schema.literal(kafkaVerificationModes.Full), + schema.literal(kafkaVerificationModes.None), + schema.literal(kafkaVerificationModes.Certificate), + schema.literal(kafkaVerificationModes.Strict), + ]) + ), }) ), proxy_id: schema.nullable(schema.string()), @@ -121,15 +145,9 @@ const KafkaTopicsSchema = schema.arrayOf( schema.object({ type: schema.maybe( schema.oneOf([ - schema.literal(kafkaTopicWhenType.And), - schema.literal(kafkaTopicWhenType.Not), - schema.literal(kafkaTopicWhenType.Or), schema.literal(kafkaTopicWhenType.Equals), schema.literal(kafkaTopicWhenType.Contains), schema.literal(kafkaTopicWhenType.Regexp), - schema.literal(kafkaTopicWhenType.HasFields), - schema.literal(kafkaTopicWhenType.Network), - schema.literal(kafkaTopicWhenType.Range), ]) ), condition: schema.maybe(schema.string()), @@ -142,7 +160,7 @@ const KafkaTopicsSchema = schema.arrayOf( export const KafkaSchema = { ...BaseSchema, type: schema.literal(outputType.Kafka), - hosts: schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }), { minSize: 1 }), + hosts: schema.arrayOf(schema.string({ validate: validateKafkaHost }), { minSize: 1 }), version: schema.maybe(schema.string()), key: schema.maybe(schema.string()), compression: schema.maybe( @@ -161,10 +179,20 @@ export const KafkaSchema = { ), client_id: schema.maybe(schema.string()), auth_type: schema.oneOf([ + schema.literal(kafkaAuthType.None), schema.literal(kafkaAuthType.Userpass), schema.literal(kafkaAuthType.Ssl), schema.literal(kafkaAuthType.Kerberos), ]), + connection_type: schema.conditional( + schema.siblingRef('auth_type'), + kafkaAuthType.None, + schema.oneOf([ + schema.literal(kafkaConnectionType.Plaintext), + schema.literal(kafkaConnectionType.Encryption), + ]), + schema.never() + ), username: schema.conditional( schema.siblingRef('auth_type'), kafkaAuthType.Userpass, @@ -206,13 +234,8 @@ export const KafkaSchema = { ), timeout: schema.maybe(schema.number()), broker_timeout: schema.maybe(schema.number()), - broker_buffer_size: schema.maybe(schema.number()), - broker_ack_reliability: schema.maybe( - schema.oneOf([ - schema.literal(kafkaAcknowledgeReliabilityLevel.Commit), - schema.literal(kafkaAcknowledgeReliabilityLevel.Replica), - schema.literal(kafkaAcknowledgeReliabilityLevel.DoNotWait), - ]) + required_acks: schema.maybe( + schema.oneOf([schema.literal(1), schema.literal(0), schema.literal(-1)]) ), }; @@ -220,9 +243,12 @@ const KafkaUpdateSchema = { ...UpdateSchema, ...KafkaSchema, type: schema.maybe(schema.literal(outputType.Kafka)), - hosts: schema.maybe(schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }), { minSize: 1 })), + hosts: schema.maybe( + schema.arrayOf(schema.string({ validate: validateKafkaHost }), { minSize: 1 }) + ), auth_type: schema.maybe( schema.oneOf([ + schema.literal(kafkaAuthType.None), schema.literal(kafkaAuthType.Userpass), schema.literal(kafkaAuthType.Ssl), schema.literal(kafkaAuthType.Kerberos), diff --git a/x-pack/plugins/fleet/server/types/so_attributes.ts b/x-pack/plugins/fleet/server/types/so_attributes.ts index 13ccff751c38d..941f27cde7c4d 100644 --- a/x-pack/plugins/fleet/server/types/so_attributes.ts +++ b/x-pack/plugins/fleet/server/types/so_attributes.ts @@ -14,6 +14,7 @@ import type { OutputType, ShipperOutput, KafkaAcknowledgeReliabilityLevel, + KafkaConnectionTypeType, } from '../../common/types'; import type { AgentType, FleetServerAgentComponent } from '../../common/types/models'; @@ -159,6 +160,7 @@ export interface OutputSoKafkaAttributes extends OutputSoBaseAttributes { compression?: ValueOf; compression_level?: number; auth_type?: ValueOf; + connection_type?: ValueOf; username?: string; password?: string; sasl?: { @@ -188,8 +190,7 @@ export interface OutputSoKafkaAttributes extends OutputSoBaseAttributes { }>; timeout?: number; broker_timeout?: number; - broker_buffer_size?: number; - broker_ack_reliability?: ValueOf; + required_acks?: ValueOf; } export type OutputSOAttributes = diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index d00aa6ff1f0e6..6bb3b834ce85f 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -53,7 +53,7 @@ export async function mountManagementSection( extensionsService: ExtensionsService, isFleetEnabled: boolean, kibanaVersion: SemVer, - enableIndexActions: boolean + enableIndexActions: boolean = true ) { const { element, setBreadcrumbs, history, theme$ } = params; const [core, startDependencies] = await coreSetup.getStartServices(); diff --git a/x-pack/plugins/index_management/public/types.ts b/x-pack/plugins/index_management/public/types.ts index 59954c6659494..20d2405a0fa4b 100644 --- a/x-pack/plugins/index_management/public/types.ts +++ b/x-pack/plugins/index_management/public/types.ts @@ -28,5 +28,5 @@ export interface ClientConfigType { ui: { enabled: boolean; }; - enableIndexActions: boolean; + enableIndexActions?: boolean; } diff --git a/x-pack/plugins/index_management/server/config.ts b/x-pack/plugins/index_management/server/config.ts index 4fd24bf3fcdf7..c5d459486a8ef 100644 --- a/x-pack/plugins/index_management/server/config.ts +++ b/x-pack/plugins/index_management/server/config.ts @@ -22,7 +22,14 @@ const schemaLatest = schema.object( ui: schema.object({ enabled: schema.boolean({ defaultValue: true }), }), - enableIndexActions: schema.boolean({ defaultValue: true }), + enableIndexActions: schema.conditional( + schema.contextRef('serverless'), + true, + // Index actions are disabled in serverless; refer to the serverless.yml file as the source of truth + // We take this approach in order to have a central place (serverless.yml) for serverless config across Kibana + schema.boolean({ defaultValue: true }), + schema.never() + ), }, { defaultValue: undefined } ); diff --git a/x-pack/plugins/infra/public/common/visualizations/constants.ts b/x-pack/plugins/infra/public/common/visualizations/constants.ts index 05841cd6e6a54..09583ef7ae3ed 100644 --- a/x-pack/plugins/infra/public/common/visualizations/constants.ts +++ b/x-pack/plugins/infra/public/common/visualizations/constants.ts @@ -11,6 +11,7 @@ import { diskIOWrite, diskReadThroughput, diskWriteThroughput, + diskSpaceAvailability, diskSpaceAvailable, diskSpaceUsage, logRate, @@ -28,6 +29,7 @@ export const hostLensFormulas = { diskIOWrite, diskReadThroughput, diskWriteThroughput, + diskSpaceAvailability, diskSpaceAvailable, diskSpaceUsage, hostCount, diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/dashboards/host/kpi_grid_config.ts b/x-pack/plugins/infra/public/common/visualizations/lens/dashboards/host/kpi_grid_config.ts index cc4f51c8f2d18..9dde33f39cbdf 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/dashboards/host/kpi_grid_config.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/dashboards/host/kpi_grid_config.ts @@ -5,16 +5,24 @@ * 2.0. */ -import { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { i18n } from '@kbn/i18n'; -import { Layer } from '../../../../../hooks/use_lens_attributes'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; +import type { Layer } from '../../../../../hooks/use_lens_attributes'; import { hostLensFormulas } from '../../../constants'; -import { FormulaConfig } from '../../../types'; import { TOOLTIP } from './translations'; -import { MetricLayerOptions } from '../../visualization_types/layers'; -export interface KPIChartProps - extends Pick { +import type { FormulaConfig } from '../../../types'; +import type { MetricLayerOptions } from '../../visualization_types'; + +export const KPI_CHART_HEIGHT = 150; +export const AVERAGE_SUBTITLE = i18n.translate( + 'xpack.infra.assetDetailsEmbeddable.overview.kpi.subtitle.average', + { + defaultMessage: 'Average', + } +); + +export interface KPIChartProps extends Pick { layers: Layer; toolTip: string; } @@ -22,7 +30,7 @@ export interface KPIChartProps export const KPI_CHARTS: KPIChartProps[] = [ { id: 'cpuUsage', - title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpuUsage.title', { + title: i18n.translate('xpack.infra.assetDetailsEmbeddable.overview.kpi.cpuUsage.title', { defaultMessage: 'CPU Usage', }), layers: { @@ -45,9 +53,12 @@ export const KPI_CHARTS: KPIChartProps[] = [ }, { id: 'normalizedLoad1m', - title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.normalizedLoad1m.title', { - defaultMessage: 'CPU Usage', - }), + title: i18n.translate( + 'xpack.infra.assetDetailsEmbeddable.overview.kpi.normalizedLoad1m.title', + { + defaultMessage: 'CPU Usage', + } + ), layers: { data: { ...hostLensFormulas.normalizedLoad1m, @@ -68,7 +79,7 @@ export const KPI_CHARTS: KPIChartProps[] = [ }, { id: 'memoryUsage', - title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memoryUsage.title', { + title: i18n.translate('xpack.infra.assetDetailsEmbeddable.overview.kpi.memoryUsage.title', { defaultMessage: 'CPU Usage', }), layers: { @@ -91,7 +102,7 @@ export const KPI_CHARTS: KPIChartProps[] = [ }, { id: 'diskSpaceUsage', - title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.diskSpaceUsage.title', { + title: i18n.translate('xpack.infra.assetDetailsEmbeddable.overview.kpi.diskSpaceUsage.title', { defaultMessage: 'CPU Usage', }), layers: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_throughput.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_throughput.ts index 946e26cec62a1..48fa795e9688a 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_throughput.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_throughput.ts @@ -9,7 +9,7 @@ import type { FormulaConfig } from '../../../types'; export const diskReadThroughput: FormulaConfig = { label: 'Disk Read Throughput', - value: "counter_rate(max(system.diskio.read.count), kql='system.diskio.read.count: *')", + value: "counter_rate(max(system.diskio.read.bytes), kql='system.diskio.read.bytes: *')", format: { id: 'bytes', params: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_availability.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_availability.ts new file mode 100644 index 0000000000000..aadbd8ccea650 --- /dev/null +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_availability.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FormulaConfig } from '../../../types'; + +export const diskSpaceAvailability: FormulaConfig = { + label: 'Disk Space Availability', + value: '1 - average(system.filesystem.used.pct)', + format: { + id: 'percent', + params: { + decimals: 0, + }, + }, +}; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_throughput.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_throughput.ts index e391bce4a1151..aed685aa34d8c 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_throughput.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_throughput.ts @@ -9,7 +9,7 @@ import type { FormulaConfig } from '../../../types'; export const diskWriteThroughput: FormulaConfig = { label: 'Disk Write Throughput', - value: "counter_rate(max(system.diskio.write.count), kql='system.diskio.write.count: *')", + value: "counter_rate(max(system.diskio.write.bytes), kql='system.diskio.write.bytes: *')", format: { id: 'bytes', params: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/index.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/index.ts index 6ac608a97d2e4..9cec01155ac19 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/index.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/index.ts @@ -10,6 +10,7 @@ export { diskIORead } from './disk_read_iops'; export { diskIOWrite } from './disk_write_iops'; export { diskReadThroughput } from './disk_read_throughput'; export { diskWriteThroughput } from './disk_write_throughput'; +export { diskSpaceAvailability } from './disk_space_availability'; export { diskSpaceAvailable } from './disk_space_available'; export { diskSpaceUsage } from './disk_space_usage'; export { hostCount } from './host_count'; diff --git a/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/asset_details_state.ts b/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/asset_details_state.ts index edcd1d0627d3d..4e88dc368ca0a 100644 --- a/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/asset_details_state.ts +++ b/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/asset_details_state.ts @@ -9,7 +9,7 @@ import type { DataViewField, DataView } from '@kbn/data-views-plugin/common'; import { UseAssetDetailsStateProps } from '../../../hooks/use_asset_details_state'; export const assetDetailsState: UseAssetDetailsStateProps['state'] = { - node: { + asset: { name: 'host1', id: 'host1-macOS', ip: '192.168.0.1', @@ -29,7 +29,7 @@ export const assetDetailsState: UseAssetDetailsStateProps['state'] = { showActionsColumn: true, }, }, - nodeType: 'host', + assetType: 'host', dateRange: { from: '2023-04-09T11:07:49Z', to: '2023-04-09T11:23:49Z', diff --git a/x-pack/plugins/infra/public/components/asset_details/asset_details.stories.tsx b/x-pack/plugins/infra/public/components/asset_details/asset_details.stories.tsx index a90fe5764f531..824a4e5f65ef0 100644 --- a/x-pack/plugins/infra/public/components/asset_details/asset_details.stories.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/asset_details.stories.tsx @@ -22,42 +22,36 @@ const tabs: Tab[] = [ name: i18n.translate('xpack.infra.nodeDetails.tabs.overview.title', { defaultMessage: 'Overview', }), - 'data-test-subj': 'hostsView-flyout-tabs-overview', }, { id: FlyoutTabIds.LOGS, name: i18n.translate('xpack.infra.nodeDetails.tabs.logs', { defaultMessage: 'Logs', }), - 'data-test-subj': 'hostsView-flyout-tabs-logs', }, { id: FlyoutTabIds.METADATA, name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.metadata', { defaultMessage: 'Metadata', }), - 'data-test-subj': 'hostsView-flyout-tabs-metadata', }, { id: FlyoutTabIds.PROCESSES, name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.processes', { defaultMessage: 'Processes', }), - 'data-test-subj': 'hostsView-flyout-tabs-processes', }, { id: FlyoutTabIds.ANOMALIES, name: i18n.translate('xpack.infra.nodeDetails.tabs.anomalies', { defaultMessage: 'Anomalies', }), - 'data-test-subj': 'hostsView-flyout-tabs-anomalies', }, { id: FlyoutTabIds.LINK_TO_APM, name: i18n.translate('xpack.infra.infra.nodeDetails.apmTabLabel', { defaultMessage: 'APM', }), - 'data-test-subj': 'hostsView-flyout-apm-link', }, ]; @@ -96,7 +90,7 @@ const FlyoutTemplate: Story = (args) => { Open flyout ); @@ -107,7 +101,7 @@ export const Page = PageTemplate.bind({}); export const Flyout = FlyoutTemplate.bind({}); Flyout.args = { renderMode: { - showInFlyout: true, + mode: 'flyout', closeFlyout: () => {}, }, }; diff --git a/x-pack/plugins/infra/public/components/asset_details/asset_details.tsx b/x-pack/plugins/infra/public/components/asset_details/asset_details.tsx index 101a23a4d084e..238a8c5f00250 100644 --- a/x-pack/plugins/infra/public/components/asset_details/asset_details.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/asset_details.tsx @@ -7,11 +7,17 @@ import React from 'react'; import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody } from '@elastic/eui'; +import useEffectOnce from 'react-use/lib/useEffectOnce'; import type { AssetDetailsProps, RenderMode } from './types'; import { Content } from './content/content'; import { Header } from './header/header'; -import { TabSwitcherProvider } from './hooks/use_tab_switcher'; -import { AssetDetailsStateProvider } from './hooks/use_asset_details_state'; +import { TabSwitcherProvider, useTabSwitcherContext } from './hooks/use_tab_switcher'; +import { + AssetDetailsStateProvider, + useAssetDetailsStateContext, +} from './hooks/use_asset_details_state'; +import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; +import { ASSET_DETAILS_FLYOUT_COMPONENT_NAME } from './constants'; interface ContentTemplateProps { header: React.ReactElement; @@ -20,8 +26,27 @@ interface ContentTemplateProps { } const ContentTemplate = ({ header, body, renderMode }: ContentTemplateProps) => { - return renderMode.showInFlyout ? ( - + const { assetType } = useAssetDetailsStateContext(); + const { initialActiveTabId } = useTabSwitcherContext(); + const { + services: { telemetry }, + } = useKibanaContextForPlugin(); + + useEffectOnce(() => { + telemetry.reportAssetDetailsFlyoutViewed({ + componentName: ASSET_DETAILS_FLYOUT_COMPONENT_NAME, + assetType, + tabId: initialActiveTabId, + }); + }); + + return renderMode.mode === 'flyout' ? ( + {header} {body} @@ -34,25 +59,27 @@ const ContentTemplate = ({ header, body, renderMode }: ContentTemplateProps) => }; export const AssetDetails = ({ - node, + asset, dateRange, activeTabId, overrides, onTabsStateChange, tabs = [], links = [], - nodeType = 'host', + assetType = 'host', renderMode = { - showInFlyout: false, + mode: 'page', }, }: AssetDetailsProps) => { return ( - + 0 ? activeTabId ?? tabs[0].id : undefined} > } + header={
} body={} renderMode={renderMode} /> diff --git a/x-pack/plugins/infra/public/components/asset_details/asset_details_embeddable.tsx b/x-pack/plugins/infra/public/components/asset_details/asset_details_embeddable.tsx index 355d8fac0055b..534ba4c2e4265 100644 --- a/x-pack/plugins/infra/public/components/asset_details/asset_details_embeddable.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/asset_details_embeddable.tsx @@ -73,8 +73,8 @@ export class AssetDetailsEmbeddable extends Embeddable { values={{ documentation: ( diff --git a/x-pack/plugins/infra/public/components/asset_details/components/expandable_content.tsx b/x-pack/plugins/infra/public/components/asset_details/components/expandable_content.tsx index 60f487d52d0ea..0cd5e1a53013a 100644 --- a/x-pack/plugins/infra/public/components/asset_details/components/expandable_content.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/components/expandable_content.tsx @@ -31,7 +31,10 @@ export const ExpandableContent = (props: ExpandableContentProps) => { {shouldShowMore && ( <> {' ... '} - + & { const APM_FIELD = 'host.hostname'; export const Header = ({ tabs = [], links = [], compact }: Props) => { - const { node, nodeType, overrides, dateRange: timeRange } = useAssetDetailsStateContext(); + const { asset, assetType, overrides, dateRange: timeRange } = useAssetDetailsStateContext(); const { euiTheme } = useEuiTheme(); const { showTab, activeTabId } = useTabSwitcherContext(); @@ -46,23 +47,23 @@ export const Header = ({ tabs = [], links = [], compact }: Props) => { const tabLinkComponents = { [FlyoutTabIds.LINK_TO_APM]: (tab: Tab) => ( - + ), [FlyoutTabIds.LINK_TO_UPTIME]: (tab: Tab) => ( - + ), }; const topCornerLinkComponents: Record = { nodeDetails: ( ), alertRule: , - apmServices: , + apmServices: , }; const tabEntries = tabs.map(({ name, ...tab }) => { @@ -77,6 +78,7 @@ export const Header = ({ tabs = [], links = [], compact }: Props) => { return ( onTabClick(tab.id)} isSelected={tab.id === activeTabId} @@ -102,7 +104,7 @@ export const Header = ({ tabs = [], links = [], compact }: Props) => { `} > - {compact ?

{node.name}

:

{node.name}

} + {compact ?

{asset.name}

:

{asset.name}

}
; } export function useAssetDetailsState({ state }: UseAssetDetailsStateProps) { - const { node, nodeType, dateRange: rawDateRange, onTabsStateChange, overrides } = state; + const { asset, assetType, dateRange: rawDateRange, onTabsStateChange, overrides } = state; const dateRange = useMemo(() => { const { from = DEFAULT_DATE_RANGE.from, to = DEFAULT_DATE_RANGE.to } = @@ -36,8 +36,8 @@ export function useAssetDetailsState({ state }: UseAssetDetailsStateProps) { const dateRangeTs = toTimestampRange(dateRange); return { - node, - nodeType, + asset, + assetType, dateRange, dateRangeTs, onTabsStateChange, diff --git a/x-pack/plugins/infra/public/components/asset_details/hooks/use_tab_switcher.tsx b/x-pack/plugins/infra/public/components/asset_details/hooks/use_tab_switcher.tsx index 60dc710b8613f..6bdcbca214d37 100644 --- a/x-pack/plugins/infra/public/components/asset_details/hooks/use_tab_switcher.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/hooks/use_tab_switcher.tsx @@ -33,6 +33,7 @@ export function useTabSwitcher({ initialActiveTabId }: TabSwitcherParams) { }; return { + initialActiveTabId, activeTabId, renderedTabsSet, showTab, diff --git a/x-pack/plugins/infra/public/components/asset_details/links/link_to_alerts.tsx b/x-pack/plugins/infra/public/components/asset_details/links/link_to_alerts.tsx index 47d7075cb60dc..e5a5cc6340abe 100644 --- a/x-pack/plugins/infra/public/components/asset_details/links/link_to_alerts.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/links/link_to_alerts.tsx @@ -15,7 +15,7 @@ export interface LinkToAlertsRuleProps { export const LinkToAlertsRule = ({ onClick }: LinkToAlertsRuleProps) => { return ( { +export const LinkToAlertsPage = ({ assetName, queryField, dateRange }: LinkToAlertsPageProps) => { const { services } = useKibanaContextForPlugin(); const { http } = services; const linkToAlertsPage = http.basePath.prepend( `${ALERTS_PATH}?_a=${encode({ - kuery: `${queryField}:"${nodeName}"`, + kuery: `${queryField}:"${assetName}"`, rangeFrom: dateRange.from, rangeTo: dateRange.to, status: 'all', @@ -35,7 +35,7 @@ export const LinkToAlertsPage = ({ nodeName, queryField, dateRange }: LinkToAler return ( { +export const LinkToApmServices = ({ assetName, apmField }: LinkToApmServicesProps) => { const { services } = useKibanaContextForPlugin(); const { http } = services; const queryString = new URLSearchParams( encode( stringify({ - kuery: `${apmField}:"${nodeName}"`, + kuery: `${apmField}:"${assetName}"`, }) ) ); @@ -34,7 +34,7 @@ export const LinkToApmServices = ({ nodeName, apmField }: LinkToApmServicesProps return ( { - const inventoryModel = findInventoryModel(nodeType); + const inventoryModel = findInventoryModel(assetType); const nodeDetailFrom = currentTimestamp - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; const nodeDetailMenuItemLinkProps = useLinkProps({ ...getNodeDetailUrl({ - nodeType, - nodeId: nodeName, + nodeType: assetType, + nodeId: assetName, from: nodeDetailFrom, to: currentTimestamp, }), @@ -37,7 +37,7 @@ export const LinkToNodeDetails = ({ return ( { +export const TabToApmTraces = ({ assetName, apmField, name, ...props }: LinkToApmServicesProps) => { const { euiTheme } = useEuiTheme(); const apmTracesMenuItemLinkProps = useLinkProps({ app: 'apm', hash: 'traces', search: { - kuery: `${apmField}:"${nodeName}"`, + kuery: `${apmField}:"${assetName}"`, }, }); @@ -30,7 +30,7 @@ export const TabToApmTraces = ({ nodeName, apmField, name, ...props }: LinkToApm { +export const TabToUptime = ({ + assetType, + assetName, + nodeIp, + name, + ...props +}: LinkToUptimeProps) => { const { share } = useKibanaContextForPlugin().services; const { euiTheme } = useEuiTheme(); return ( share.url.locators .get(uptimeOverviewLocatorID)! - .navigate({ [nodeType]: nodeName, ip: nodeIp }) + .navigate({ [assetType]: assetName, ip: nodeIp }) } > { - const { node, overrides } = useAssetDetailsStateContext(); + const { asset, overrides } = useAssetDetailsStateContext(); const { onClose = () => {} } = overrides?.anomalies ?? {}; - return ; + return ; }; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx index 087c34551c440..35337032805c1 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx @@ -22,7 +22,7 @@ import { useAssetDetailsStateContext } from '../../hooks/use_asset_details_state const TEXT_QUERY_THROTTLE_INTERVAL_MS = 500; export const Logs = () => { - const { node, nodeType, overrides, onTabsStateChange, dateRangeTs } = + const { asset, assetType, overrides, onTabsStateChange, dateRangeTs } = useAssetDetailsStateContext(); const { logView: overrideLogView, query: overrideQuery } = overrides?.logs ?? {}; @@ -49,7 +49,7 @@ export const Logs = () => { const filter = useMemo(() => { const query = [ - `${findInventoryFields(nodeType).id}: "${node.name}"`, + `${findInventoryFields(assetType).id}: "${asset.name}"`, ...(textQueryDebounced !== '' ? [textQueryDebounced] : []), ].join(' and '); @@ -57,7 +57,7 @@ export const Logs = () => { language: 'kuery', query, }; - }, [nodeType, node.name, textQueryDebounced]); + }, [assetType, asset.name, textQueryDebounced]); const onQueryChange = useCallback((e: React.ChangeEvent) => { setTextQuery(e.target.value); @@ -70,13 +70,20 @@ export const Logs = () => { const logsUrl = useMemo(() => { return locators.nodeLogsLocator.getRedirectUrl({ - nodeType, - nodeId: node.name, + nodeType: assetType, + nodeId: asset.name, time: startTimestamp, filter: textQueryDebounced, logView, }); - }, [locators.nodeLogsLocator, node.name, nodeType, startTimestamp, textQueryDebounced, logView]); + }, [ + locators.nodeLogsLocator, + asset.name, + assetType, + startTimestamp, + textQueryDebounced, + logView, + ]); return ( diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_metadata_filter_button.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_metadata_filter_button.tsx index 31d978235be32..a2d04b1c36184 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_metadata_filter_button.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_metadata_filter_button.tsx @@ -75,7 +75,7 @@ export const AddMetadataFilterButton = ({ item }: AddMetadataFilterButtonProps) color="text" iconType="filter" display="base" - data-test-subj="hostsView-flyout-metadata-remove-filter" + data-test-subj="infraAssetDetailsMetadataRemoveFilterButton" aria-label={i18n.translate('xpack.infra.metadataEmbeddable.filterAriaLabel', { defaultMessage: 'Filter', })} @@ -102,7 +102,7 @@ export const AddMetadataFilterButton = ({ item }: AddMetadataFilterButtonProps) color="primary" size="s" iconType="filter" - data-test-subj="hostsView-flyout-metadata-add-filter" + data-test-subj="infraAssetDetailsMetadataAddFilterButton" aria-label={i18n.translate('xpack.infra.metadataEmbeddable.AddFilterAriaLabel', { defaultMessage: 'Add Filter', })} diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_pin_to_row.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_pin_to_row.tsx index a1e7c3f106497..1e5e31b887911 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_pin_to_row.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_pin_to_row.tsx @@ -47,8 +47,8 @@ export const AddMetadataPinToRow = ({ size="s" color="primary" iconType="pinFilled" - data-test-subj="infraMetadataEmbeddableRemovePin" - aria-label={i18n.translate('xpack.infra.metadataEmbeddable.pinAriaLabel', { + data-test-subj="infraAssetDetailsMetadataRemovePin" + aria-label={i18n.translate('xpack.infra.metadata.pinAriaLabel', { defaultMessage: 'Pinned field', })} onClick={handleRemovePin} @@ -65,7 +65,7 @@ export const AddMetadataPinToRow = ({ color="primary" size="s" iconType="pin" - data-test-subj="infraMetadataEmbeddableAddPin" + data-test-subj="infraAssetDetailsMetadataAddPin" aria-label={PIN_FIELD} onClick={handleAddPin} /> diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.test.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.test.tsx index eeff58d30d1e7..9f8a04ef64e6c 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.test.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.test.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { Metadata } from './metadata'; - import { useMetadata } from '../../hooks/use_metadata'; import { useSourceContext } from '../../../../containers/metrics_source'; import { render } from '@testing-library/react'; @@ -27,8 +26,8 @@ const renderHostMetadata = () => from: '2023-04-09T11:07:49Z', to: '2023-04-09T11:23:49Z', }, - nodeType: 'host', - node: { + assetType: 'host', + asset: { id: 'host-1', name: 'host-1', }, @@ -69,30 +68,30 @@ describe('Single Host Metadata (Hosts View)', () => { mockUseMetadata({ error: 'Internal server error' }); const result = renderHostMetadata(); - expect(result.queryByTestId('infraMetadataErrorCallout')).toBeInTheDocument(); + expect(result.queryByTestId('infraAssetDetailsMetadataErrorCallout')).toBeInTheDocument(); }); it('should show an no data message if fetching the metadata returns an empty array', async () => { mockUseMetadata({ metadata: [] }); const result = renderHostMetadata(); - expect(result.queryByTestId('infraHostMetadataSearchBarInput')).toBeInTheDocument(); - expect(result.queryByTestId('infraHostMetadataNoData')).toBeInTheDocument(); + expect(result.queryByTestId('infraAssetDetailsMetadataSearchBarInput')).toBeInTheDocument(); + expect(result.queryByTestId('infraAssetDetailsMetadataNoData')).toBeInTheDocument(); }); it('should show the metadata table if metadata is returned', async () => { mockUseMetadata({ metadata: [{ name: 'host.os.name', value: 'Ubuntu' }] }); const result = renderHostMetadata(); - expect(result.queryByTestId('infraHostMetadataSearchBarInput')).toBeInTheDocument(); - expect(result.queryByTestId('infraMetadataTable')).toBeInTheDocument(); + expect(result.queryByTestId('infraAssetDetailsMetadataSearchBarInput')).toBeInTheDocument(); + expect(result.queryByTestId('infraAssetDetailsMetadataTable')).toBeInTheDocument(); }); it('should return loading text if loading', async () => { mockUseMetadata({ loading: true }); const result = renderHostMetadata(); - expect(result.queryByTestId('infraHostMetadataSearchBarInput')).toBeInTheDocument(); - expect(result.queryByTestId('infraHostMetadataLoading')).toBeInTheDocument(); + expect(result.queryByTestId('infraAssetDetailsMetadataSearchBarInput')).toBeInTheDocument(); + expect(result.queryByTestId('infraAssetDetailsMetadataLoading')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.tsx index 23a3e5f193409..4a759e5718d13 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.tsx @@ -24,26 +24,26 @@ export interface MetadataSearchUrlState { } export interface MetadataProps { + assetName: string; + assetType: InventoryItemType; dateRange: TimeRange; - nodeName: string; - nodeType: InventoryItemType; showActionsColumn?: boolean; search?: string; onSearchChange?: (query: string) => void; } export const Metadata = () => { - const { node, nodeType, overrides, dateRangeTs, onTabsStateChange } = + const { asset, assetType, overrides, dateRangeTs, onTabsStateChange } = useAssetDetailsStateContext(); const { query, showActionsColumn = false } = overrides?.metadata ?? {}; - const inventoryModel = findInventoryModel(nodeType); + const inventoryModel = findInventoryModel(assetType); const { sourceId } = useSourceContext(); const { loading: metadataLoading, error: fetchMetadataError, metadata, - } = useMetadata(node.name, nodeType, inventoryModel.requiredMetrics, sourceId, dateRangeTs); + } = useMetadata(asset.name, assetType, inventoryModel.requiredMetrics, sourceId, dateRangeTs); const fields = useMemo(() => getAllFields(metadata), [metadata]); @@ -64,7 +64,7 @@ export const Metadata = () => { })} color="danger" iconType="error" - data-test-subj="infraMetadataErrorCallout" + data-test-subj="infraAssetDetailsMetadataErrorCallout" > {LOADING} +
{LOADING}
) : ( -
{NO_METADATA_FOUND}
+
{NO_METADATA_FOUND}
) } /> diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/osquery/osquery.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/osquery/osquery.tsx index 06640540af16d..b18a2f802e085 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/osquery/osquery.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/osquery/osquery.tsx @@ -14,12 +14,12 @@ import { useMetadata } from '../../hooks/use_metadata'; import { useAssetDetailsStateContext } from '../../hooks/use_asset_details_state'; export const Osquery = () => { - const { node, nodeType, dateRangeTs } = useAssetDetailsStateContext(); - const inventoryModel = findInventoryModel(nodeType); + const { asset, assetType, dateRangeTs } = useAssetDetailsStateContext(); + const inventoryModel = findInventoryModel(assetType); const { sourceId } = useSourceContext(); const { loading, metadata } = useMetadata( - node.name, - nodeType, + asset.name, + assetType, inventoryModel.requiredMetrics, sourceId, dateRangeTs diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx index 87ebaae1f5f20..2edac4abbbdda 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx @@ -25,12 +25,12 @@ import { useBoolean } from '../../../../hooks/use_boolean'; import { ALERT_STATUS_ALL } from '../../../../common/alerts/constants'; export const AlertsSummaryContent = ({ - nodeName, - nodeType, + assetName, + assetType, dateRange, }: { - nodeName: string; - nodeType: InventoryItemType; + assetName: string; + assetType: InventoryItemType; dateRange: TimeRange; }) => { const [isAlertFlyoutVisible, { toggle: toggleAlertFlyout }] = useBoolean(false); @@ -39,10 +39,10 @@ export const AlertsSummaryContent = ({ () => createAlertsEsQuery({ dateRange, - hostNodeNames: [nodeName], + hostNodeNames: [assetName], status: ALERT_STATUS_ALL, }), - [nodeName, dateRange] + [assetName, dateRange] ); return ( @@ -56,8 +56,8 @@ export const AlertsSummaryContent = ({
@@ -65,8 +65,8 @@ export const AlertsSummaryContent = ({ @@ -112,7 +112,7 @@ const AlertsSectionTitle = () => { return ( - +
{ diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpis/kpi_grid.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpis/kpi_grid.tsx index b86201a29098c..dfc7f8823ba71 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpis/kpi_grid.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpis/kpi_grid.tsx @@ -4,21 +4,55 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; + +import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { Tile, type TileProps } from './tile'; -import { KPI_CHARTS } from '../../../../../common/visualizations/lens/dashboards/host/kpi_grid_config'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { TimeRange } from '@kbn/es-query'; +import { LensChart, TooltipContent } from '../../../../lens'; +import { buildCombinedHostsFilter } from '../../../../../utils/filters/build'; +import { + KPI_CHARTS, + KPI_CHART_HEIGHT, + AVERAGE_SUBTITLE, +} from '../../../../../common/visualizations/lens/dashboards/host/kpi_grid_config'; + +interface Props { + dataView?: DataView; + nodeName: string; + timeRange: TimeRange; +} + +export const KPIGrid = React.memo(({ nodeName, dataView, timeRange }: Props) => { + const filters = useMemo(() => { + return [ + buildCombinedHostsFilter({ + field: 'host.name', + values: [nodeName], + dataView, + }), + ]; + }, [dataView, nodeName]); -export const KPIGrid = React.memo(({ nodeName, dataView, timeRange: dateRange }: TileProps) => { return ( - <> - - {KPI_CHARTS.map((chartProp, index) => ( - - - - ))} - - + + {KPI_CHARTS.map(({ id, layers, title, toolTip }, index) => ( + + } + visualizationType="lnsMetric" + disableTriggers + hidePanelTitles + /> + + ))} + ); }); diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpis/tile.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpis/tile.tsx deleted file mode 100644 index 9907b81d64fc0..0000000000000 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpis/tile.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { useMemo } from 'react'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import { i18n } from '@kbn/i18n'; -import { EuiIcon, EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; -import styled from 'styled-components'; -import type { Action } from '@kbn/ui-actions-plugin/public'; -import { TimeRange } from '@kbn/es-query'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { LensWrapper, TooltipContent } from '../../../../lens'; -import type { KPIChartProps } from '../../../../../common/visualizations/lens/dashboards/host/kpi_grid_config'; -import { useLensAttributes } from '../../../../../hooks/use_lens_attributes'; -import { buildCombinedHostsFilter } from '../../../../../utils/filters/build'; - -const MIN_HEIGHT = 150; - -export interface TileProps { - timeRange: TimeRange; - dataView?: DataView; - nodeName: string; -} - -export const Tile = ({ - id, - layers, - title, - toolTip, - dataView, - nodeName, - timeRange, -}: KPIChartProps & TileProps) => { - const getSubtitle = () => - i18n.translate('xpack.infra.assetDetailsEmbeddable.overview.metricTrend.subtitle.average', { - defaultMessage: 'Average', - }); - - const { formula, attributes, getExtraActions, error } = useLensAttributes({ - dataView, - title, - layers: { ...layers, options: { ...layers.options, subtitle: getSubtitle() } }, - visualizationType: 'lnsMetric', - }); - - const filters = useMemo(() => { - return [ - buildCombinedHostsFilter({ - field: 'host.name', - values: [nodeName], - dataView, - }), - ]; - }, [dataView, nodeName]); - - const extraActions: Action[] = useMemo( - () => - getExtraActions({ - timeRange, - filters, - }), - [filters, getExtraActions, timeRange] - ); - - const loading = !attributes; - - return ( - - {error ? ( - - - - - - - - - - - ) : ( - } - anchorClassName="eui-fullWidth" - > - - - )} - - ); -}; - -const EuiPanelStyled = styled(EuiPanel)` - min-height: ${MIN_HEIGHT}px; - .echMetric { - border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; - pointer-events: none; - } -`; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_header.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_header.tsx index d3a0001f06db1..66f2c3585d62d 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_header.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_header.tsx @@ -55,7 +55,7 @@ export const MetadataHeader = ({ metadataValue }: MetadataSummaryProps) => { @@ -72,7 +72,7 @@ export const MetadataHeader = ({ metadataValue }: MetadataSummaryProps) => { values={{ documentation: ( diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_summary_list.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_summary_list.tsx index 1aedec3f05037..ee7210dd2d8de 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_summary_list.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_summary_list.tsx @@ -78,7 +78,7 @@ export const MetadataSummaryList = ({ metadata, metadataLoading }: MetadataSumma { - title: string; - layers: Array>; - dataView?: DataView; - timeRange: TimeRange; - nodeName: string; -} - -const MIN_HEIGHT = 250; - -export const MetricChart = ({ - id, - title, - layers, - nodeName, - timeRange, - dataView, - overrides, -}: MetricChartProps) => { - const { euiTheme } = useEuiTheme(); - - const { attributes, getExtraActions, error } = useLensAttributes({ - dataView, - layers, - title, - visualizationType: 'lnsXY', - }); - - const filters = useMemo(() => { - return [ - buildCombinedHostsFilter({ - field: 'host.name', - values: [nodeName], - dataView, - }), - ]; - }, [dataView, nodeName]); - - const extraActions: Action[] = useMemo( - () => - getExtraActions({ - timeRange, - filters, - }), - [timeRange, filters, getExtraActions] - ); - - const loading = !attributes; - - return ( - - {error ? ( - - - - - - - - - - - ) : ( - - )} - - ); -}; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx index 9a62fe1d2fe72..5f0bce1b54856 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx @@ -4,18 +4,30 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { EuiFlexGrid, EuiFlexItem, EuiTitle, EuiSpacer, EuiFlexGroup } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { DataView } from '@kbn/data-views-plugin/public'; -import { TimeRange } from '@kbn/es-query'; +import type { TimeRange } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; -import { HostMetricsDocsLink } from '../../../../lens'; -import { MetricChart, type MetricChartProps } from './metric_chart'; -import { hostLensFormulas } from '../../../../../common/visualizations'; +import { buildCombinedHostsFilter } from '../../../../../utils/filters/build'; +import type { Layer } from '../../../../../hooks/use_lens_attributes'; +import { HostMetricsDocsLink, LensChart, type LensChartProps } from '../../../../lens'; +import { + type FormulaConfig, + hostLensFormulas, + type XYLayerOptions, +} from '../../../../../common/visualizations'; +import { METRIC_CHART_HEIGHT } from '../../../constants'; -const PERCENT_LEFT_AXIS: Pick['overrides'] = { +type DataViewOrigin = 'logs' | 'metrics'; +interface MetricChartConfig extends Pick { + layers: Array>; + toolTip: string; +} + +const PERCENT_LEFT_AXIS: Pick['overrides'] = { axisLeft: { domain: { min: 0, @@ -24,7 +36,7 @@ const PERCENT_LEFT_AXIS: Pick['overrides'] = { }, }; -const LEGEND_SETTINGS: Pick['overrides'] = { +const LEGEND_SETTINGS: Pick['overrides'] = { settings: { showLegend: true, legendPosition: 'bottom', @@ -33,8 +45,8 @@ const LEGEND_SETTINGS: Pick['overrides'] = { }; const CHARTS_IN_ORDER: Array< - Pick & { - dataViewType: 'logs' | 'metrics'; + Pick & { + dataViewOrigin: DataViewOrigin; } > = [ { @@ -49,7 +61,7 @@ const CHARTS_IN_ORDER: Array< layerType: 'data', }, ], - dataViewType: 'metrics', + dataViewOrigin: 'metrics', overrides: { axisLeft: PERCENT_LEFT_AXIS.axisLeft, }, @@ -65,7 +77,7 @@ const CHARTS_IN_ORDER: Array< layerType: 'data', }, ], - dataViewType: 'metrics', + dataViewOrigin: 'metrics', overrides: { axisLeft: PERCENT_LEFT_AXIS.axisLeft, }, @@ -96,7 +108,7 @@ const CHARTS_IN_ORDER: Array< layerType: 'referenceLine', }, ], - dataViewType: 'metrics', + dataViewOrigin: 'metrics', }, { id: 'logRate', @@ -109,7 +121,7 @@ const CHARTS_IN_ORDER: Array< layerType: 'data', }, ], - dataViewType: 'logs', + dataViewOrigin: 'logs', }, { id: 'diskSpaceUsageAvailable', @@ -126,7 +138,7 @@ const CHARTS_IN_ORDER: Array< }), }, { - ...hostLensFormulas.diskSpaceAvailable, + ...hostLensFormulas.diskSpaceAvailability, label: i18n.translate( 'xpack.infra.assetDetails.metricsCharts.diskSpace.label.available', { @@ -152,7 +164,7 @@ const CHARTS_IN_ORDER: Array< axisLeft: PERCENT_LEFT_AXIS.axisLeft, settings: LEGEND_SETTINGS.settings, }, - dataViewType: 'metrics', + dataViewOrigin: 'metrics', }, { id: 'diskThroughputReadWrite', @@ -163,13 +175,13 @@ const CHARTS_IN_ORDER: Array< { data: [ { - ...hostLensFormulas.diskReadThroughput, + ...hostLensFormulas.diskIORead, label: i18n.translate('xpack.infra.assetDetails.metricsCharts.metric.label.read', { defaultMessage: 'Read', }), }, { - ...hostLensFormulas.diskWriteThroughput, + ...hostLensFormulas.diskIOWrite, label: i18n.translate('xpack.infra.assetDetails.metricsCharts.metric.label.write', { defaultMessage: 'Write', }), @@ -184,7 +196,7 @@ const CHARTS_IN_ORDER: Array< overrides: { settings: LEGEND_SETTINGS.settings, }, - dataViewType: 'metrics', + dataViewOrigin: 'metrics', }, { id: 'diskIOReadWrite', @@ -195,13 +207,13 @@ const CHARTS_IN_ORDER: Array< { data: [ { - ...hostLensFormulas.diskIORead, + ...hostLensFormulas.diskReadThroughput, label: i18n.translate('xpack.infra.assetDetails.metricsCharts.metric.label.read', { defaultMessage: 'Read', }), }, { - ...hostLensFormulas.diskIOWrite, + ...hostLensFormulas.diskWriteThroughput, label: i18n.translate('xpack.infra.assetDetails.metricsCharts.metric.label.write', { defaultMessage: 'Write', }), @@ -216,7 +228,7 @@ const CHARTS_IN_ORDER: Array< overrides: { settings: LEGEND_SETTINGS.settings, }, - dataViewType: 'metrics', + dataViewOrigin: 'metrics', }, { id: 'rxTx', @@ -248,7 +260,7 @@ const CHARTS_IN_ORDER: Array< overrides: { settings: LEGEND_SETTINGS.settings, }, - dataViewType: 'metrics', + dataViewOrigin: 'metrics', }, ]; @@ -259,8 +271,35 @@ export interface MetricsGridProps { logsDataView?: DataView; } +export interface MetricsGridProps { + nodeName: string; + timeRange: TimeRange; + metricsDataView?: DataView; + logsDataView?: DataView; +} + export const MetricsGrid = React.memo( ({ nodeName, metricsDataView, logsDataView, timeRange }: MetricsGridProps) => { + const getDataView = useCallback( + (dataViewOrigin: DataViewOrigin) => { + return dataViewOrigin === 'metrics' ? metricsDataView : logsDataView; + }, + [logsDataView, metricsDataView] + ); + + const getFilters = useCallback( + (dataViewOrigin: DataViewOrigin) => { + return [ + buildCombinedHostsFilter({ + field: 'host.name', + values: [nodeName], + dataView: getDataView(dataViewOrigin), + }), + ]; + }, + [getDataView, nodeName] + ); + return ( @@ -276,14 +315,25 @@ export const MetricsGrid = React.memo( - - {CHARTS_IN_ORDER.map(({ dataViewType, ...chartProp }, index) => ( + + {CHARTS_IN_ORDER.map(({ dataViewOrigin, id, layers, title, overrides }, index) => ( - ))} diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/overview.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/overview.tsx index 3a9a642675993..5485a76a71dd3 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/overview.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/overview.tsx @@ -21,18 +21,18 @@ import { toTimestampRange } from '../../utils'; import { useAssetDetailsStateContext } from '../../hooks/use_asset_details_state'; export const Overview = () => { - const { node, nodeType, overrides, dateRange } = useAssetDetailsStateContext(); + const { asset, assetType, overrides, dateRange } = useAssetDetailsStateContext(); const { logsDataView, metricsDataView } = overrides?.overview ?? {}; - const inventoryModel = findInventoryModel(nodeType); + const inventoryModel = findInventoryModel(assetType); const { sourceId } = useSourceContext(); const { loading: metadataLoading, error: fetchMetadataError, metadata, } = useMetadata( - node.name, - nodeType, + asset.name, + assetType, inventoryModel.requiredMetrics, sourceId, toTimestampRange(dateRange) @@ -41,7 +41,7 @@ export const Overview = () => { return ( - + {fetchMetadataError ? ( @@ -59,7 +59,7 @@ export const Overview = () => { values={{ reload: ( window.location.reload()} > {i18n.translate('xpack.infra.assetDetailsEmbeddable.overview.errorAction', { @@ -76,7 +76,7 @@ export const Overview = () => { - + @@ -84,7 +84,7 @@ export const Overview = () => { timeRange={dateRange} logsDataView={logsDataView} metricsDataView={metricsDataView} - nodeName={node.name} + nodeName={asset.name} /> diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx index b91c96c3c948e..7bb8e96276fd8 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx @@ -35,7 +35,7 @@ const options = Object.entries(STATE_NAMES).map(([value, view]: [string, string] })); export const Processes = () => { - const { node, nodeType, overrides, dateRangeTs, onTabsStateChange } = + const { asset, assetType, overrides, dateRangeTs, onTabsStateChange } = useAssetDetailsStateContext(); const { query: overrideQuery } = overrides?.processes ?? {}; @@ -52,9 +52,9 @@ export const Processes = () => { }); const hostTerm = useMemo(() => { - const field = getFieldByType(nodeType) ?? nodeType; - return { [field]: node.name }; - }, [node.name, nodeType]); + const field = getFieldByType(assetType) ?? assetType; + return { [field]: asset.name }; + }, [asset.name, assetType]); const { loading, @@ -159,7 +159,7 @@ export const Processes = () => { } actions={ - + {columns.map((column) => ( diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/summary_table.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/summary_table.tsx index 57814aa7bacc2..928f48307eee7 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/summary_table.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/summary_table.tsx @@ -59,7 +59,10 @@ export const SummaryTable = ({ processSummary, isLoading }: Props) => { {Object.entries(processCount).map(([field, value]) => ( - + {columnTitles[field as keyof SummaryRecord]} {value === -1 ? : value} diff --git a/x-pack/plugins/infra/public/components/asset_details/types.ts b/x-pack/plugins/infra/public/components/asset_details/types.ts index ebd3c8823b8ca..cbf66d66f0a27 100644 --- a/x-pack/plugins/infra/public/components/asset_details/types.ts +++ b/x-pack/plugins/infra/public/components/asset_details/types.ts @@ -13,7 +13,7 @@ import type { InventoryItemType } from '../../../common/inventory_models/types'; interface Metadata { ip?: string | null; } -export type Node = Metadata & { +export type Asset = Metadata & { id: string; name: string; }; @@ -60,11 +60,11 @@ export interface TabState { export interface FlyoutProps { closeFlyout: () => void; - showInFlyout: true; + mode: 'flyout'; } export interface FullPageProps { - showInFlyout: false; + mode: 'page'; } export type RenderMode = FlyoutProps | FullPageProps; @@ -72,14 +72,13 @@ export type RenderMode = FlyoutProps | FullPageProps; export interface Tab { id: FlyoutTabIds; name: string; - 'data-test-subj': string; } export type LinkOptions = 'alertRule' | 'nodeDetails' | 'apmServices'; export interface AssetDetailsProps { - node: Node; - nodeType: InventoryItemType; + asset: Asset; + assetType: InventoryItemType; dateRange: TimeRange; tabs: Tab[]; activeTabId?: TabIds; diff --git a/x-pack/plugins/infra/public/components/lens/chart_load_error.tsx b/x-pack/plugins/infra/public/components/lens/chart_load_error.tsx new file mode 100644 index 0000000000000..5ffdc573a2cd7 --- /dev/null +++ b/x-pack/plugins/infra/public/components/lens/chart_load_error.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; + +export const ChartLoadError = () => { + return ( + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/lens/chart_placeholder.tsx b/x-pack/plugins/infra/public/components/lens/chart_placeholder.tsx index 7bf35f0e0392a..2dfcbfa21c814 100644 --- a/x-pack/plugins/infra/public/components/lens/chart_placeholder.tsx +++ b/x-pack/plugins/infra/public/components/lens/chart_placeholder.tsx @@ -6,8 +6,7 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiProgress, EuiFlexItem, EuiLoadingChart } from '@elastic/eui'; -import { useEuiTheme } from '@elastic/eui'; +import { EuiFlexGroup, EuiProgress, EuiFlexItem, EuiLoadingChart, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; export const ChartLoadingProgress = ({ hasTopMargin = false }: { hasTopMargin?: boolean }) => { diff --git a/x-pack/plugins/infra/public/components/lens/index.tsx b/x-pack/plugins/infra/public/components/lens/index.tsx index 17a2f5b480442..93d050209a219 100644 --- a/x-pack/plugins/infra/public/components/lens/index.tsx +++ b/x-pack/plugins/infra/public/components/lens/index.tsx @@ -5,8 +5,7 @@ * 2.0. */ +export { LensChart, type LensChartProps } from './lens_chart'; export { ChartPlaceholder } from './chart_placeholder'; -export { LensWrapper } from './lens_wrapper'; - export { TooltipContent } from './metric_explanation/tooltip_content'; export { HostMetricsDocsLink } from './metric_explanation/host_metrics_docs_link'; diff --git a/x-pack/plugins/infra/public/components/lens/lens_chart.tsx b/x-pack/plugins/infra/public/components/lens/lens_chart.tsx new file mode 100644 index 0000000000000..2d4b599d56de2 --- /dev/null +++ b/x-pack/plugins/infra/public/components/lens/lens_chart.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { CSSProperties, useMemo } from 'react'; +import { EuiPanel, EuiToolTip, type EuiPanelProps } from '@elastic/eui'; +import { Action } from '@kbn/ui-actions-plugin/public'; +import { css } from '@emotion/react'; +import { useLensAttributes, type UseLensAttributesParams } from '../../hooks/use_lens_attributes'; +import type { BaseChartProps } from './types'; +import type { TooltipContentProps } from './metric_explanation/tooltip_content'; +import { LensWrapper } from './lens_wrapper'; +import { ChartLoadError } from './chart_load_error'; + +const MIN_HEIGHT = 300; + +export type LensChartProps = UseLensAttributesParams & + BaseChartProps & + Pick & { + toolTip?: React.ReactElement; + }; + +export const LensChart = ({ + id, + borderRadius, + dateRange, + filters, + hidePanelTitles, + lastReloadRequestTime, + query, + onBrushEnd, + overrides, + toolTip, + disableTriggers = false, + height = MIN_HEIGHT, + loading = false, + ...lensAttributesParams +}: LensChartProps) => { + const { formula, attributes, getExtraActions, error } = useLensAttributes({ + ...lensAttributesParams, + }); + + const isLoading = loading || !attributes; + + const extraActions: Action[] = useMemo( + () => + getExtraActions({ + timeRange: dateRange, + query, + filters, + }), + [dateRange, filters, getExtraActions, query] + ); + + const sytle: CSSProperties = useMemo(() => ({ height }), [height]); + + const Lens = ( + + ); + + const getContent = () => { + if (!toolTip) { + return Lens; + } + + return ( + + {Lens} + + ); + }; + + return ( + + {error ? : getContent()} + + ); +}; diff --git a/x-pack/plugins/infra/public/components/lens/lens_wrapper.tsx b/x-pack/plugins/infra/public/components/lens/lens_wrapper.tsx index dc3c11dccacc0..f203c9c344797 100644 --- a/x-pack/plugins/infra/public/components/lens/lens_wrapper.tsx +++ b/x-pack/plugins/infra/public/components/lens/lens_wrapper.tsx @@ -9,7 +9,8 @@ import type { Action } from '@kbn/ui-actions-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import type { TimeRange } from '@kbn/es-query'; import { TypedLensByValueInput } from '@kbn/lens-plugin/public'; -import { euiStyled } from '@kbn/kibana-react-plugin/common'; +import { css } from '@emotion/react'; +import { useEuiTheme } from '@elastic/eui'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; import { ChartLoadingProgress, ChartPlaceholder } from './chart_placeholder'; import { parseDateRange } from '../../utils/datemath'; @@ -30,10 +31,11 @@ export const LensWrapper = ({ dateRange, filters, lastReloadRequestTime, - loading, + loading = false, query, ...props }: LensWrapperProps) => { + const { euiTheme } = useEuiTheme(); const [intersectionObserverEntry, setIntersectionObserverEntry] = useState(); const [embeddableLoaded, setEmbeddableLoaded] = useState(false); @@ -96,11 +98,25 @@ export const LensWrapper = ({ return { from, to }; }, [state.dateRange]); - const isLoading = loading || !state.attributes; return ( - +
<> {isLoading && !embeddableLoaded ? ( @@ -120,7 +136,7 @@ export const LensWrapper = ({ )} - +
); }; @@ -142,13 +158,3 @@ const EmbeddableComponentMemo = React.memo( return ; } ); - -const Container = euiStyled.div` - position: relative; - border-radius: ${({ theme }) => theme.eui.euiSizeS}; - overflow: hidden; - height: 100%; - .echLegend .echLegendList { - display: flex; - } -`; diff --git a/x-pack/plugins/infra/public/components/lens/metric_explanation/tooltip_content.tsx b/x-pack/plugins/infra/public/components/lens/metric_explanation/tooltip_content.tsx index fd46700130ee4..52459e70e2b05 100644 --- a/x-pack/plugins/infra/public/components/lens/metric_explanation/tooltip_content.tsx +++ b/x-pack/plugins/infra/public/components/lens/metric_explanation/tooltip_content.tsx @@ -11,14 +11,14 @@ import { css } from '@emotion/react'; import { FormattedMessage } from '@kbn/i18n-react'; import { HOST_METRICS_DOC_HREF } from '../../../common/visualizations/constants'; -interface Props extends Pick, 'style'> { +export interface TooltipContentProps extends Pick, 'style'> { description: string; formula?: string; showDocumentationLink?: boolean; } export const TooltipContent = React.memo( - ({ description, formula, showDocumentationLink = false, style }: Props) => { + ({ description, formula, showDocumentationLink = false, style }: TooltipContentProps) => { const onClick = (e: React.MouseEvent) => { e.stopPropagation(); }; diff --git a/x-pack/plugins/infra/public/components/lens/types.ts b/x-pack/plugins/infra/public/components/lens/types.ts new file mode 100644 index 0000000000000..8a4791de8cdf5 --- /dev/null +++ b/x-pack/plugins/infra/public/components/lens/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { LensWrapperProps } from './lens_wrapper'; + +export type BaseChartProps = Pick< + LensWrapperProps, + | 'id' + | 'dateRange' + | 'disableTriggers' + | 'filters' + | 'hidePanelTitles' + | 'lastReloadRequestTime' + | 'loading' + | 'overrides' + | 'onBrushEnd' + | 'query' + | 'title' +> & { + dataView?: DataView; + height?: number; +}; diff --git a/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts b/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts index 721bf5669d203..6f3ae0cf37aff 100644 --- a/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts @@ -68,7 +68,9 @@ interface UseLensAttributesMetricChartParams visualizationType: 'lnsMetric'; } -type UseLensAttributesParams = UseLensAttributesXYChartParams | UseLensAttributesMetricChartParams; +export type UseLensAttributesParams = + | UseLensAttributesXYChartParams + | UseLensAttributesMetricChartParams; export const useLensAttributes = ({ dataView, diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx index 97be988142aa0..e78aae020b46b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx @@ -6,8 +6,8 @@ */ import React, { useEffect, useRef, CSSProperties } from 'react'; import { Chart, Metric, type MetricWNumber, type MetricWTrend } from '@elastic/charts'; -import { EuiPanel, EuiToolTip } from '@elastic/eui'; -import styled from 'styled-components'; +import { EuiPanel, EuiToolTip, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; import { ChartPlaceholder } from '../../../../../components/lens'; export interface Props extends Pick { @@ -16,11 +16,11 @@ export interface Props extends Pick { + const euiTheme = useEuiTheme(); const loadedOnce = useRef(false); useEffect(() => { @@ -42,7 +42,7 @@ export const MetricChartWrapper = React.memo( }; return ( - + {loading && !loadedOnce.current ? ( ) : ( @@ -52,19 +52,20 @@ export const MetricChartWrapper = React.memo( content={toolTip} anchorClassName="eui-fullWidth" > - + - + )} ); } ); - -const KPIChartStyled = styled(Chart)` - .echMetric { - border-radius: ${(p) => p.theme.eui.euiBorderRadius}; - pointer-events: none; - } -`; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout_wrapper.tsx index a1d8542130c14..884e0dd389cd3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout_wrapper.tsx @@ -7,7 +7,6 @@ import React from 'react'; import useAsync from 'react-use/lib/useAsync'; -import type { InventoryItemType } from '../../../../../../common/inventory_models/types'; import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; import type { HostNodeRow } from '../../hooks/use_hosts_table'; import { HostFlyout, useHostFlyoutUrlState } from '../../hooks/use_host_flyout_url_state'; @@ -21,8 +20,6 @@ export interface Props { closeFlyout: () => void; } -const NODE_TYPE = 'host' as InventoryItemType; - export const FlyoutWrapper = ({ node, closeFlyout }: Props) => { const { searchCriteria } = useUnifiedSearchContext(); const { dataView } = useMetricsDataViewContext(); @@ -39,8 +36,8 @@ export const FlyoutWrapper = ({ node, closeFlyout }: Props) => { return ( { tabs={orderedFlyoutTabs} links={['apmServices', 'nodeDetails']} renderMode={{ - showInFlyout: true, + mode: 'flyout', closeFlyout, }} /> diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/tabs.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/tabs.ts index 4445e5fba924a..7d354d19bed12 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/tabs.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/tabs.ts @@ -14,41 +14,35 @@ export const orderedFlyoutTabs: Tab[] = [ name: i18n.translate('xpack.infra.nodeDetails.tabs.overview.title', { defaultMessage: 'Overview', }), - 'data-test-subj': 'hostsView-flyout-tabs-overview', }, { id: FlyoutTabIds.METADATA, name: i18n.translate('xpack.infra.nodeDetails.tabs.metadata.title', { defaultMessage: 'Metadata', }), - 'data-test-subj': 'hostsView-flyout-tabs-metadata', }, { id: FlyoutTabIds.PROCESSES, name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.processes', { defaultMessage: 'Processes', }), - 'data-test-subj': 'hostsView-flyout-tabs-processes', }, { id: FlyoutTabIds.LOGS, name: i18n.translate('xpack.infra.nodeDetails.tabs.logs.title', { defaultMessage: 'Logs', }), - 'data-test-subj': 'hostsView-flyout-tabs-logs', }, { id: FlyoutTabIds.ANOMALIES, name: i18n.translate('xpack.infra.nodeDetails.tabs.anomalies', { defaultMessage: 'Anomalies', }), - 'data-test-subj': 'hostsView-flyout-tabs-anomalies', }, { id: FlyoutTabIds.OSQUERY, name: i18n.translate('xpack.infra.nodeDetails.tabs.osquery', { defaultMessage: 'Osquery', }), - 'data-test-subj': 'hostsView-flyout-tabs-Osquery', }, ]; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx index 0571733b80034..f38e6772a3c84 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx @@ -6,7 +6,6 @@ */ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { KPIChartProps } from '../../../../../common/visualizations/lens/dashboards/host/kpi_grid_config'; import { hostLensFormulas } from '../../../../../common/visualizations'; import { useHostCountContext } from '../../hooks/use_host_count'; import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; @@ -16,21 +15,20 @@ import { type Props, MetricChartWrapper } from '../chart/metric_chart_wrapper'; import { TooltipContent } from '../../../../../components/lens'; const HOSTS_CHART: Omit = { - id: `metric-hostCount`, + id: 'hostsViewKPI-hostsCount', color: '#6DCCB1', - title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.title', { + title: i18n.translate('xpack.infra.hostsViewPage.kpi.hostCount.title', { defaultMessage: 'Hosts', }), - ['data-test-subj']: 'hostsViewKPI-hostsCount', }; -export const HostsTile = ({ style }: Pick) => { +export const HostsTile = ({ height }: { height: number }) => { const { data: hostCountData, isRequestRunning: hostCountLoading } = useHostCountContext(); const { searchCriteria } = useUnifiedSearchContext(); const getSubtitle = () => { return searchCriteria.limit < (hostCountData?.count.value ?? 0) - ? i18n.translate('xpack.infra.hostsViewPage.metricTrend.subtitle.hostCount.limit', { + ? i18n.translate('xpack.infra.hostsViewPage.kpi.subtitle.hostCount.limit', { defaultMessage: 'Limited to {limit}', values: { limit: searchCriteria.limit, @@ -42,7 +40,7 @@ export const HostsTile = ({ style }: Pick) => { return ( { return ( @@ -26,11 +24,11 @@ export const KPIGrid = () => { - + {KPI_CHARTS.map((chartProp, index) => ( - + ))} diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx index 950e74478dd4b..7211c89ce9071 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx @@ -4,53 +4,47 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useMemo, useCallback } from 'react'; - +import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { BrushTriggerEvent } from '@kbn/charts-plugin/public'; -import { EuiIcon, EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; -import styled from 'styled-components'; -import { Action } from '@kbn/ui-actions-plugin/public'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { LensWrapper, TooltipContent } from '../../../../../components/lens'; -import { KPIChartProps } from '../../../../../common/visualizations/lens/dashboards/host/kpi_grid_config'; +import { LensChart, TooltipContent } from '../../../../../components/lens'; +import { + type KPIChartProps, + AVERAGE_SUBTITLE, +} from '../../../../../common/visualizations/lens/dashboards/host/kpi_grid_config'; import { buildCombinedHostsFilter } from '../../../../../utils/filters/build'; -import { useLensAttributes } from '../../../../../hooks/use_lens_attributes'; import { useMetricsDataViewContext } from '../../hooks/use_data_view'; import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; import { useHostsViewContext } from '../../hooks/use_hosts_view'; import { useHostCountContext } from '../../hooks/use_host_count'; import { useAfterLoadedState } from '../../hooks/use_after_loaded_state'; -import { KPI_CHART_MIN_HEIGHT } from '../../constants'; -export const Tile = ({ id, title, layers, style, toolTip }: KPIChartProps) => { - const { searchCriteria, onSubmit } = useUnifiedSearchContext(); +export const Tile = ({ + id, + title, + layers, + toolTip, + height, +}: KPIChartProps & { height: number }) => { + const { searchCriteria } = useUnifiedSearchContext(); const { dataView } = useMetricsDataViewContext(); const { requestTs, hostNodes, loading: hostsLoading } = useHostsViewContext(); const { data: hostCountData, isRequestRunning: hostCountLoading } = useHostCountContext(); const shouldUseSearchCriteria = hostNodes.length === 0; + const loading = hostsLoading || hostCountLoading; + const getSubtitle = () => { return searchCriteria.limit < (hostCountData?.count.value ?? 0) - ? i18n.translate('xpack.infra.hostsViewPage.metricTrend.subtitle.average.limit', { + ? i18n.translate('xpack.infra.hostsViewPage.kpi.subtitle.average.limit', { defaultMessage: 'Average (of {limit} hosts)', values: { limit: searchCriteria.limit, }, }) - : i18n.translate('xpack.infra.hostsViewPage.metricTrend.subtitle.average', { - defaultMessage: 'Average', - }); + : AVERAGE_SUBTITLE; }; - const { formula, attributes, getExtraActions, error } = useLensAttributes({ - dataView, - title, - layers: { ...layers, options: { ...layers.options, subtitle: getSubtitle() } }, - visualizationType: 'lnsMetric', - }); - const filters = useMemo(() => { return shouldUseSearchCriteria ? searchCriteria.filters @@ -61,106 +55,33 @@ export const Tile = ({ id, title, layers, style, toolTip }: KPIChartProps) => { dataView, }), ]; - }, [shouldUseSearchCriteria, searchCriteria.filters, hostNodes, dataView]); - - const loading = hostsLoading || !attributes || hostCountLoading; + }, [dataView, hostNodes, searchCriteria.filters, shouldUseSearchCriteria]); - // prevents requestTs and serchCriteria states from reloading the chart - // we want it to reload only once the host count and table have finished loading + // prevents requestTs and searchCriteria state from reloading the chart + // we want it to reload only once the table has finished loading const { afterLoadedState } = useAfterLoadedState(loading, { - attributes, lastReloadRequestTime: requestTs, - ...searchCriteria, + dateRange: searchCriteria.dateRange, + query: shouldUseSearchCriteria ? searchCriteria.query : undefined, filters, }); - const extraActions: Action[] = useMemo( - () => - getExtraActions({ - timeRange: afterLoadedState.dateRange, - query: shouldUseSearchCriteria ? afterLoadedState.query : undefined, - filters, - }), - [ - afterLoadedState.dateRange, - afterLoadedState.query, - filters, - getExtraActions, - shouldUseSearchCriteria, - ] - ); - - const handleBrushEnd = useCallback( - ({ range }: BrushTriggerEvent['data']) => { - const [min, max] = range; - onSubmit({ - dateRange: { - from: new Date(min).toISOString(), - to: new Date(max).toISOString(), - mode: 'absolute', - }, - }); - }, - [onSubmit] - ); - return ( - - {error ? ( - - - - - - - - - - - ) : ( - } - anchorClassName="eui-fullWidth" - > -
- -
-
- )} -
+ } + visualizationType="lnsMetric" + disableTriggers + hidePanelTitles + /> ); }; - -const EuiPanelStyled = styled(EuiPanel)` - min-height: ${KPI_CHART_MIN_HEIGHT}px; - .echMetric { - border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; - pointer-events: none; - } -`; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx index b31e5c1cb08d2..c0f05c55d1e9e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx @@ -4,59 +4,35 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { CSSProperties, useCallback, useMemo } from 'react'; -import { Action } from '@kbn/ui-actions-plugin/public'; -import { BrushTriggerEvent } from '@kbn/charts-plugin/public'; -import { EuiIcon, EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { TypedLensByValueInput } from '@kbn/lens-plugin/public'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { LensWrapper } from '../../../../../../components/lens'; -import { useLensAttributes, Layer } from '../../../../../../hooks/use_lens_attributes'; +import React, { useMemo } from 'react'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; +import { LensChart } from '../../../../../../components/lens'; +import type { Layer } from '../../../../../../hooks/use_lens_attributes'; import { useMetricsDataViewContext } from '../../../hooks/use_data_view'; import { useUnifiedSearchContext } from '../../../hooks/use_unified_search'; -import { FormulaConfig, XYLayerOptions } from '../../../../../../common/visualizations'; +import type { FormulaConfig, XYLayerOptions } from '../../../../../../common/visualizations'; import { useHostsViewContext } from '../../../hooks/use_hosts_view'; import { buildCombinedHostsFilter } from '../../../../../../utils/filters/build'; import { useHostsTableContext } from '../../../hooks/use_hosts_table'; import { useAfterLoadedState } from '../../../hooks/use_after_loaded_state'; -import { METRIC_CHART_MIN_HEIGHT } from '../../../constants'; +import { METRIC_CHART_HEIGHT } from '../../../constants'; export interface MetricChartProps extends Pick { title: string; layers: Array>; } -const lensStyle: CSSProperties = { - height: METRIC_CHART_MIN_HEIGHT, -}; - export const MetricChart = ({ id, title, layers, overrides }: MetricChartProps) => { - const { euiTheme } = useEuiTheme(); - const { searchCriteria, onSubmit } = useUnifiedSearchContext(); + const { searchCriteria } = useUnifiedSearchContext(); const { dataView } = useMetricsDataViewContext(); const { requestTs, loading } = useHostsViewContext(); const { currentPage } = useHostsTableContext(); const shouldUseSearchCriteria = currentPage.length === 0; - // prevents requestTs and serchCriteria states from reloading the chart - // we want it to reload only once the table has finished loading - const { afterLoadedState } = useAfterLoadedState(loading, { - lastReloadRequestTime: requestTs, - ...searchCriteria, - }); - - const { attributes, getExtraActions, error } = useLensAttributes({ - dataView, - layers, - title, - visualizationType: 'lnsXY', - }); - const filters = useMemo(() => { return shouldUseSearchCriteria - ? afterLoadedState.filters + ? searchCriteria.filters : [ buildCombinedHostsFilter({ field: 'host.name', @@ -64,85 +40,31 @@ export const MetricChart = ({ id, title, layers, overrides }: MetricChartProps) dataView, }), ]; - }, [afterLoadedState.filters, currentPage, dataView, shouldUseSearchCriteria]); - - const extraActions: Action[] = useMemo( - () => - getExtraActions({ - timeRange: afterLoadedState.dateRange, - query: shouldUseSearchCriteria ? afterLoadedState.query : undefined, - filters, - }), - [ - afterLoadedState.dateRange, - afterLoadedState.query, - filters, - getExtraActions, - shouldUseSearchCriteria, - ] - ); + }, [searchCriteria.filters, currentPage, dataView, shouldUseSearchCriteria]); - const handleBrushEnd = useCallback( - ({ range }: BrushTriggerEvent['data']) => { - const [min, max] = range; - onSubmit({ - dateRange: { - from: new Date(min).toISOString(), - to: new Date(max).toISOString(), - mode: 'absolute', - }, - }); - }, - [onSubmit] - ); + // prevents requestTs and searchCriteria state from reloading the chart + // we want it to reload only once the table has finished loading + const { afterLoadedState } = useAfterLoadedState(loading, { + lastReloadRequestTime: requestTs, + dateRange: searchCriteria.dateRange, + query: shouldUseSearchCriteria ? searchCriteria.query : undefined, + }); return ( - - {error ? ( - - - - - - - - - - - ) : ( - - )} - + dataView={dataView} + dateRange={afterLoadedState.dateRange} + height={METRIC_CHART_HEIGHT} + layers={layers} + lastReloadRequestTime={afterLoadedState.lastReloadRequestTime} + loading={loading} + filters={filters} + query={afterLoadedState.query} + title={title} + overrides={overrides} + visualizationType="lnsXY" + /> ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts index 296d779e8a0fd..aace07448692e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts @@ -15,8 +15,7 @@ export const DEFAULT_PAGE_SIZE = 10; export const LOCAL_STORAGE_HOST_LIMIT_KEY = 'hostsView:hostLimitSelection'; export const LOCAL_STORAGE_PAGE_SIZE_KEY = 'hostsView:pageSizeSelection'; -export const KPI_CHART_MIN_HEIGHT = 150; -export const METRIC_CHART_MIN_HEIGHT = 300; +export const METRIC_CHART_HEIGHT = 300; export const HOST_LIMIT_OPTIONS = [50, 100, 500] as const; export const HOST_METRICS_DOC_HREF = 'https://ela.st/docs-infra-host-metrics'; diff --git a/x-pack/plugins/infra/public/services/telemetry/telemetry_client.mock.ts b/x-pack/plugins/infra/public/services/telemetry/telemetry_client.mock.ts index 3f913bd8d5611..604fdcc272493 100644 --- a/x-pack/plugins/infra/public/services/telemetry/telemetry_client.mock.ts +++ b/x-pack/plugins/infra/public/services/telemetry/telemetry_client.mock.ts @@ -13,4 +13,5 @@ export const createTelemetryClientMock = (): jest.Mocked => ({ reportHostFlyoutFilterRemoved: jest.fn(), reportHostFlyoutFilterAdded: jest.fn(), reportHostsViewTotalHostCountRetrieved: jest.fn(), + reportAssetDetailsFlyoutViewed: jest.fn(), }); diff --git a/x-pack/plugins/infra/public/services/telemetry/telemetry_client.ts b/x-pack/plugins/infra/public/services/telemetry/telemetry_client.ts index 56eb8d1af2c77..9107157c9835d 100644 --- a/x-pack/plugins/infra/public/services/telemetry/telemetry_client.ts +++ b/x-pack/plugins/infra/public/services/telemetry/telemetry_client.ts @@ -7,6 +7,7 @@ import { AnalyticsServiceSetup } from '@kbn/core-analytics-server'; import { + AssetDetailsFlyoutViewedParams, HostEntryClickedParams, HostFlyoutFilterActionParams, HostsViewQueryHostsCountRetrievedParams, @@ -60,4 +61,8 @@ export class TelemetryClient implements ITelemetryClient { params ); } + + public reportAssetDetailsFlyoutViewed = (params: AssetDetailsFlyoutViewedParams) => { + this.analytics.reportEvent(InfraTelemetryEventTypes.ASSET_DETAILS_FLYOUT_VIEWED, params); + }; } diff --git a/x-pack/plugins/infra/public/services/telemetry/telemetry_events.ts b/x-pack/plugins/infra/public/services/telemetry/telemetry_events.ts index 4d55baf61674b..56cce313ec219 100644 --- a/x-pack/plugins/infra/public/services/telemetry/telemetry_events.ts +++ b/x-pack/plugins/infra/public/services/telemetry/telemetry_events.ts @@ -112,7 +112,35 @@ const hostViewTotalHostCountRetrieved: InfraTelemetryEvent = { }, }; +const assetDetailsFlyoutViewed: InfraTelemetryEvent = { + eventType: InfraTelemetryEventTypes.ASSET_DETAILS_FLYOUT_VIEWED, + schema: { + componentName: { + type: 'keyword', + _meta: { + description: 'Hostname for the clicked host.', + optional: false, + }, + }, + assetType: { + type: 'keyword', + _meta: { + description: 'Cloud provider for the clicked host.', + optional: false, + }, + }, + tabId: { + type: 'keyword', + _meta: { + description: 'Cloud provider for the clicked host.', + optional: true, + }, + }, + }, +}; + export const infraTelemetryEvents = [ + assetDetailsFlyoutViewed, hostsViewQuerySubmittedEvent, hostsEntryClickedEvent, hostFlyoutRemoveFilter, diff --git a/x-pack/plugins/infra/public/services/telemetry/telemetry_service.test.ts b/x-pack/plugins/infra/public/services/telemetry/telemetry_service.test.ts index 6e2030a3fdaf3..b3c4b02468ca6 100644 --- a/x-pack/plugins/infra/public/services/telemetry/telemetry_service.test.ts +++ b/x-pack/plugins/infra/public/services/telemetry/telemetry_service.test.ts @@ -183,4 +183,28 @@ describe('TelemetryService', () => { ); }); }); + + describe('#reportAssetDetailsFlyoutViewed', () => { + it('should report asset details viewed with properties', async () => { + const setupParams = getSetupParams(); + service.setup(setupParams); + const telemetry = service.start(); + + telemetry.reportAssetDetailsFlyoutViewed({ + componentName: 'infraAssetDetailsFlyout', + assetType: 'host', + tabId: 'overview', + }); + + expect(setupParams.analytics.reportEvent).toHaveBeenCalledTimes(1); + expect(setupParams.analytics.reportEvent).toHaveBeenCalledWith( + InfraTelemetryEventTypes.ASSET_DETAILS_FLYOUT_VIEWED, + { + componentName: 'infraAssetDetailsFlyout', + assetType: 'host', + tabId: 'overview', + } + ); + }); + }); }); diff --git a/x-pack/plugins/infra/public/services/telemetry/types.ts b/x-pack/plugins/infra/public/services/telemetry/types.ts index 0ccd6c0633b4a..2ecf8115eaa58 100644 --- a/x-pack/plugins/infra/public/services/telemetry/types.ts +++ b/x-pack/plugins/infra/public/services/telemetry/types.ts @@ -18,6 +18,7 @@ export enum InfraTelemetryEventTypes { HOST_FLYOUT_FILTER_REMOVED = 'Host Flyout Filter Removed', HOST_FLYOUT_FILTER_ADDED = 'Host Flyout Filter Added', HOST_VIEW_TOTAL_HOST_COUNT_RETRIEVED = 'Host View Total Host Count Retrieved', + ASSET_DETAILS_FLYOUT_VIEWED = 'Asset Details Flyout Viewed', } export interface HostsViewQuerySubmittedParams { @@ -41,11 +42,18 @@ export interface HostsViewQueryHostsCountRetrievedParams { total: number; } +export interface AssetDetailsFlyoutViewedParams { + assetType: string; + componentName: string; + tabId?: string; +} + export type InfraTelemetryEventParams = | HostsViewQuerySubmittedParams | HostEntryClickedParams | HostFlyoutFilterActionParams - | HostsViewQueryHostsCountRetrievedParams; + | HostsViewQueryHostsCountRetrievedParams + | AssetDetailsFlyoutViewedParams; export interface ITelemetryClient { reportHostEntryClicked(params: HostEntryClickedParams): void; @@ -53,6 +61,7 @@ export interface ITelemetryClient { reportHostFlyoutFilterAdded(params: HostFlyoutFilterActionParams): void; reportHostsViewTotalHostCountRetrieved(params: HostsViewQueryHostsCountRetrievedParams): void; reportHostsViewQuerySubmitted(params: HostsViewQuerySubmittedParams): void; + reportAssetDetailsFlyoutViewed(params: AssetDetailsFlyoutViewedParams): void; } export type InfraTelemetryEvent = @@ -75,4 +84,8 @@ export type InfraTelemetryEvent = | { eventType: InfraTelemetryEventTypes.HOST_VIEW_TOTAL_HOST_COUNT_RETRIEVED; schema: RootSchema; + } + | { + eventType: InfraTelemetryEventTypes.ASSET_DETAILS_FLYOUT_VIEWED; + schema: RootSchema; }; diff --git a/x-pack/plugins/lens/public/data_views_service/loader.test.ts b/x-pack/plugins/lens/public/data_views_service/loader.test.ts index e7e2bab166a70..f91d236986b11 100644 --- a/x-pack/plugins/lens/public/data_views_service/loader.test.ts +++ b/x-pack/plugins/lens/public/data_views_service/loader.test.ts @@ -159,6 +159,65 @@ describe('loader', () => { expect(cache.foo.getFieldByName('timestamp')!.meta).toEqual(true); }); + it('should move over any time series meta data', async () => { + const cache = await loadIndexPatterns({ + cache: {}, + patterns: ['foo'], + dataViews: { + get: jest.fn(async () => ({ + id: 'foo', + title: 'Foo index', + metaFields: ['timestamp'], + isPersisted: () => true, + toSpec: () => ({}), + typeMeta: {}, + fields: [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes_counter', + displayName: 'bytes_counter', + type: 'number', + aggregatable: true, + searchable: true, + timeSeriesMetric: 'counter', + }, + { + name: 'bytes_gauge', + displayName: 'bytes_gauge', + type: 'number', + aggregatable: true, + searchable: true, + timeSeriesMetric: 'gauge', + }, + { + name: 'dimension', + displayName: 'dimension', + type: 'string', + aggregatable: true, + searchable: true, + timeSeriesDimension: true, + }, + ], + })), + getIdsWithTitle: jest.fn(async () => ({ + id: 'foo', + title: 'Foo index', + })), + create: jest.fn(), + } as unknown as Pick, + }); + + expect(cache.foo.getFieldByName('bytes_counter')!.timeSeriesMetric).toEqual('counter'); + expect(cache.foo.getFieldByName('bytes_gauge')!.timeSeriesMetric).toEqual('gauge'); + expect(cache.foo.getFieldByName('dimension')!.timeSeriesDimension).toEqual(true); + }); + it('should call the refresh callback when loading new indexpatterns', async () => { const onIndexPatternRefresh = jest.fn(); await loadIndexPatterns({ diff --git a/x-pack/plugins/lens/public/data_views_service/loader.ts b/x-pack/plugins/lens/public/data_views_service/loader.ts index f38a84cc60b48..784c97d832e34 100644 --- a/x-pack/plugins/lens/public/data_views_service/loader.ts +++ b/x-pack/plugins/lens/public/data_views_service/loader.ts @@ -29,6 +29,7 @@ export function convertDataViewIntoLensIndexPattern( dataView: DataView, restrictionRemapper: (name: string) => string = onRestrictionMapping ): IndexPattern { + const metaKeys = new Set(dataView.metaFields); const newFields = dataView.fields .filter(isFieldLensCompatible) .map((field): IndexPatternField => { @@ -40,13 +41,14 @@ export function convertDataViewIntoLensIndexPattern( aggregatable: field.aggregatable, filterable: field.filterable, searchable: field.searchable, - meta: dataView.metaFields.includes(field.name), + meta: metaKeys.has(field.name), esTypes: field.esTypes, scripted: field.scripted, isMapped: field.isMapped, customLabel: field.customLabel, runtimeField: field.runtimeField, runtime: Boolean(field.runtimeField), + timeSeriesDimension: field.timeSeriesDimension, timeSeriesMetric: field.timeSeriesMetric, timeSeriesRollup: field.isRolledUpField, partiallyApplicableFunctions: field.isRolledUpField diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_input.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_input.test.tsx index 1a22e34e0a3ec..d307e0eb094a2 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_input.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_input.test.tsx @@ -17,12 +17,15 @@ import { FieldBasedIndexPatternColumn, termsOperation, staticValueOperation, + minOperation, } from '../operations/definitions'; import { FieldInput, getErrorMessage } from './field_input'; -import { createMockedIndexPattern } from '../mocks'; +import { createMockedIndexPattern, createMockedIndexPatternWithAdditionalFields } from '../mocks'; import { getOperationSupportMatrix } from '.'; import { GenericIndexPatternColumn, FormBasedLayer, FormBasedPrivateState } from '../types'; import { ReferenceBasedIndexPatternColumn } from '../operations/definitions/column_types'; +import { FieldSelect } from './field_select'; +import { IndexPattern, VisualizationDimensionGroupConfig } from '../../../types'; jest.mock('../operations/layer_helpers', () => { const original = jest.requireActual('../operations/layer_helpers'); @@ -41,7 +44,7 @@ const defaultProps = { incompleteField: null, incompleteOperation: undefined, incompleteParams: {}, - dimensionGroups: [], + dimensionGroups: [] as VisualizationDimensionGroupConfig[], groupId: 'any', operationDefinitionMap: { terms: termsOperation, @@ -49,6 +52,7 @@ const defaultProps = { count: countOperation, differences: derivativeOperation, staticValue: staticValueOperation, + min: minOperation, } as unknown as Record, }; @@ -102,9 +106,12 @@ function getCountOperationColumn(): GenericIndexPatternColumn { operationType: 'count', }; } -function getLayer(col1: GenericIndexPatternColumn = getStringBasedOperationColumn()) { +function getLayer( + col1: GenericIndexPatternColumn = getStringBasedOperationColumn(), + indexPattern?: IndexPattern +) { return { - indexPatternId: '1', + indexPatternId: defaultProps.indexPattern.id, columnOrder: ['col1', 'col2'], columns: { col1, @@ -112,7 +119,11 @@ function getLayer(col1: GenericIndexPatternColumn = getStringBasedOperationColum }, }; } -function getDefaultOperationSupportMatrix(layer: FormBasedLayer, columnId: string) { +function getDefaultOperationSupportMatrix( + layer: FormBasedLayer, + columnId: string, + indexPattern?: IndexPattern +) { return getOperationSupportMatrix({ state: { layers: { layer1: layer }, @@ -121,7 +132,7 @@ function getDefaultOperationSupportMatrix(layer: FormBasedLayer, columnId: strin filterOperations: () => true, columnId, indexPatterns: { - [defaultProps.indexPattern.id]: defaultProps.indexPattern, + [defaultProps.indexPattern.id]: indexPattern ?? defaultProps.indexPattern, }, }); } @@ -421,6 +432,80 @@ describe('FieldInput', () => { expect(onDeleteColumn).toHaveBeenCalled(); expect(updateLayerSpy).not.toHaveBeenCalled(); }); + + describe('time series group', () => { + function getLayerWithTSDBMetric() { + const layer = getLayer(); + layer.columns.col2 = { + label: 'Min of TSDB counter', + dataType: 'number', + isBucketed: false, + sourceField: 'bytes_counter', + operationType: 'min', + }; + return layer; + } + it('should not render the time dimension category if it has tsdb metric column but the group is not a breakdown', () => { + const updateLayerSpy = jest.fn(); + const layer = getLayerWithTSDBMetric(); + const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1'); + const instance = mount( + + ); + + expect(instance.find(FieldSelect).prop('showTimeSeriesDimensions')).toBeFalsy(); + }); + + it('should render the time dimension category if it has tsdb metric column and the group is a breakdown one', () => { + const updateLayerSpy = jest.fn(); + const layer = getLayerWithTSDBMetric(); + const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1'); + const instance = mount( + + ); + + expect(instance.find(FieldSelect).prop('showTimeSeriesDimensions')).toBeTruthy(); + }); + }); }); describe('getErrorMessage', () => { diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_input.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_input.tsx index 462cd0b546f22..5120c89c37b30 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_input.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_input.tsx @@ -16,6 +16,7 @@ import type { GenericIndexPatternColumn, } from '../operations/definitions'; import type { FieldBasedIndexPatternColumn } from '../operations/definitions/column_types'; +import { shouldShowTimeSeriesOption } from '../pure_utils'; export function FieldInput({ layer, @@ -83,6 +84,12 @@ export function FieldInput({ }) ); }} + showTimeSeriesDimensions={shouldShowTimeSeriesOption( + layer, + indexPattern, + groupId, + dimensionGroups + )} /> ); diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_select.tsx index cbfafbfbfd2c9..909bc64781399 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/field_select.tsx @@ -33,6 +33,7 @@ export interface FieldSelectProps extends EuiComboBoxProps { @@ -66,7 +68,15 @@ export function FieldSelect({ return fieldContainsData(fieldName, currentIndexPattern, hasFieldData); } - function fieldNamesToOptions(items: string[]) { + interface FieldOption { + label: string; + value: { type: 'field'; field: string; dataType: string | undefined; operationType: string }; + exists: boolean; + compatible: number; + 'data-test-subj': string; + } + + function fieldNamesToOptions(items: string[]): FieldOption[] { return items .filter((field) => currentIndexPattern.getFieldByName(field)?.displayName) .map((field) => { @@ -75,9 +85,9 @@ export function FieldSelect({ const exists = containsData(field); const fieldInstance = currentIndexPattern.getFieldByName(field); return { - label: currentIndexPattern.getFieldByName(field)?.displayName, + label: currentIndexPattern.getFieldByName(field)?.displayName ?? field, value: { - type: 'field', + type: 'field' as const, field, dataType: fieldInstance ? getFieldType(fieldInstance) : undefined, // Use the operation directly, or choose the first compatible operation. @@ -102,21 +112,47 @@ export function FieldSelect({ ); const [availableFields, emptyFields] = partition(nonMetaFields, containsData); - const constructFieldsOptions = (fieldsArr: string[], label: string) => + const constructFieldsOptions = ( + fieldsArr: string[], + label: string + ): { label: string; options: FieldOption[] } | false => fieldsArr.length > 0 && { label, options: fieldNamesToOptions(fieldsArr), }; - const availableFieldsOptions = constructFieldsOptions( + const isTimeSeriesFields = (field: string) => { + return ( + showTimeSeriesDimensions && currentIndexPattern.getFieldByName(field)?.timeSeriesDimension + ); + }; + + const [availableTimeSeriesFields, availableNonTimeseriesFields] = partition( availableFields, + isTimeSeriesFields + ); + const [emptyTimeSeriesFields, emptyNonTimeseriesFields] = partition( + emptyFields, + isTimeSeriesFields + ); + + const timeSeriesFieldsOptions = constructFieldsOptions( + // This group includes both available and empty fields + availableTimeSeriesFields.concat(emptyTimeSeriesFields), + i18n.translate('xpack.lens.indexPattern.timeSeriesFieldsLabel', { + defaultMessage: 'Time series dimensions', + }) + ); + + const availableFieldsOptions = constructFieldsOptions( + availableNonTimeseriesFields, i18n.translate('xpack.lens.indexPattern.availableFieldsLabel', { defaultMessage: 'Available fields', }) ); const emptyFieldsOptions = constructFieldsOptions( - emptyFields, + emptyNonTimeseriesFields, i18n.translate('xpack.lens.indexPattern.emptyFieldsLabel', { defaultMessage: 'Empty fields', }) @@ -131,17 +167,19 @@ export function FieldSelect({ return [ ...fieldNamesToOptions(specialFields), + timeSeriesFieldsOptions, availableFieldsOptions, emptyFieldsOptions, metaFieldsOptions, ].filter(Boolean); }, [ + operationByField, incompleteOperation, selectedOperationType, currentIndexPattern, - operationByField, hasFieldData, markAllFieldsCompatible, + showTimeSeriesDimensions, ]); return ( diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/reference_editor.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/reference_editor.tsx index 28810825dd7a0..66a01daae0581 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/reference_editor.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/reference_editor.tsx @@ -317,6 +317,7 @@ export const ReferenceEditor = (props: ReferenceEditorProps) => { markAllFieldsCompatible={selectionStyle === 'field'} onDeleteColumn={onDeleteColumn} onChoose={onChooseField} + showTimeSeriesDimensions={false} /> ) : null} diff --git a/x-pack/plugins/lens/public/datasources/form_based/mocks.ts b/x-pack/plugins/lens/public/datasources/form_based/mocks.ts index c01495f1993e4..fcefa97ecd4b1 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/mocks.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/mocks.ts @@ -216,3 +216,15 @@ export const createMockedIndexPatternWithoutType = ( getFieldByName: getFieldByNameFactory(filteredFields), }; }; + +export const createMockedIndexPatternWithAdditionalFields = ( + newFields: IndexPatternField[] +): IndexPattern => { + const { fields, ...otherIndexPatternProps } = createMockedIndexPattern(); + const completeFields = fields.concat(newFields); + return { + ...otherIndexPatternProps, + fields: completeFields, + getFieldByName: getFieldByNameFactory(completeFields), + }; +}; diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/field_inputs.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/field_inputs.tsx index 72941739b24ed..428747b6cda75 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/field_inputs.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/field_inputs.tsx @@ -30,6 +30,7 @@ export interface FieldInputsProps { invalidFields?: string[]; operationSupportMatrix: Pick; onChange: (newValues: string[]) => void; + showTimeSeriesDimensions: boolean; } interface WrappedValue { @@ -50,6 +51,7 @@ export function FieldInputs({ indexPattern, operationSupportMatrix, invalidFields, + showTimeSeriesDimensions, }: FieldInputsProps) { const onChangeWrapped = useCallback( (values: WrappedValue[]) => @@ -165,6 +167,7 @@ export function FieldInputs({ data-test-subj={ localValues.length !== 1 ? `indexPattern-dimension-field-${index}` : undefined } + showTimeSeriesDimensions={localValues.length < 2 && showTimeSeriesDimensions} /> ); diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/index.tsx index 504abd15955e0..4aafe38ea39ee 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/index.tsx @@ -53,6 +53,7 @@ import { supportedTypes, } from './constants'; import { IncludeExcludeRow } from './include_exclude_options'; +import { shouldShowTimeSeriesOption } from '../../../pure_utils'; export function supportsRarityRanking(field?: IndexPatternField) { // these es field types can't be sorted by rarity @@ -591,6 +592,12 @@ export const termsOperation: OperationDefinition< operationSupportMatrix={operationSupportMatrix} onChange={onFieldSelectChange} invalidFields={invalidFields} + showTimeSeriesDimensions={shouldShowTimeSeriesOption( + layer, + indexPattern, + groupId, + dimensionGroups + )} /> ); diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/values_input.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/values_input.tsx index 33dd5b7206008..a776227986d68 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/values_input.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/values_input.tsx @@ -94,6 +94,7 @@ export const ValuesInput = ({ const inputNumber = Number(inputValue); setInputValue(String(Math.min(maxValue, Math.max(inputNumber, minValue)))); }} + data-test-subj={'indexPattern-terms-values'} /> ); diff --git a/x-pack/plugins/lens/public/datasources/form_based/pure_utils.ts b/x-pack/plugins/lens/public/datasources/form_based/pure_utils.ts index b13fd50a45e73..256c1de51f832 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/pure_utils.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/pure_utils.ts @@ -5,7 +5,12 @@ * 2.0. */ -import type { DataType, IndexPattern, IndexPatternField } from '../../types'; +import type { + DataType, + IndexPattern, + IndexPatternField, + VisualizationDimensionGroupConfig, +} from '../../types'; import type { FormBasedLayer } from './types'; import type { BaseIndexPatternColumn, @@ -26,6 +31,28 @@ export function hasField(column: BaseIndexPatternColumn): column is FieldBasedIn return 'sourceField' in column; } +export function shouldShowTimeSeriesOption( + layer: FormBasedLayer, + indexPattern: IndexPattern, + groupId: string, + dimensionGroups: VisualizationDimensionGroupConfig[] +) { + return Boolean( + dimensionGroups.find(({ groupId: id }) => groupId === id)?.isBreakdownDimension && + containsColumnWithTimeSeriesMetric(layer, indexPattern) + ); +} + +function containsColumnWithTimeSeriesMetric( + layer: FormBasedLayer, + indexPattern: IndexPattern +): boolean { + return Object.values(layer.columns).some( + (column) => + hasField(column) && indexPattern.getFieldByName(column.sourceField)?.timeSeriesMetric + ); +} + export function getFieldType(field: IndexPatternField) { if (field.timeSeriesMetric) { return field.timeSeriesMetric; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index 18d0833ed4250..6514e13d65a11 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -530,7 +530,9 @@ describe('workspace_panel', () => { const onEvent = expressionRendererMock.mock.calls[0][0].onEvent!; - const eventData = { myData: true, table: { rows: [], columns: [] }, column: 0 }; + const eventData = { + data: [{ table: { rows: [], columns: [] }, cells: [{ column: 0, row: 0 }] }], + }; onEvent({ name: 'multiFilter', data: eventData }); expect(uiActionsMock.getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.multiFilter); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 3f92236c99e5d..02db9e18919f7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -434,7 +434,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ plugins.uiActions.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ data: { ...event.data, - timeFieldName: inferTimeField(plugins.data.datatableUtilities, event.data), + timeFieldName: inferTimeField(plugins.data.datatableUtilities, event), }, }); } @@ -442,7 +442,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ plugins.uiActions.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ data: { ...event.data, - timeFieldName: inferTimeField(plugins.data.datatableUtilities, event.data), + timeFieldName: inferTimeField(plugins.data.datatableUtilities, event), }, }); } diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 8741d73a4a7b4..67dea2f98231c 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -1227,45 +1227,35 @@ export class Embeddable if (!this.deps.getTrigger || this.input.disableTriggers) { return; } + + let eventHandler: + | LensBaseEmbeddableInput['onBrushEnd'] + | LensBaseEmbeddableInput['onFilter'] + | LensBaseEmbeddableInput['onTableRowClick']; + let shouldExecuteDefaultTriggers = true; + if (isLensBrushEvent(event)) { - let shouldExecuteDefaultTriggers = true; - if (this.input.onBrushEnd) { - this.input.onBrushEnd({ - ...event.data, - preventDefault: () => { - shouldExecuteDefaultTriggers = false; - }, - }); - } - if (shouldExecuteDefaultTriggers) { - this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ - data: { - ...event.data, - timeFieldName: - event.data.timeFieldName || - inferTimeField(this.deps.data.datatableUtilities, event.data), - }, - embeddable: this, - }); - } + eventHandler = this.input.onBrushEnd; + } else if (isLensFilterEvent(event) || isLensMultiFilterEvent(event)) { + eventHandler = this.input.onFilter; + } else if (isLensTableRowContextMenuClickEvent(event)) { + eventHandler = this.input.onTableRowClick; } - if (isLensFilterEvent(event) || isLensMultiFilterEvent(event)) { - let shouldExecuteDefaultTriggers = true; - if (this.input.onFilter) { - this.input.onFilter({ - ...event.data, - preventDefault: () => { - shouldExecuteDefaultTriggers = false; - }, - }); - } + + eventHandler?.({ + ...event.data, + preventDefault: () => { + shouldExecuteDefaultTriggers = false; + }, + }); + + if (isLensFilterEvent(event) || isLensMultiFilterEvent(event) || isLensBrushEvent(event)) { if (shouldExecuteDefaultTriggers) { this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ data: { ...event.data, timeFieldName: - event.data.timeFieldName || - inferTimeField(this.deps.data.datatableUtilities, event.data), + event.data.timeFieldName || inferTimeField(this.deps.data.datatableUtilities, event), }, embeddable: this, }); @@ -1273,15 +1263,6 @@ export class Embeddable } if (isLensTableRowContextMenuClickEvent(event)) { - let shouldExecuteDefaultTriggers = true; - if (this.input.onTableRowClick) { - this.input.onTableRowClick({ - ...event.data, - preventDefault: () => { - shouldExecuteDefaultTriggers = false; - }, - }); - } if (shouldExecuteDefaultTriggers) { this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec( { diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 3b989366fa307..13651c3bc5e69 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -814,6 +814,7 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { supportStaticValue?: boolean; // used by text based datasource to restrict the field selection only to number fields for the metric dimensions isMetricDimension?: boolean; + isBreakdownDimension?: boolean; paramEditorCustomProps?: ParamEditorCustomProps; enableFormatSelector?: boolean; labels?: { buttonAriaLabel: string; buttonLabel: string }; @@ -1331,6 +1332,12 @@ export interface LensTableRowContextMenuEvent { data: RowClickContext['data']; } +export type TriggerEvent = + | BrushTriggerEvent + | ClickTriggerEvent + | MultiClickTriggerEvent + | LensTableRowContextMenuEvent; + export function isLensFilterEvent(event: ExpressionRendererEvent): event is ClickTriggerEvent { return event.name === 'filter'; } diff --git a/x-pack/plugins/lens/public/utils.test.ts b/x-pack/plugins/lens/public/utils.test.ts index 52e509557b2fc..e775059586aff 100644 --- a/x-pack/plugins/lens/public/utils.test.ts +++ b/x-pack/plugins/lens/public/utils.test.ts @@ -56,9 +56,12 @@ describe('utils', () => { test('infer time field for brush event', () => { expect( inferTimeField(datatableUtilities, { - table, - column: 0, - range: [1, 2], + name: 'brush', + data: { + table, + column: 0, + range: [1, 2], + }, }) ).toEqual('abc'); }); @@ -66,9 +69,12 @@ describe('utils', () => { test('do not return time field if time range is not bound', () => { expect( inferTimeField(datatableUtilities, { - table: tableWithoutAppliedTimeRange, - column: 0, - range: [1, 2], + name: 'brush', + data: { + table: tableWithoutAppliedTimeRange, + column: 0, + range: [1, 2], + }, }) ).toEqual(undefined); }); @@ -76,14 +82,17 @@ describe('utils', () => { test('infer time field for click event', () => { expect( inferTimeField(datatableUtilities, { - data: [ - { - table, - column: 0, - row: 0, - value: 1, - }, - ], + name: 'filter', + data: { + data: [ + { + table, + column: 0, + row: 0, + value: 1, + }, + ], + }, }) ).toEqual('abc'); }); @@ -91,15 +100,18 @@ describe('utils', () => { test('do not return time field for negated click event', () => { expect( inferTimeField(datatableUtilities, { - data: [ - { - table, - column: 0, - row: 0, - value: 1, - }, - ], - negate: true, + name: 'filter', + data: { + data: [ + { + table, + column: 0, + row: 0, + value: 1, + }, + ], + negate: true, + }, }) ).toEqual(undefined); }); @@ -107,14 +119,17 @@ describe('utils', () => { test('do not return time field for click event without bound time field', () => { expect( inferTimeField(datatableUtilities, { - data: [ - { - table: tableWithoutAppliedTimeRange, - column: 0, - row: 0, - value: 1, - }, - ], + name: 'filter', + data: { + data: [ + { + table: tableWithoutAppliedTimeRange, + column: 0, + row: 0, + value: 1, + }, + ], + }, }) ).toEqual(undefined); }); diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 446d7aac37e66..b1deface2cd77 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -14,11 +14,6 @@ import type { TimefilterContract } from '@kbn/data-plugin/public'; import type { IUiSettingsClient, SavedObjectReference } from '@kbn/core/public'; import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public'; import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; -import { - BrushTriggerEvent, - ClickTriggerEvent, - MultiClickTriggerEvent, -} from '@kbn/charts-plugin/public'; import { emptyTitleText } from '@kbn/visualization-ui-components'; import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { ISearchStart } from '@kbn/data-plugin/public'; @@ -34,6 +29,10 @@ import { DragDropOperation, isOperation, UserMessage, + TriggerEvent, + isLensBrushEvent, + isLensMultiFilterEvent, + isLensFilterEvent, } from './types'; import type { DatasourceStates, VisualizationState } from './state_management'; import type { IndexPatternServiceAPI } from './data_views_service/service'; @@ -214,17 +213,28 @@ export function getRemoveOperation( return layerCount === 1 ? 'clear' : 'remove'; } -export function inferTimeField( - datatableUtilities: DatatableUtilitiesService, - context: BrushTriggerEvent['data'] | ClickTriggerEvent['data'] | MultiClickTriggerEvent['data'] -) { - const tablesAndColumns = - 'table' in context - ? [{ table: context.table, column: context.column }] - : !context.negate - ? context.data - : // if it's a negated filter, never respect bound time field - []; +function getTablesAndColumnsFromContext(event: TriggerEvent) { + // if it's a negated filter, never respect bound time field + if ('negate' in event.data && event.data.negate) { + return []; + } + if (isLensBrushEvent(event)) { + return [{ table: event.data.table, column: event.data.column }]; + } + if (isLensMultiFilterEvent(event)) { + return event.data.data.map(({ table, cells }) => ({ + table, + column: cells[0].column, + })); + } + if (isLensFilterEvent(event)) { + return event.data.data; + } + return event.data; +} + +export function inferTimeField(datatableUtilities: DatatableUtilitiesService, event: TriggerEvent) { + const tablesAndColumns = getTablesAndColumnsFromContext(event); return !Array.isArray(tablesAndColumns) ? [tablesAndColumns] : tablesAndColumns diff --git a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.test.ts index 596c334aeac9e..dc9202a4d7e92 100644 --- a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.test.ts @@ -150,6 +150,7 @@ describe('heatmap', () => { groupLabel: 'Vertical axis', accessors: [{ columnId: 'y-accessor' }], filterOperations: filterOperationsAxis, + isBreakdownDimension: true, supportsMoreColumns: false, requiredMinDimensionCount: 0, dataTestSubj: 'lnsHeatmap_yDimensionPanel', @@ -210,6 +211,7 @@ describe('heatmap', () => { accessors: [], filterOperations: filterOperationsAxis, supportsMoreColumns: true, + isBreakdownDimension: true, requiredMinDimensionCount: 0, dataTestSubj: 'lnsHeatmap_yDimensionPanel', }, @@ -267,6 +269,7 @@ describe('heatmap', () => { accessors: [{ columnId: 'y-accessor' }], filterOperations: filterOperationsAxis, supportsMoreColumns: false, + isBreakdownDimension: true, requiredMinDimensionCount: 0, dataTestSubj: 'lnsHeatmap_yDimensionPanel', }, diff --git a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx index 654d69284718c..5d19c402a27e2 100644 --- a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx @@ -197,6 +197,7 @@ export const getHeatmapVisualization = ({ filterOperations: filterOperationsAxis, supportsMoreColumns: !state.yAccessor, requiredMinDimensionCount: 0, + isBreakdownDimension: true, dataTestSubj: 'lnsHeatmap_yDimensionPanel', }, { diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index 0d000db6e5206..9f5f9755d1781 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -461,6 +461,7 @@ export const getXyVisualization = ({ requiredMinDimensionCount: dataLayer.seriesType.includes('percentage') && hasOnlyOneAccessor ? 1 : 0, enableDimensionEditor: true, + isBreakdownDimension: true, }, ], }; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/transform_request.ts b/x-pack/plugins/maps/public/connected_components/mb_map/transform_request.ts index 47f14956d6dca..c30fc533a322f 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/transform_request.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/transform_request.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import { FONTS_API_PATH, MVT_GETTILE_API_PATH, @@ -22,7 +25,10 @@ export function transformRequest(url: string, resourceType: string | undefined) return { url, method: 'GET' as 'GET', - headers: { [ELASTIC_HTTP_VERSION_HEADER]: '1' }, + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + [X_ELASTIC_INTERNAL_ORIGIN_REQUEST]: 'kibana', + }, }; } @@ -30,7 +36,10 @@ export function transformRequest(url: string, resourceType: string | undefined) return { url, method: 'GET' as 'GET', - headers: { [ELASTIC_HTTP_VERSION_HEADER]: '1' }, + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + [X_ELASTIC_INTERNAL_ORIGIN_REQUEST]: 'kibana', + }, }; } @@ -38,7 +47,10 @@ export function transformRequest(url: string, resourceType: string | undefined) return { url, method: 'GET' as 'GET', - headers: { [ELASTIC_HTTP_VERSION_HEADER]: '1' }, + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + [X_ELASTIC_INTERNAL_ORIGIN_REQUEST]: 'kibana', + }, }; } diff --git a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts index 72a6100c15a7c..c5325f4aa7614 100644 --- a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts +++ b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts @@ -24,7 +24,7 @@ import { type MlApiServices } from '../services/ml_api_service'; let _capabilities: MlCapabilities = getDefaultCapabilities(); -const CAPABILITIES_REFRESH_INTERVAL = 60000; +const CAPABILITIES_REFRESH_INTERVAL = 5 * 60 * 1000; // 5min; export class MlCapabilitiesService { private _isLoading$ = new BehaviorSubject(true); diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index e4b79eafb896d..6941e53cd68b2 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -70,6 +70,11 @@ export const JobsListPage: FC<{ const I18nContext = coreStart.i18n.Context; const theme$ = coreStart.theme.theme$; + const mlServices = useMemo( + () => getMlGlobalServices(coreStart.http, usageCollection), + [coreStart.http, usageCollection] + ); + const check = async () => { try { await checkGetManagementMlJobsResolver(mlApiServices); @@ -122,7 +127,7 @@ export const JobsListPage: FC<{ usageCollection, fieldFormats, spacesApi, - mlServices: getMlGlobalServices(coreStart.http, usageCollection), + mlServices, }} > diff --git a/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/allocated_models.tsx b/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/allocated_models.tsx index 5cdc922824ae7..02738f3bebe51 100644 --- a/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/allocated_models.tsx +++ b/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/allocated_models.tsx @@ -122,13 +122,27 @@ export const AllocatedModels: FC = ({ }, }, { - field: 'node.throughput_last_minute', - name: i18n.translate( - 'xpack.ml.trainedModels.nodesList.modelsList.throughputLastMinuteHeader', - { - defaultMessage: 'Throughput', - } + name: ( + + + {i18n.translate( + 'xpack.ml.trainedModels.nodesList.modelsList.throughputLastMinuteHeader', + { + defaultMessage: 'Throughput', + } + )} + + + ), + field: 'node.throughput_last_minute', width: '100px', truncateText: false, 'data-test-subj': 'mlAllocatedModelsTableThroughput', diff --git a/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_active_alerts_badge.stories.tsx b/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_active_alerts_badge.stories.tsx index 08fa152301d43..3aed4658ab766 100644 --- a/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_active_alerts_badge.stories.tsx +++ b/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_active_alerts_badge.stories.tsx @@ -25,4 +25,4 @@ const Template: ComponentStory = (props: Props) => ( ); export const Default = Template.bind({}); -Default.args = { activeAlerts: { count: 2, ruleIds: ['rule-1', 'rule-2'] } }; +Default.args = { activeAlerts: { count: 2 } }; diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_active_alerts.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_active_alerts.ts index 319f3e57a4a4c..580d72e550e6b 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_fetch_active_alerts.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_active_alerts.ts @@ -19,7 +19,6 @@ interface Params { export interface ActiveAlerts { count: number; - ruleIds: string[]; } type ActiveAlertsMap = Record; @@ -37,7 +36,6 @@ interface FindApiResponse { buckets: Array<{ key: string; doc_count: number; - perRuleId: { buckets: Array<{ key: string; doc_count: number }> }; }>; }; }; @@ -77,20 +75,22 @@ export function useFetchActiveAlerts({ sloIds = [] }: Params): UseFetchActiveAle }, }, ], + should: [ + { + terms: { + 'kibana.alert.rule.parameters.sloId': sloIds, + }, + }, + ], + minimum_should_match: 1, }, }, aggs: { perSloId: { terms: { + size: sloIds.length, field: 'kibana.alert.rule.parameters.sloId', }, - aggs: { - perRuleId: { - terms: { - field: 'kibana.alert.rule.uuid', - }, - }, - }, }, }, }), @@ -102,7 +102,6 @@ export function useFetchActiveAlerts({ sloIds = [] }: Params): UseFetchActiveAle ...acc, [bucket.key]: { count: bucket.doc_count ?? 0, - ruleIds: bucket.perRuleId.buckets.map((rule) => rule.key), } as ActiveAlerts, }), {} diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_index_pattern_fields.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_index_pattern_fields.ts index bd83b069133c3..2f51e4d7faf26 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_fetch_index_pattern_fields.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_index_pattern_fields.ts @@ -18,6 +18,8 @@ export interface UseFetchIndexPatternFieldsResponse { export interface Field { name: string; type: string; + aggregatable: boolean; + searchable: boolean; } export function useFetchIndexPatternFields( diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/common/group_by_field_selector.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/common/group_by_field_selector.tsx new file mode 100644 index 0000000000000..0733e682e9ba7 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/common/group_by_field_selector.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiComboBox, + EuiComboBoxOptionOption, + EuiFlexItem, + EuiFormRow, + EuiIconTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ALL_VALUE } from '@kbn/slo-schema'; +import React from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; +import { createOptionsFromFields } from '../../helpers/create_options'; +import { CreateSLOForm } from '../../types'; + +interface Props { + index?: string; +} +export function GroupByFieldSelector({ index }: Props) { + const { control, getFieldState } = useFormContext(); + const { isLoading, data: indexFields = [] } = useFetchIndexPatternFields(index); + const groupableFields = indexFields.filter((field) => field.aggregatable); + + const label = i18n.translate('xpack.observability.slo.sloEdit.groupBy.placeholder', { + defaultMessage: 'Select an optional field to partition by', + }); + + return ( + + + {i18n.translate('xpack.observability.slo.sloEdit.groupBy.label', { + defaultMessage: 'Partition by', + })}{' '} + + + } + isInvalid={getFieldState('groupBy').invalid} + > + ( + { + if (selected.length) { + return field.onChange(selected[0].value); + } + + field.onChange(ALL_VALUE); + }} + options={createOptionsFromFields(groupableFields)} + selectedOptions={ + !!index && + !!field.value && + groupableFields.some((groupableField) => groupableField.name === field.value) + ? [{ value: field.value, label: field.value }] + : [] + } + singleSelection + /> + )} + /> + + + ); +} diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/common/query_builder.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/common/query_builder.tsx index c33f5646ff8d9..954b89991f528 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/common/query_builder.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/common/query_builder.tsx @@ -36,7 +36,6 @@ export function QueryBuilder({ useKibana().services; const { control, getFieldState } = useFormContext(); - const { dataView } = useCreateDataView({ indexPatternString }); return ( diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx index c07ce5d132489..6d7ae6c012a2e 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx @@ -20,6 +20,7 @@ import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_inde import { createOptionsFromFields } from '../../helpers/create_options'; import { CreateSLOForm } from '../../types'; import { DataPreviewChart } from '../common/data_preview_chart'; +import { GroupByFieldSelector } from '../common/group_by_field_selector'; import { QueryBuilder } from '../common/query_builder'; import { IndexSelection } from '../custom_common/index_selection'; @@ -81,7 +82,7 @@ export function CustomKqlIndicatorTypeForm() { ? [{ value: field.value, label: field.value }] : [] } - singleSelection={{ asPlainText: true }} + singleSelection /> )} /> @@ -175,6 +176,8 @@ export function CustomKqlIndicatorTypeForm() { />
+ +
); diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/custom_metric_type_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/custom_metric_type_form.tsx index e9a96fbc0c929..25de4568dc06c 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/custom_metric_type_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/custom_metric_type_form.tsx @@ -27,15 +27,15 @@ import { DataPreviewChart } from '../common/data_preview_chart'; import { QueryBuilder } from '../common/query_builder'; import { IndexSelection } from '../custom_common/index_selection'; import { MetricIndicator } from './metric_indicator'; +import { GroupByFieldSelector } from '../common/group_by_field_selector'; export { NEW_CUSTOM_METRIC } from './metric_indicator'; export function CustomMetricIndicatorTypeForm() { const { control, watch, getFieldState } = useFormContext(); - const { isLoading, data: indexFields } = useFetchIndexPatternFields( - watch('indicator.params.index') - ); + const index = watch('indicator.params.index'); + const { isLoading, data: indexFields } = useFetchIndexPatternFields(index); const timestampFields = (indexFields ?? []).filter((field) => field.type === 'date'); return ( @@ -181,6 +181,8 @@ export function CustomMetricIndicatorTypeForm() {
+ +
diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator_type_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator_type_form.tsx index 323cd48a67cc0..56a867ab2e332 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator_type_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator_type_form.tsx @@ -27,6 +27,7 @@ import { DataPreviewChart } from '../common/data_preview_chart'; import { QueryBuilder } from '../common/query_builder'; import { IndexSelection } from '../custom_common/index_selection'; import { HistogramIndicator } from './histogram_indicator'; +import { GroupByFieldSelector } from '../common/group_by_field_selector'; export function HistogramIndicatorTypeForm() { const { control, watch, getFieldState } = useFormContext(); @@ -163,6 +164,9 @@ export function HistogramIndicatorTypeForm() { + + + diff --git a/x-pack/plugins/observability/public/pages/slo_edit/helpers/process_slo_form_values.ts b/x-pack/plugins/observability/public/pages/slo_edit/helpers/process_slo_form_values.ts index d87dd10450f3d..a4ecec640e2df 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/helpers/process_slo_form_values.ts +++ b/x-pack/plugins/observability/public/pages/slo_edit/helpers/process_slo_form_values.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { CreateSLOInput, SLOWithSummaryResponse, UpdateSLOInput } from '@kbn/slo-schema'; +import { CreateSLOInput, SLOWithSummaryResponse, UpdateSLOInput } from '@kbn/slo-schema'; import { toDuration } from '../../../utils/slo/duration'; import { CreateSLOForm } from '../types'; diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_badges.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_badges.tsx index 93b4e2802bc1c..f9c7cc3faeeb8 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_badges.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_badges.tsx @@ -29,7 +29,7 @@ export interface Props { export function SloBadges({ activeAlerts, isLoading, rules, slo, onClickRuleBadge }: Props) { return ( - + {isLoading ? ( <> - + - + ({ name, template: { settings: { - auto_expand_replicas: '0-all', + auto_expand_replicas: '0-1', hidden: true, }, }, diff --git a/x-pack/plugins/observability/server/routes/slo/route.ts b/x-pack/plugins/observability/server/routes/slo/route.ts index fdcdde0197a04..14e5a26e7c7ff 100644 --- a/x-pack/plugins/observability/server/routes/slo/route.ts +++ b/x-pack/plugins/observability/server/routes/slo/route.ts @@ -65,7 +65,7 @@ const isLicenseAtLeastPlatinum = async (context: ObservabilityRequestHandlerCont const createSLORoute = createObservabilityServerRoute({ endpoint: 'POST /api/observability/slos 2023-10-31', options: { - tags: ['access:public', 'access:slo_write'], + tags: ['access:slo_write'], }, params: createSLOParamsSchema, handler: async ({ context, params, logger }) => { @@ -90,7 +90,7 @@ const createSLORoute = createObservabilityServerRoute({ const updateSLORoute = createObservabilityServerRoute({ endpoint: 'PUT /api/observability/slos/{id} 2023-10-31', options: { - tags: ['access:public', 'access:slo_write'], + tags: ['access:slo_write'], }, params: updateSLOParamsSchema, handler: async ({ context, params, logger }) => { @@ -116,7 +116,7 @@ const updateSLORoute = createObservabilityServerRoute({ const deleteSLORoute = createObservabilityServerRoute({ endpoint: 'DELETE /api/observability/slos/{id} 2023-10-31', options: { - tags: ['access:public', 'access:slo_write'], + tags: ['access:slo_write'], }, params: deleteSLOParamsSchema, handler: async ({ @@ -148,7 +148,7 @@ const deleteSLORoute = createObservabilityServerRoute({ const getSLORoute = createObservabilityServerRoute({ endpoint: 'GET /api/observability/slos/{id} 2023-10-31', options: { - tags: ['access:public', 'access:slo_read'], + tags: ['access:slo_read'], }, params: getSLOParamsSchema, handler: async ({ context, params }) => { @@ -173,7 +173,7 @@ const getSLORoute = createObservabilityServerRoute({ const enableSLORoute = createObservabilityServerRoute({ endpoint: 'POST /api/observability/slos/{id}/enable 2023-10-31', options: { - tags: ['access:public', 'access:slo_write'], + tags: ['access:slo_write'], }, params: manageSLOParamsSchema, handler: async ({ context, params, logger }) => { @@ -199,7 +199,7 @@ const enableSLORoute = createObservabilityServerRoute({ const disableSLORoute = createObservabilityServerRoute({ endpoint: 'POST /api/observability/slos/{id}/disable 2023-10-31', options: { - tags: ['access:public', 'access:slo_write'], + tags: ['access:slo_write'], }, params: manageSLOParamsSchema, handler: async ({ context, params, logger }) => { @@ -225,7 +225,7 @@ const disableSLORoute = createObservabilityServerRoute({ const findSLORoute = createObservabilityServerRoute({ endpoint: 'GET /api/observability/slos 2023-10-31', options: { - tags: ['access:public', 'access:slo_read'], + tags: ['access:slo_read'], }, params: findSLOParamsSchema, handler: async ({ context, params, logger }) => { diff --git a/x-pack/plugins/observability/server/services/slo/fetch_historical_summary.ts b/x-pack/plugins/observability/server/services/slo/fetch_historical_summary.ts index 75df72baa870f..095dbd8a64c38 100644 --- a/x-pack/plugins/observability/server/services/slo/fetch_historical_summary.ts +++ b/x-pack/plugins/observability/server/services/slo/fetch_historical_summary.ts @@ -25,11 +25,13 @@ export class FetchHistoricalSummary { const sloIds = params.list.map((slo) => slo.sloId); const sloList = await this.repository.findAllByIds(sloIds); - const list: SLOWithInstanceId[] = params.list.map(({ sloId, instanceId }) => ({ - sloId, - instanceId, - slo: sloList.find((slo) => slo.id === sloId)!, - })); + const list: SLOWithInstanceId[] = params.list + .filter(({ sloId }) => sloList.find((slo) => slo.id === sloId)) + .map(({ sloId, instanceId }) => ({ + sloId, + instanceId, + slo: sloList.find((slo) => slo.id === sloId)!, + })); const historicalSummary = await this.historicalSummaryClient.fetch(list); diff --git a/x-pack/plugins/observability/server/services/slo/slo_installer.ts b/x-pack/plugins/observability/server/services/slo/slo_installer.ts index b59612ca30253..d6e8b8295348f 100644 --- a/x-pack/plugins/observability/server/services/slo/slo_installer.ts +++ b/x-pack/plugins/observability/server/services/slo/slo_installer.ts @@ -37,7 +37,6 @@ export class DefaultSLOInstaller implements SLOInstaller { this.logger.error('Failed to install SLO common resources and summary transforms', { error, }); - throw error; } finally { this.isInstalling = false; clearTimeout(installTimeout); diff --git a/x-pack/plugins/observability/server/services/slo/summary_client.ts b/x-pack/plugins/observability/server/services/slo/summary_client.ts index b12a376be532a..3f799b2ca3473 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_client.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_client.ts @@ -20,8 +20,6 @@ import { DateRange, SLO, Summary } from '../../domain/models'; import { computeSLI, computeSummaryStatus, toErrorBudget } from '../../domain/services'; import { toDateRange } from '../../domain/services/date_range'; -// TODO: Change name of this service... -// It does compute a summary but from the rollup data. export interface SummaryClient { computeSummary(slo: SLO, instanceId?: string): Promise; } diff --git a/x-pack/plugins/observability/server/services/slo/summary_search_client.ts b/x-pack/plugins/observability/server/services/slo/summary_search_client.ts index c7d87ac8b322c..f2bfa1ed29df3 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_search_client.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_search_client.ts @@ -125,7 +125,7 @@ export class DefaultSummarySearchClient implements SummarySearchClient { page: pagination.page, results: finalResults.map((doc) => ({ id: doc._source!.slo.id, - instanceId: doc._source?.slo.instanceId ?? ALL_VALUE, + instanceId: doc._source!.slo.instanceId ?? ALL_VALUE, summary: { errorBudget: { initial: toHighPrecision(doc._source!.errorBudgetInitial), diff --git a/x-pack/plugins/security/common/index.ts b/x-pack/plugins/security/common/index.ts index f7ee13c610a45..c547833949ded 100644 --- a/x-pack/plugins/security/common/index.ts +++ b/x-pack/plugins/security/common/index.ts @@ -23,7 +23,6 @@ export type { UserProfileWithSecurity, UserProfileData, UserProfileLabels, - UserProfileAvatarData, UserProfileUserInfoWithSecurity, ApiKey, UserRealm, diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index 822d6036efc06..c8505a644503f 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -22,7 +22,6 @@ export type { UserProfileData, UserProfileLabels, UserProfileUserInfoWithSecurity, - UserProfileAvatarData, } from './user_profile'; export { getUserAvatarColor, diff --git a/x-pack/plugins/security/common/model/user_profile.ts b/x-pack/plugins/security/common/model/user_profile.ts index c4dd6addd51fc..152b0d0266bbe 100644 --- a/x-pack/plugins/security/common/model/user_profile.ts +++ b/x-pack/plugins/security/common/model/user_profile.ts @@ -7,6 +7,8 @@ import { VISUALIZATION_COLORS } from '@elastic/eui'; +import type { UserProfileAvatarData } from '@kbn/user-profile-components'; + import type { AuthenticatedUser } from './authenticated_user'; import { getUserDisplayName } from './user'; @@ -72,33 +74,6 @@ export type UserProfileData = Record; */ export type UserProfileLabels = Record; -/** - * Avatar stored in user profile. - */ -export interface UserProfileAvatarData { - /** - * Optional initials (two letters) of the user to use as avatar if avatar picture isn't specified. - */ - initials?: string; - /** - * Background color of the avatar when initials are used. - */ - color?: string; - /** - * Base64 data URL for the user avatar image. - */ - imageUrl?: string | null; -} - -export type DarkModeValue = '' | 'dark' | 'light'; - -/** - * User settings stored in the data object of the User Profile - */ -export interface UserSettingsData { - darkMode?: DarkModeValue; -} - /** * Extended user information returned in user profile (both basic and security related properties). */ diff --git a/x-pack/plugins/security/public/account_management/account_management_app.tsx b/x-pack/plugins/security/public/account_management/account_management_app.tsx index 29722c10ea84d..a5b98d66d46ff 100644 --- a/x-pack/plugins/security/public/account_management/account_management_app.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_app.tsx @@ -21,8 +21,13 @@ import type { import { AppNavLinkStatus } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n-react'; -import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { + KibanaContextProvider, + KibanaThemeProvider, + toMountPoint, +} from '@kbn/kibana-react-plugin/public'; import { Router } from '@kbn/shared-ux-router'; +import { UserProfilesKibanaProvider } from '@kbn/user-profile-components'; import type { AuthenticationServiceSetup } from '../authentication'; import type { SecurityApiClients } from '../components'; @@ -96,7 +101,17 @@ export const Providers: FunctionComponent = ({ - {children} + + + {children} + + diff --git a/x-pack/plugins/security/public/account_management/index.ts b/x-pack/plugins/security/public/account_management/index.ts index eca7287537318..e1a4957aa71e7 100644 --- a/x-pack/plugins/security/public/account_management/index.ts +++ b/x-pack/plugins/security/public/account_management/index.ts @@ -11,5 +11,4 @@ export type { UserProfileBulkGetParams, UserProfileGetCurrentParams, UserProfileSuggestParams, - UpdateUserProfileHook, } from './user_profile'; diff --git a/x-pack/plugins/security/public/account_management/user_profile/index.ts b/x-pack/plugins/security/public/account_management/user_profile/index.ts index 93a1c7d04d315..ed34d7d4a4339 100644 --- a/x-pack/plugins/security/public/account_management/user_profile/index.ts +++ b/x-pack/plugins/security/public/account_management/user_profile/index.ts @@ -13,5 +13,3 @@ export type { UserProfileBulkGetParams, UserProfileSuggestParams, } from './user_profile_api_client'; - -export type { UpdateUserProfileHook } from './use_update_user_profile'; diff --git a/x-pack/plugins/security/public/account_management/user_profile/use_update_user_profile.tsx b/x-pack/plugins/security/public/account_management/user_profile/use_update_user_profile.tsx deleted file mode 100644 index 2dafa61496fe8..0000000000000 --- a/x-pack/plugins/security/public/account_management/user_profile/use_update_user_profile.tsx +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback, useRef, useState } from 'react'; -import useObservable from 'react-use/lib/useObservable'; - -import type { NotificationsStart, ToastInput, ToastOptions } from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; - -import type { UserProfileData } from './user_profile'; -import type { UserProfileAPIClient } from './user_profile_api_client'; - -interface Deps { - apiClient: UserProfileAPIClient; - notifications: NotificationsStart; -} - -interface Props { - notificationSuccess?: { - /** Flag to indicate if a notification is shown after update. Default: `true` */ - enabled?: boolean; - /** Customize the title of the notification */ - title?: string; - /** Customize the "page reload needed" text of the notification */ - pageReloadText?: string; - }; - /** Predicate to indicate if the update requires a page reload */ - pageReloadChecker?: ( - previsous: UserProfileData | null | undefined, - next: UserProfileData - ) => boolean; -} - -export type UpdateUserProfileHook = (props?: Props) => { - /** Update the user profile */ - update: (data: UserProfileData) => void; - /** Handler to show a notification after the user profile has been updated */ - showSuccessNotification: (props: { isRefreshRequired: boolean }) => void; - /** Flag to indicate if currently updating */ - isLoading: boolean; - /** The current user profile data */ - userProfileData?: UserProfileData | null; -}; - -const i18nTexts = { - notificationSuccess: { - title: i18n.translate('xpack.security.accountManagement.userProfile.submitSuccessTitle', { - defaultMessage: 'Profile updated', - }), - pageReloadText: i18n.translate( - 'xpack.security.accountManagement.userProfile.requiresPageReloadToastDescription', - { - defaultMessage: 'One or more settings require you to reload the page to take effect.', - } - ), - }, -}; - -export const getUseUpdateUserProfile = ({ apiClient, notifications }: Deps) => { - const { userProfile$ } = apiClient; - - const useUpdateUserProfile = ({ notificationSuccess = {}, pageReloadChecker }: Props = {}) => { - const { - enabled: notificationSuccessEnabled = true, - title: notificationTitle = i18nTexts.notificationSuccess.title, - pageReloadText = i18nTexts.notificationSuccess.pageReloadText, - } = notificationSuccess; - const [isLoading, setIsLoading] = useState(false); - const userProfileData = useObservable(userProfile$); - // Keep a snapshot before updating the user profile so we can compare previous and updated values - const userProfileSnapshot = useRef(); - - const showSuccessNotification = useCallback( - ({ isRefreshRequired = false }: { isRefreshRequired?: boolean } = {}) => { - let successToastInput: ToastInput = { - title: notificationTitle, - }; - let successToastOptions: ToastOptions = {}; - - if (isRefreshRequired) { - successToastOptions = { - toastLifeTimeMs: 1000 * 60 * 5, - }; - - successToastInput = { - ...successToastInput, - text: toMountPoint( - - -

{pageReloadText}

- window.location.reload()} - data-test-subj="windowReloadButton" - > - {i18n.translate( - 'xpack.security.accountManagement.userProfile.requiresPageReloadToastButtonLabel', - { - defaultMessage: 'Reload page', - } - )} - -
-
- ), - }; - } - - notifications.toasts.addSuccess(successToastInput, successToastOptions); - }, - [notificationTitle, pageReloadText] - ); - - const onUserProfileUpdate = useCallback( - (updatedData: UserProfileData) => { - setIsLoading(false); - - if (notificationSuccessEnabled) { - const isRefreshRequired = pageReloadChecker?.(userProfileSnapshot.current, updatedData); - showSuccessNotification({ isRefreshRequired }); - } - }, - [notificationSuccessEnabled, showSuccessNotification, pageReloadChecker] - ); - - const update = useCallback( - (udpatedData: D) => { - userProfileSnapshot.current = userProfileData; - setIsLoading(true); - return apiClient.update(udpatedData).then(() => onUserProfileUpdate(udpatedData)); - }, - [onUserProfileUpdate, userProfileData] - ); - - return { - update, - showSuccessNotification, - userProfileData, - isLoading, - }; - }; - - return useUpdateUserProfile; -}; diff --git a/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx b/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx index 2632f73e99d07..a6227baee4061 100644 --- a/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx +++ b/x-pack/plugins/security/public/account_management/user_profile/user_profile.tsx @@ -39,7 +39,8 @@ import type { CoreStart, IUiSettingsClient } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { UserAvatar } from '@kbn/user-profile-components'; +import type { DarkModeValue, UserProfileData } from '@kbn/user-profile-components'; +import { UserAvatar, useUpdateUserProfile } from '@kbn/user-profile-components'; import type { AuthenticatedUser } from '../../../common'; import { @@ -48,11 +49,6 @@ import { getUserAvatarColor, getUserAvatarInitials, } from '../../../common/model'; -import type { - DarkModeValue, - UserProfileAvatarData, - UserSettingsData, -} from '../../../common/model/user_profile'; import { useSecurityApiClients } from '../../components'; import { Breadcrumb } from '../../components/breadcrumb'; import { @@ -65,15 +61,8 @@ import { FormLabel } from '../../components/form_label'; import { FormRow, OptionalText } from '../../components/form_row'; import { ChangePasswordModal } from '../../management/users/edit_user/change_password_modal'; import { isUserReserved } from '../../management/users/user_utils'; -import { getUseUpdateUserProfile } from './use_update_user_profile'; import { createImageHandler, getRandomColor, IMAGE_FILE_TYPES, VALID_HEX_COLOR } from './utils'; -export interface UserProfileData { - avatar?: UserProfileAvatarData; - userSettings?: UserSettingsData; - [key: string]: unknown; -} - export interface UserProfileProps { user: AuthenticatedUser; data?: UserProfileData; @@ -833,12 +822,11 @@ export const UserProfile: FunctionComponent = ({ user, data }) export function useUserProfileForm({ user, data }: UserProfileProps) { const { services } = useKibana(); - const { userProfiles, users } = useSecurityApiClients(); + const { users } = useSecurityApiClients(); - const { update, showSuccessNotification } = getUseUpdateUserProfile({ - apiClient: userProfiles, - notifications: services.notifications, - })({ notificationSuccess: { enabled: false } }); + const { update, showSuccessNotification } = useUpdateUserProfile({ + notificationSuccess: { enabled: false }, + }); const [initialValues, resetInitialValues] = useState({ user: { diff --git a/x-pack/plugins/security/public/account_management/user_profile/user_profile_api_client.ts b/x-pack/plugins/security/public/account_management/user_profile/user_profile_api_client.ts index 4b992f616ca14..4760aa15ab0b3 100644 --- a/x-pack/plugins/security/public/account_management/user_profile/user_profile_api_client.ts +++ b/x-pack/plugins/security/public/account_management/user_profile/user_profile_api_client.ts @@ -10,9 +10,9 @@ import type { Observable } from 'rxjs'; import { BehaviorSubject, Subject } from 'rxjs'; import type { HttpStart } from '@kbn/core/public'; +import type { UserProfileData } from '@kbn/user-profile-components'; import type { GetUserProfileResponse, UserProfile } from '../../../common'; -import type { UserProfileData } from './user_profile'; /** * Parameters for the get user profile for the current user API. diff --git a/x-pack/plugins/security/public/index.ts b/x-pack/plugins/security/public/index.ts index b51bb3d25092e..209bc5ff576b6 100644 --- a/x-pack/plugins/security/public/index.ts +++ b/x-pack/plugins/security/public/index.ts @@ -24,7 +24,6 @@ export type { UserProfileBulkGetParams, UserProfileGetCurrentParams, UserProfileSuggestParams, - UpdateUserProfileHook, } from './account_management'; export type { AuthenticationServiceStart, AuthenticationServiceSetup } from './authentication'; diff --git a/x-pack/plugins/security/public/mocks.ts b/x-pack/plugins/security/public/mocks.ts index f0081307ef33f..8a9232869b430 100644 --- a/x-pack/plugins/security/public/mocks.ts +++ b/x-pack/plugins/security/public/mocks.ts @@ -32,9 +32,6 @@ function createStartMock() { userProfile$: of({}), }, uiApi: getUiApiMock.createStart(), - hooks: { - useUpdateUserProfile: jest.fn(), - }, }; } diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx index 4a5e8ad545d64..13bcb3bcb4341 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx @@ -22,9 +22,8 @@ import type { Observable } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { UserAvatar } from '@kbn/user-profile-components'; +import { UserAvatar, type UserProfileAvatarData } from '@kbn/user-profile-components'; -import type { UserProfileAvatarData } from '../../common'; import { getUserDisplayName, isUserAnonymous } from '../../common/model'; import { useCurrentUser, useUserProfile } from '../components'; diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index 87ce15a19202d..fdab78a1f91d0 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -96,9 +96,6 @@ describe('Security Plugin', () => { "areAPIKeysEnabled": [Function], "getCurrentUser": [Function], }, - "hooks": Object { - "useUpdateUserProfile": [Function], - }, "navControlService": Object { "addUserMenuLinks": [Function], "getUserMenuLinks$": [Function], diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index e8c2c13ab0eb6..49c0a14e2fd9c 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -24,9 +24,7 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import type { SecurityLicense } from '../common/licensing'; import { SecurityLicenseService } from '../common/licensing'; -import type { UpdateUserProfileHook } from './account_management'; import { accountManagementApp, UserProfileAPIClient } from './account_management'; -import { getUseUpdateUserProfile } from './account_management/user_profile/use_update_user_profile'; import { AnalyticsService } from './analytics'; import { AnonymousAccessService } from './anonymous_access'; import type { AuthenticationServiceSetup, AuthenticationServiceStart } from './authentication'; @@ -213,12 +211,6 @@ export class SecurityPlugin ), userProfile$: this.securityApiClients.userProfiles.userProfile$, }, - hooks: { - useUpdateUserProfile: getUseUpdateUserProfile({ - apiClient: this.securityApiClients.userProfiles, - notifications: core.notifications, - }), - }, }; } @@ -263,13 +255,6 @@ export interface SecurityPluginStart { 'getCurrent' | 'bulkGet' | 'suggest' | 'update' | 'userProfile$' >; - /** - * A set of hooks to work with Kibana user profiles - */ - hooks: { - useUpdateUserProfile: UpdateUserProfileHook; - }; - /** * Exposes UI components that will be loaded asynchronously. * @deprecated diff --git a/x-pack/plugins/security_solution/common/api/tags/create_tag/create_tag_route.ts b/x-pack/plugins/security_solution/common/api/tags/create_tag/create_tag_route.ts new file mode 100644 index 0000000000000..160ae43b9aa0f --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/tags/create_tag/create_tag_route.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as rt from 'io-ts'; + +export const createTagRequest = rt.intersection([ + rt.type({ + name: rt.string, + description: rt.string, + }), + rt.partial({ color: rt.string }), +]); diff --git a/x-pack/plugins/security_solution/common/api/tags/get_dashboards_by_tags/get_dashboards_by_tags_route.ts b/x-pack/plugins/security_solution/common/api/tags/get_dashboards_by_tags/get_dashboards_by_tags_route.ts new file mode 100644 index 0000000000000..af2c89a967311 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/tags/get_dashboards_by_tags/get_dashboards_by_tags_route.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; + +export const getDashboardsRequest = rt.type({ tagIds: rt.array(rt.string) }); diff --git a/x-pack/plugins/security_solution/common/api/tags/get_tags_by_name/get_tags_by_name_route.ts b/x-pack/plugins/security_solution/common/api/tags/get_tags_by_name/get_tags_by_name_route.ts new file mode 100644 index 0000000000000..4ae16c320c02e --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/tags/get_tags_by_name/get_tags_by_name_route.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; + +export const getTagsByNameRequest = rt.type({ name: rt.string }); diff --git a/x-pack/plugins/security_solution/common/api/tags/index.ts b/x-pack/plugins/security_solution/common/api/tags/index.ts new file mode 100644 index 0000000000000..3f48011365f42 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/tags/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './create_tag/create_tag_route'; +export * from './get_dashboards_by_tags/get_dashboards_by_tags_route'; +export * from './get_tags_by_name/get_tags_by_name_route'; diff --git a/x-pack/plugins/security_solution/cypress/README.md b/x-pack/plugins/security_solution/cypress/README.md index db67eac909b19..6b3b641e8e59c 100644 --- a/x-pack/plugins/security_solution/cypress/README.md +++ b/x-pack/plugins/security_solution/cypress/README.md @@ -2,17 +2,12 @@ The `security_solution/cypress` directory contains functional UI tests that execute using [Cypress](https://www.cypress.io/). -Currently with Cypress you can develop `functional` tests and coming soon `CCS` and `Upgrade` functional tests. +Currently with Cypress you can develop `functional` tests. If you are still having doubts, questions or queries, please feel free to ping our Cypress champions: - Functional Tests: - Gloria Hornero and Patryk Kopycinsky -- CCS Tests: - - Technical questions around the https://github.com/elastic/integration-test repo: - - Domenico Andreoli - - Doubts regarding testing CCS and Cypress best practices: - - Gloria Hornero ## Table of Contents @@ -51,246 +46,17 @@ Run the tests with the following yarn scripts: | Script Name | Description | | ----------- | ----------- | | cypress | Runs the default Cypress command | -| cypress:open | Opens the Cypress UI with all tests in the `e2e` directory. This also runs a local kibana and ES instance. The kibana instance will reload when you make code changes. This is the recommended way to debug and develop tests. | -| cypress:open:ccs | Opens the Cypress UI and runs all tests in the `ccs_e2e` directory | -| cypress:open:upgrade | Opens the Cypress UI and runs all tests in the `upgrade_e2e` directory | +| cypress:open | Opens the Cypress UI with all tests in the `e2e` directory. This also runs a local kibana and ES instance. The kibana instance will reload when you make code changes. This is the recommended way to debug and develop tests. |C | cypress:run | Runs all tests in the `e2e` directory excluding `investigations` and `explore` directories in headless mode | | cypress:run:cases | Runs all tests under `explore/cases` in the `e2e` directory related to the Cases area team in headless mode | | cypress:run:reporter | Runs all tests with the specified configuration in headless mode and produces a report using `cypress-multi-reporters` | | cypress:run:respops | Runs all tests related to the Response Ops area team, specifically tests in `detection_alerts`, `detection_rules`, and `exceptions` directories in headless mode | -| cypress:run:ccs | Runs all tests in the `ccs_e2e` directory in headless mode | -| cypress:run:upgrade | Runs all tests in the `upgrade_e2e` directory in headless mode | | cypress:investigations:run | Runs all tests in the `e2e/investigations` directory in headless mode | | cypress:explore:run | Runs all tests in the `e2e/explore` directory in headless mode | | junit:merge | Merges individual test reports into a single report and moves the report to the `junit` directory | Please note that all the headless mode commands do not open the Cypress UI and are typically used in CI/CD environments. The scripts that open the Cypress UI are useful for development and debugging. -### Execution modes - -There are currently four ways to run the tests, comprised of two execution modes and two target environments, which will be detailed below. - -#### Interactive mode - -When you run Cypress in interactive mode, an interactive runner is displayed that allows you to see commands as they execute while also viewing the application under test. For more information, please see [cypress documentation](https://docs.cypress.io/guides/core-concepts/test-runner.html#Overview). - -#### Headless mode - -A headless browser is a browser simulation program that does not have a user interface. These programs operate like any other browser, but do not display any UI. This is why meanwhile you are executing the tests on this mode you are not going to see the application under test. Just the output of the test is displayed on the terminal once the execution is finished. - -### Target environments - -#### FTR (CI) - -This is the configuration used by CI. It uses the FTR to spawn both a Kibana instance (http://localhost:5620) and an Elasticsearch instance (http://localhost:9220) with a preloaded minimum set of data (see preceding "Test data" section), and then executes cypress against this stack. You can find this configuration in `x-pack/test/security_solution_cypress` - -Tests run on buildkite PR pipeline is parallelized. It can be configured in [.buildkite/pipelines/pull_request/security_solution.yml](https://github.com/elastic/kibana/blob/main/.buildkite/pipelines/pull_request/security_solution.yml) with property `parallelism` - -```yml - ... - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - parallelism: 4 - ... -``` - -#### Custom Targets - -This configuration runs cypress tests against an arbitrary host. -**WARNING**: When using your own instances you need to take into account that if you already have data on it, the tests may fail, as well as, they can put your instances in an undesired state, since our tests uses es_archive to populate data. - -#### integration-test (CI) - -This configuration is driven by [elastic/integration-test](https://github.com/elastic/integration-test) which, as part of a bigger set of tests, provisions one VM with two instances configured in CCS mode and runs the [CCS Cypress test specs](./ccs_e2e). - -The two clusters are named `admin` and `data` and are reachable as follows: - -| | Elasticsearch | Kibana | -| ----- | ---------------------- | ---------------------- | -| admin | https://localhost:9200 | https://localhost:5601 | -| data | https://localhost:9210 | https://localhost:5602 | - -### Working with integration-test - -#### Initial setup and prerequisites - -The entry point is [integration-test/jenkins_test.sh](https://github.com/elastic/integration-test/blob/master/jenkins_test.sh), it essentially prepares the VMs and there runs tests. Some snapshots (`phase1` and `phase2`) are taken along the way so that it's possible to short cut the VM preparation when iterating over tests for development or debugging. - -The VMs are managed with Vagrant using the VirtualBox provider therefore you need to install both these tools. The host OS can be either Windows, Linux or MacOS. - -`jenkins_test.sh` assumes that a `kibana` folder is present alongside the `integration-test` where it's executed from. The `kibana` folder is used only for loading the test suites though, the actual packages for the VMs preparation are downloaded from elastic.co according to the `BUILD` environment variable or the branch which `jenkins_test.sh` is invoked from. It's your responsibility to checkout the matching branches in `kibana` and `integration-test` as needed. - -Read [integration-test#readme](https://github.com/elastic/integration-test#readme) for further details. - -#### Use cases - -There is no way to just set up the test environment without also executing tests at least once. On the other hand it's time consuming to go throught the whole CI procedure to just iterate over the tests therefore the following instructions support the two use cases: - -- reproduce e2e the CI execution locally, ie. for debugging a CI failure -- use the CI script to easily setup the environment for tests development/debugging - -The missing use case, application TDD, requires a different solution that runs from the checked out repositories instead of the pre-built packages and it's yet to be developed. - -#### Run the CI flow - -This is the CI flow narrowed down to the execution of CCS Cypress tests: - -```shell -cd integration-test -VMS=ubuntu16_tar_ccs_cypress ./jenkins_test.sh -``` - -It destroys and rebuilds the VM. There installs, provisions and starts the stack according to the configuration in [integration-test/provision/ubuntu16_tar_ccs_cypress.sh](https://github.com/elastic/integration-test/blob/master/provision/ubuntu16_tar_ccs_cypress.sh). - -The tests are executed using the FTR runner `SecuritySolutionCypressCcsTestRunner` defined in [x-pack/test/security_solution_cypress/runner.ts](../../../test/security_solution_cypress/runner.ts) as configured in [x-pack/test/security_solution_cypress/ccs_config.ts](../../../test/security_solution_cypress/ccs_config.ts). - -#### Re-run the tests - -After the first run it's possible to restore the VM at `phase2`, right before tests were executed, and run them again: - -```shell -cd integration-test -MODE=retest ./jenkins_test.sh -``` - -It remembers which VM the first round was executed on, you don't need to specify `VMS` any more. - -In case your tests are cleaning after themselves and therefore result idempotent, you can skip the restoration to `phase2` and directly run the Cypress command line. See [CCS Custom Target + Headless](#ccs-custom-target--headless) further below for details but ensure you'll define the `CYPRESS_*` following the correspondence: - -| Cypress command line | [integration-test/provision/ubuntu16_tar_ccs_cypress.sh](https://github.com/elastic/integration-test/blob/master/provision/ubuntu16_tar_ccs_cypress.sh) | -| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -| CYPRESS_BASE_URL | TEST_KIBANA_URL | -| CYPRESS_ELASTICSEARCH_URL | TEST_ES_URL | -| CYPRESS_CCS_KIBANA_URL | TEST_KIBANA_URLDATA | -| CYPRESS_CCS_ELASTICSEARCH_URL | TEST_ES_URLDATA | -| CYPRESS_CCS_REMOTE_NAME | TEST_CCS_REMOTE_NAME | -| CYPRESS_ELASTICSEARCH_USERNAME | ELASTICSEARCH_USERNAME | -| CYPRESS_ELASTICSEARCH_PASSWORD | ELASTICSEARCH_PASSWORD | -| TEST_CA_CERT_PATH | integration-test/certs/ca/ca.crt | - -Note: `TEST_CA_CERT_PATH` above is truly without `CYPRESS_` prefix. - -### Test Execution: Examples - -#### FTR + Headless (Chrome) - -Since this is how tests are run on CI, this will likely be the configuration you want to reproduce failures locally, etc. - -```shell -# bootstrap kibana from the project root -yarn kbn bootstrap - -# build the plugins/assets that cypress will execute against -node scripts/build_kibana_platform_plugins - -# launch the cypress test runner -cd x-pack/plugins/security_solution -yarn cypress:run-as-ci -``` - -#### FTR + Headless (Firefox) - -Since this is how tests are run on CI, this will likely be the configuration you want to reproduce failures locally, etc. - -```shell -# bootstrap kibana from the project root -yarn kbn bootstrap - -# build the plugins/assets that cypress will execute against -node scripts/build_kibana_platform_plugins - -# launch the cypress test runner -cd x-pack/plugins/security_solution -yarn cypress:run-as-ci:firefox -``` - -#### FTR + Interactive - -This is the preferred mode for developing new tests. - -```shell -# bootstrap kibana from the project root -yarn kbn bootstrap - -# build the plugins/assets that cypress will execute against -node scripts/build_kibana_platform_plugins - -# launch the cypress test runner -cd x-pack/plugins/security_solution -yarn cypress:open-as-ci -``` - -Note that you can select the browser you want to use on the top right side of the interactive runner. - -#### Custom Target + Headless (Chrome) - -This mode may be useful for testing a release, e.g. spin up a build candidate -and point cypress at it to catch regressions. - -```shell -# bootstrap kibana from the project root -yarn kbn bootstrap - -# load auditbeat data needed for test execution (which FTR normally does for us) -cd x-pack/plugins/security_solution -node ../../../scripts/es_archiver load auditbeat --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http(s)://:@ --kibana-url http(s)://:@ - -# launch the cypress test runner with overridden environment variables -cd x-pack/plugins/security_solution -CYPRESS_BASE_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_USERNAME= CYPRESS_ELASTICSEARCH_PASSWORD= yarn cypress:run -``` - -#### Custom Target + Headless (Firefox) - -This mode may be useful for testing a release, e.g. spin up a build candidate -and point cypress at it to catch regressions. - -```shell -# bootstrap kibana from the project root -yarn kbn bootstrap - -# load auditbeat data needed for test execution (which FTR normally does for us) -cd x-pack/plugins/security_solution -node ../../../scripts/es_archiver load auditbeat --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http(s)://:@ --kibana-url http(s)://:@ - -# launch the cypress test runner with overridden environment variables -cd x-pack/plugins/security_solution -CYPRESS_BASE_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_USERNAME= CYPRESS_ELASTICSEARCH_PASSWORD= yarn cypress:run:firefox -``` - -#### CCS Custom Target + Headless - -This test execution requires two clusters configured for CCS. See [Search across clusters](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-cross-cluster-search.html) for instructions on how to prepare such setup. - -The instructions below assume: - -- Search cluster is on server1 -- Remote cluster is on server2 -- Remote cluster is accessible from the search cluster with name `remote` -- Security and TLS are enabled - -```shell -# bootstrap Kibana from the project root -yarn kbn bootstrap - -# launch the Cypress test runner with overridden environment variables -cd x-pack/plugins/security_solution -CYPRESS_ELASTICSEARCH_USERNAME="user" \ -CYPRESS_ELASTICSEARCH_PASSWORD="pass" \ -CYPRESS_BASE_URL="https://user:pass@server1:5601" \ -CYPRESS_ELASTICSEARCH_URL="https://user:pass@server1:9200" \ -CYPRESS_CCS_KIBANA_URL="https://user:pass@server2:5601" \ -CYPRESS_CCS_ELASTICSEARCH_URL="https://user:pass@server2:9200" \ -CYPRESS_CCS_REMOTE_NAME="remote" \ -yarn cypress:run:ccs -``` - -Similar sequence, just ending with `yarn cypress:open:ccs`, can be used for interactive test running via Cypress UI. - -Appending `--browser firefox` to the `yarn cypress:run:ccs` command above will run the tests on Firefox instead of Chrome. - ## Debugging your test In order to be able to debug any Cypress test you need to open Cypress on visual mode. [Here](https://docs.cypress.io/guides/guides/debugging) @@ -302,10 +68,6 @@ If you are debugging a flaky test, a good tip is to insert a `cy.wait('); -``` - -They will use the `CYPRESS_CCS_*_URL` environment variables for accessing the remote cluster. Complex tests involving local and remote data can interleave them with `esArchiverLoad` and `esArchiverUnload` as needed. - -#### Remote indices queries - -Queries accessing remote indices follow the usual `:` notation but should not hard-code the remote name in the test itself. - -For such reason the environemnt variable `CYPRESS_CCS_REMOTE_NAME` is defined and, in the case of detection rules, used as shown below: - -```javascript -const ccsRemoteName: string = Cypress.env('CCS_REMOTE_NAME'); - -export const unmappedCCSRule: CustomRule = { - customQuery: '*:*', - index: [`${ccsRemoteName}:unmapped*`], - ... -}; - -``` - -Similar approach should be used in defining all index patterns, rules, and queries to be applied on remote data. - ## Development Best Practices Below you will a set of best practices that should be followed when writing Cypress tests. @@ -478,12 +202,6 @@ taken into consideration until another solution is implemented: Remember that minimizing the number of times the web page is loaded, we minimize as well the execution time. -### CCS test specific - -When testing CCS we want to put our focus in making sure that our `Source` instance is receiving properly the data that comes from the `Remote` instances, as well as the data is displayed as we expect on the `Source`. - -For that reason and in order to make our test more stable, use the API to execute all the actions needed before the assertions, and use Cypress to assert that the UI is displaying all the expected things. - ## Test Artifacts When Cypress tests are run headless on the command line, artifacts diff --git a/x-pack/plugins/security_solution/cypress/ccs_e2e/detection_alerts/alerts_details.cy.ts b/x-pack/plugins/security_solution/cypress/ccs_e2e/detection_alerts/alerts_details.cy.ts deleted file mode 100644 index 2865eb6e5ee9b..0000000000000 --- a/x-pack/plugins/security_solution/cypress/ccs_e2e/detection_alerts/alerts_details.cy.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { JSON_TEXT } from '../../screens/alerts_details'; - -import { expandFirstAlert, waitForAlerts } from '../../tasks/alerts'; -import { openJsonView } from '../../tasks/alerts_details'; -import { createRule } from '../../tasks/api_calls/rules'; -import { cleanKibana } from '../../tasks/common'; -import { login, visitWithoutDateRange } from '../../tasks/login'; - -import { getUnmappedCCSRule } from '../../objects/rule'; - -import { ALERTS_URL } from '../../urls/navigation'; - -describe('Alert details with unmapped fields', () => { - beforeEach(() => { - login(); - cleanKibana(); - cy.task('esArchiverCCSLoad', 'unmapped_fields'); - createRule(getUnmappedCCSRule()); - visitWithoutDateRange(ALERTS_URL); - waitForAlerts(); - expandFirstAlert(); - }); - - it('Displays the unmapped field on the JSON view', () => { - const expectedUnmappedValue = 'This is the unmapped field'; - - openJsonView(); - - cy.get(JSON_TEXT).then((x) => { - const parsed = JSON.parse(x.text()); - expect(parsed.fields.unmapped[0]).to.equal(expectedUnmappedValue); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/cypress/ccs_e2e/detection_rules/event_correlation_rule.cy.ts b/x-pack/plugins/security_solution/cypress/ccs_e2e/detection_rules/event_correlation_rule.cy.ts deleted file mode 100644 index ea2a1797fec5b..0000000000000 --- a/x-pack/plugins/security_solution/cypress/ccs_e2e/detection_rules/event_correlation_rule.cy.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getCCSEqlRule } from '../../objects/rule'; - -import { ALERTS_COUNT, ALERT_DATA_GRID } from '../../screens/alerts'; - -import { - filterByCustomRules, - goToRuleDetails, - waitForRulesTableToBeLoaded, -} from '../../tasks/alerts_detection_rules'; -import { createRule } from '../../tasks/api_calls/rules'; -import { cleanKibana } from '../../tasks/common'; -import { waitForAlertsToPopulate, waitForTheRuleToBeExecuted } from '../../tasks/create_new_rule'; -import { login, visitWithoutDateRange } from '../../tasks/login'; - -import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; - -describe('Detection rules', function () { - const expectedNumberOfAlerts = '1 alert'; - - beforeEach('Reset signals index', function () { - cleanKibana(); - }); - - it('EQL rule on remote indices generates alerts', function () { - cy.task('esArchiverCCSLoad', 'linux_process'); - const rule = getCCSEqlRule(); - login(); - createRule(rule); - visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - waitForRulesTableToBeLoaded(); - filterByCustomRules(); - goToRuleDetails(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - - cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfAlerts); - cy.get(ALERT_DATA_GRID) - .invoke('text') - .then((text) => { - cy.log('ALERT_DATA_GRID', text); - expect(text).contains(rule.name); - expect(text).contains(rule.severity); - expect(text).contains(rule.risk_score); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/cypress/support/e2e.js b/x-pack/plugins/security_solution/cypress/support/e2e.js index 3984b4be49727..477c2606153b7 100644 --- a/x-pack/plugins/security_solution/cypress/support/e2e.js +++ b/x-pack/plugins/security_solution/cypress/support/e2e.js @@ -23,6 +23,9 @@ // Import commands.js using ES2015 syntax: import './commands'; import 'cypress-real-events/support'; +import registerCypressGrep from '@cypress/grep'; + +registerCypressGrep(); Cypress.on('uncaught:exception', () => { return false; diff --git a/x-pack/plugins/security_solution/cypress/upgrade_e2e/detections/detection_rules/custom_query_rule.cy.ts b/x-pack/plugins/security_solution/cypress/upgrade_e2e/detections/detection_rules/custom_query_rule.cy.ts deleted file mode 100644 index a9950f840fa72..0000000000000 --- a/x-pack/plugins/security_solution/cypress/upgrade_e2e/detections/detection_rules/custom_query_rule.cy.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import semver from 'semver'; -import { - ALERT_GRID_CELL, - DESTINATION_IP, - HOST_NAME, - PROCESS_NAME_COLUMN, - PROCESS_NAME, - REASON, - RISK_SCORE, - RULE_NAME, - SEVERITY, - SOURCE_IP, - USER_NAME, -} from '../../../screens/alerts'; -import { SERVER_SIDE_EVENT_COUNT } from '../../../screens/alerts_detection_rules'; -import { - ADDITIONAL_LOOK_BACK_DETAILS, - ABOUT_DETAILS, - ABOUT_RULE_DESCRIPTION, - CUSTOM_QUERY_DETAILS, - DEFINITION_DETAILS, - INDEX_PATTERNS_DETAILS, - RISK_SCORE_DETAILS, - RULE_NAME_HEADER, - RULE_TYPE_DETAILS, - RUNS_EVERY_DETAILS, - SCHEDULE_DETAILS, - SEVERITY_DETAILS, - TIMELINE_TEMPLATE_DETAILS, -} from '../../../screens/rule_details'; - -import { getDetails } from '../../../tasks/rule_details'; -import { waitForPageToBeLoaded } from '../../../tasks/common'; -import { - waitForRulesTableToBeLoaded, - goToTheRuleDetailsOf, -} from '../../../tasks/alerts_detection_rules'; -import { login, visit } from '../../../tasks/login'; - -import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; - -const EXPECTED_NUMBER_OF_ALERTS = '1'; - -const alert = { - rule: 'Custom query rule for upgrade', - severity: 'low', - riskScore: '7', - reason: - 'file event with process test, file The file to test, by Security Solution on security-solution.local created low alert Custom query rule for upgrade.', - reasonAlt: '—', - hostName: 'security-solution.local', - username: 'Security Solution', - processName: 'test', - fileName: 'The file to test', - sourceIp: '127.0.0.1', - destinationIp: '127.0.0.2', -}; - -const rule = { - customQuery: '*:*', - name: 'Custom query rule for upgrade', - description: 'My description', - index: ['auditbeat-custom*'], - severity: 'Low', - riskScore: '7', - timelineTemplate: 'none', - runsEvery: '24h', - lookBack: '49976h', - timeline: 'None', -}; - -describe('After an upgrade, the custom query rule', () => { - before(() => { - login(); - visit(DETECTIONS_RULE_MANAGEMENT_URL); - waitForRulesTableToBeLoaded(); - goToTheRuleDetailsOf(rule.name); - waitForPageToBeLoaded(); - // Possible bug on first attempt sometimes redirects page back to alerts - // Going to retry the block once - cy.url().then((url) => { - const currentUrl = url; - cy.log(`Current URL is : ${currentUrl}`); - if (!currentUrl.includes(DETECTIONS_RULE_MANAGEMENT_URL)) { - cy.log('Retrying not on correct page!'); - visit(DETECTIONS_RULE_MANAGEMENT_URL); - waitForRulesTableToBeLoaded(); - goToTheRuleDetailsOf(rule.name); - waitForPageToBeLoaded(); - } - }); - cy.url().should('include', DETECTIONS_RULE_MANAGEMENT_URL); - }); - - it('Has the expected alerts number', () => { - cy.get(SERVER_SIDE_EVENT_COUNT).contains(EXPECTED_NUMBER_OF_ALERTS); - }); - - it('Displays the rule details', () => { - cy.get(RULE_NAME_HEADER).should('contain', `${rule.name}`); - cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description); - cy.get(ABOUT_DETAILS).within(() => { - getDetails(SEVERITY_DETAILS).should('have.text', rule.severity); - getDetails(RISK_SCORE_DETAILS).should('have.text', rule.riskScore); - }); - cy.get(DEFINITION_DETAILS).within(() => { - getDetails(INDEX_PATTERNS_DETAILS).should('have.text', rule.index.join('')); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.customQuery); - getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); - getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', rule.timeline); - }); - cy.get(SCHEDULE_DETAILS).within(() => { - getDetails(RUNS_EVERY_DETAILS).should('have.text', rule.runsEvery); - getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should('have.text', rule.lookBack); - }); - }); - - it('Displays the alert details at the tgrid', () => { - let expectedReason = alert.reason; - if (semver.lt(Cypress.env('ORIGINAL_VERSION'), '7.15.0')) { - expectedReason = alert.reasonAlt; - } - cy.get(ALERT_GRID_CELL).first().focus(); - cy.get(RULE_NAME).should('have.text', alert.rule); - cy.get(SEVERITY).should('have.text', alert.severity); - cy.get(RISK_SCORE).should('have.text', alert.riskScore); - cy.get(REASON).contains(expectedReason); - cy.get(HOST_NAME).should('have.text', alert.hostName); - cy.get(USER_NAME).should('have.text', alert.username); - cy.get(PROCESS_NAME_COLUMN).eq(0).scrollIntoView(); - cy.get(PROCESS_NAME).should('have.text', alert.processName); - cy.get(SOURCE_IP).should('have.text', alert.sourceIp); - cy.get(DESTINATION_IP).should('have.text', alert.destinationIp); - }); -}); diff --git a/x-pack/plugins/security_solution/cypress/upgrade_e2e/detections/detection_rules/threshold_rule.cy.ts b/x-pack/plugins/security_solution/cypress/upgrade_e2e/detections/detection_rules/threshold_rule.cy.ts deleted file mode 100644 index 78c34dfd43a7e..0000000000000 --- a/x-pack/plugins/security_solution/cypress/upgrade_e2e/detections/detection_rules/threshold_rule.cy.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import semver from 'semver'; -import { REASON, RISK_SCORE, RULE_NAME, SEVERITY } from '../../../screens/alerts'; -import { SERVER_SIDE_EVENT_COUNT } from '../../../screens/alerts_detection_rules'; -import { - ADDITIONAL_LOOK_BACK_DETAILS, - ABOUT_DETAILS, - ABOUT_RULE_DESCRIPTION, - CUSTOM_QUERY_DETAILS, - DEFINITION_DETAILS, - INDEX_PATTERNS_DETAILS, - RISK_SCORE_DETAILS, - RULE_NAME_HEADER, - RULE_TYPE_DETAILS, - RUNS_EVERY_DETAILS, - SCHEDULE_DETAILS, - SEVERITY_DETAILS, - THRESHOLD_DETAILS, - TIMELINE_TEMPLATE_DETAILS, -} from '../../../screens/rule_details'; - -import { getDetails } from '../../../tasks/rule_details'; -import { expandFirstAlert } from '../../../tasks/alerts'; -import { waitForPageToBeLoaded } from '../../../tasks/common'; -import { - goToTheRuleDetailsOf, - waitForRulesTableToBeLoaded, -} from '../../../tasks/alerts_detection_rules'; -import { login, visit } from '../../../tasks/login'; - -import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; -import { - OVERVIEW_RISK_SCORE, - OVERVIEW_RULE, - OVERVIEW_SEVERITY, - OVERVIEW_STATUS, - OVERVIEW_RULE_TYPE, -} from '../../../screens/alerts_details'; - -const EXPECTED_NUMBER_OF_ALERTS = '1'; - -const alert = { - rule: 'Threshold rule', - severity: 'medium', - riskScore: '17', - reason: 'event created medium alert Threshold rule.', - reasonAlt: '—', - hostName: 'security-solution.local', - thresholdCount: '2', -}; - -const rule = { - customQuery: '*:*', - name: 'Threshold rule', - description: 'Threshold rule for testing upgrade', - index: ['auditbeat-threshold*'], - severity: 'Medium', - riskScore: '17', - timelineTemplate: 'none', - runsEvery: '24h', - lookBack: '49976h', - timeline: 'None', - ruleType: 'threshold', - thresholdField: 'host.name', - thresholdValue: '1', -}; - -describe('After an upgrade, the threshold rule', () => { - before(() => { - login(); - visit(DETECTIONS_RULE_MANAGEMENT_URL); - waitForRulesTableToBeLoaded(); - goToTheRuleDetailsOf(rule.name); - waitForPageToBeLoaded(); - }); - - it('Has the expected alerts number', () => { - cy.get(SERVER_SIDE_EVENT_COUNT).contains(EXPECTED_NUMBER_OF_ALERTS); - }); - - it('Displays the rule details', () => { - cy.get(RULE_NAME_HEADER).should('contain', rule.name); - cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description); - cy.get(ABOUT_DETAILS).within(() => { - getDetails(SEVERITY_DETAILS).should('have.text', rule.severity); - getDetails(RISK_SCORE_DETAILS).should('have.text', rule.riskScore); - }); - cy.get(DEFINITION_DETAILS).within(() => { - getDetails(INDEX_PATTERNS_DETAILS).should('have.text', rule.index.join('')); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.customQuery); - getDetails(RULE_TYPE_DETAILS).should('have.text', 'Threshold'); - getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', rule.timeline); - getDetails(THRESHOLD_DETAILS).should( - 'have.text', - `Results aggregated by ${rule.thresholdField} >= ${rule.thresholdValue}` - ); - }); - cy.get(SCHEDULE_DETAILS).within(() => { - getDetails(RUNS_EVERY_DETAILS).should('have.text', rule.runsEvery); - getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should('have.text', rule.lookBack); - }); - }); - - it('Displays the alert details in the TGrid', () => { - let expectedReason = alert.reason; - if (semver.lt(Cypress.env('ORIGINAL_VERSION'), '7.15.0')) { - expectedReason = alert.reasonAlt; - } - cy.scrollTo('bottom'); - cy.get(RULE_NAME).should('have.text', alert.rule); - cy.get(SEVERITY).should('have.text', alert.severity); - cy.get(RISK_SCORE).should('have.text', alert.riskScore); - cy.get(REASON).contains(expectedReason); - // TODO: Needs data-test-subj - // cy.get(HOST_NAME).should('have.text', alert.hostName); - }); - - it('Displays the Overview alert details in the alert flyout', () => { - expandFirstAlert(); - - cy.get(OVERVIEW_STATUS).should('have.text', 'open'); - cy.get(OVERVIEW_RULE).should('have.text', alert.rule); - cy.get(OVERVIEW_SEVERITY).contains(alert.severity, { matchCase: false }); - cy.get(OVERVIEW_RISK_SCORE).should('have.text', alert.riskScore); - // TODO: Find out what this is - // cy.get(OVERVIEW_HOST_NAME).should('have.text', alert.hostName); - // TODO: Needs data-test-subj - // cy.get(OVERVIEW_THRESHOLD_COUNT).should('have.text', alert.thresholdCount); - cy.get(OVERVIEW_RULE_TYPE).should('have.text', rule.ruleType); - // TODO: Needs data-test-subj - // cy.get(OVERVIEW_THRESHOLD_VALUE).should('have.text', rule.thresholdValue); - }); -}); diff --git a/x-pack/plugins/security_solution/cypress/upgrade_e2e/threat_hunting/cases/import_case.cy.ts b/x-pack/plugins/security_solution/cypress/upgrade_e2e/threat_hunting/cases/import_case.cy.ts deleted file mode 100644 index 4eecc36fe928a..0000000000000 --- a/x-pack/plugins/security_solution/cypress/upgrade_e2e/threat_hunting/cases/import_case.cy.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - ALL_CASES_CLOSED_CASES_STATS, - ALL_CASES_COMMENTS_COUNT, - ALL_CASES_IN_PROGRESS_CASES_STATS, - ALL_CASES_NAME, - ALL_CASES_NOT_PUSHED, - ALL_CASES_NUMBER_OF_ALERTS, - ALL_CASES_OPEN_CASES_STATS, - ALL_CASES_IN_PROGRESS_STATUS, -} from '../../../screens/all_cases'; -import { - CASES_TAGS, - CASE_CONNECTOR, - CASE_DETAILS_PAGE_TITLE, - CASE_DETAILS_USERNAMES, - CASE_EVENT_UPDATE, - CASE_IN_PROGRESS_STATUS, - CASE_SWITCH, - CASE_USER_ACTION, -} from '../../../screens/case_details'; -import { CASES_PAGE } from '../../../screens/kibana_navigation'; - -import { goToCaseDetails } from '../../../tasks/all_cases'; -import { deleteCase } from '../../../tasks/case_details'; -import { - navigateFromKibanaCollapsibleTo, - openKibanaNavigation, -} from '../../../tasks/kibana_navigation'; -import { login, visitWithoutDateRange } from '../../../tasks/login'; -import { importCase } from '../../../tasks/saved_objects'; - -import { KIBANA_SAVED_OBJECTS } from '../../../urls/navigation'; - -const CASE_NDJSON = '7_16_case.ndjson'; -const importedCase = { - title: '7.16 case to export', - user: 'glo', - initial: 'g', - reporter: 'glo@test.co', - tags: 'export case', - numberOfAlerts: '1', - numberOfComments: '2', - description: - "This is the description of the 7.16 case that I'm going to import in future versions.", - timeline: 'This is just a timeline', - status: 'In progress', - ruleName: 'This is a test', - participants: ['test', 'elastic'], - connector: 'Jira test', -}; -const updateStatusRegex = new RegExp( - `\\S${importedCase.user}marked case as${importedCase.status}\\S*\\s?(\\S*)?\\s?(\\S*)?` -); -const alertUpdateRegex = new RegExp( - `\\S${importedCase.user}added an alert from Unknown\\S*\\s?(\\S*)?\\s?(\\S*)?` -); -const incidentManagementSystemRegex = new RegExp( - `\\S${importedCase.participants[0]}selected ${importedCase.connector} as incident management system\\S*\\s?(\\S*)?\\s?(\\S*)?` -); -const DESCRIPTION = 0; -const TIMELINE = 1; -const LENS = 2; -const STATUS_UPDATE = 0; -const FIRST_ALERT_UPDATE = 1; -const SECOND_ALERT_UPDATE = 2; -const INCIDENT_MANAGEMENT_SYSTEM_UPDATE = 3; -const EXPECTED_NUMBER_OF_UPDATES = 4; -const REPORTER = 0; - -describe('Import case after upgrade', () => { - before(() => { - login(); - visitWithoutDateRange(KIBANA_SAVED_OBJECTS); - importCase(CASE_NDJSON); - openKibanaNavigation(); - navigateFromKibanaCollapsibleTo(CASES_PAGE); - }); - - after(() => { - deleteCase(); - }); - - it('Displays the correct number of opened cases on the cases page', () => { - const EXPECTED_NUMBER_OF_OPENED_CASES = '0'; - cy.get(ALL_CASES_OPEN_CASES_STATS).should('have.text', EXPECTED_NUMBER_OF_OPENED_CASES); - }); - - it('Displays the correct number of in progress cases on the cases page', () => { - const EXPECTED_NUMBER_OF_IN_PROGRESS_CASES = '1'; - cy.get(ALL_CASES_IN_PROGRESS_CASES_STATS).should( - 'have.text', - EXPECTED_NUMBER_OF_IN_PROGRESS_CASES - ); - }); - - it('Displays the correct number of closed cases on the cases page', () => { - const EXPECTED_NUMBER_OF_CLOSED_CASES = '0'; - cy.get(ALL_CASES_CLOSED_CASES_STATS).should('have.text', EXPECTED_NUMBER_OF_CLOSED_CASES); - }); - - it('Displays the correct case details on the cases page', () => { - cy.get(ALL_CASES_NAME).should('have.text', importedCase.title); - cy.get(ALL_CASES_NUMBER_OF_ALERTS).should('have.text', importedCase.numberOfAlerts); - cy.get(ALL_CASES_COMMENTS_COUNT).should('have.text', importedCase.numberOfComments); - cy.get(ALL_CASES_NOT_PUSHED).should('be.visible'); - cy.get(ALL_CASES_IN_PROGRESS_STATUS).should('be.visible'); - }); - - it('Displays the correct case details on the case details page', () => { - goToCaseDetails(); - - cy.get(CASE_DETAILS_PAGE_TITLE).should('have.text', importedCase.title); - cy.get(CASE_IN_PROGRESS_STATUS).should('exist'); - cy.get(CASE_SWITCH).should('have.attr', 'aria-checked', 'false'); - cy.get(CASE_USER_ACTION).eq(DESCRIPTION).should('have.text', importedCase.description); - cy.get(CASE_USER_ACTION).eq(TIMELINE).should('have.text', importedCase.timeline); - cy.get(CASE_USER_ACTION).eq(LENS).should('have.text', ''); - cy.get(CASE_EVENT_UPDATE).should('have.length', EXPECTED_NUMBER_OF_UPDATES); - cy.get(CASE_EVENT_UPDATE).eq(STATUS_UPDATE).invoke('text').should('match', updateStatusRegex); - cy.get(CASE_EVENT_UPDATE) - .eq(FIRST_ALERT_UPDATE) - .invoke('text') - .should('match', alertUpdateRegex); - cy.get(CASE_EVENT_UPDATE) - .eq(SECOND_ALERT_UPDATE) - .invoke('text') - .should('match', alertUpdateRegex); - cy.get(CASE_EVENT_UPDATE) - .eq(INCIDENT_MANAGEMENT_SYSTEM_UPDATE) - .invoke('text') - .should('match', incidentManagementSystemRegex); - // TODO: Needs data-test-subj - // cy.get(CASE_DETAILS_USERNAMES).should('have.length', EXPECTED_NUMBER_OF_PARTICIPANTS); - // TODO: Investigate why this changes, not reliable to verify - // cy.get(CASE_DETAILS_USERNAMES).eq(FIRST_PARTICIPANT).should('have.text', importedCase.user); - // cy.get(CASE_DETAILS_USERNAMES) - // .eq(SECOND_PARTICIPANT) - // .should('have.text', importedCase.participants[0]); - // cy.get(CASE_DETAILS_USERNAMES) - // .eq(THIRD_PARTICIPANT) - // .should('have.text', importedCase.participants[1]); - cy.get(CASE_DETAILS_USERNAMES).eq(REPORTER).should('have.text', importedCase.user); - cy.get(CASES_TAGS(importedCase.tags)).should('exist'); - cy.get(CASE_CONNECTOR).should('have.text', importedCase.connector); - }); -}); diff --git a/x-pack/plugins/security_solution/cypress/upgrade_e2e/threat_hunting/timeline/import_timeline.cy.ts b/x-pack/plugins/security_solution/cypress/upgrade_e2e/threat_hunting/timeline/import_timeline.cy.ts deleted file mode 100644 index 476ad34652a63..0000000000000 --- a/x-pack/plugins/security_solution/cypress/upgrade_e2e/threat_hunting/timeline/import_timeline.cy.ts +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import semver from 'semver'; -import { - CORRELATION_EVENT_TABLE_CELL, - DATA_PROVIDERS, - DATE_PICKER_END, - DATE_PICKER_START, - DESTINATION_IP_KPI, - GRAPH_TAB_BUTTON, - HOST_KPI, - QUERY_TAB_BUTTON, - NOTE_DESCRIPTION, - NOTE_PREVIEW, - NOTES_TAB_BUTTON, - PINNED_TAB_BUTTON, - PROCESS_KPI, - QUERY_EVENT_TABLE_CELL, - SOURCE_IP_KPI, - TIMELINE_CORRELATION_TAB, - TIMELINE_CORRELATION_INPUT, - TIMELINE_DESCRIPTION, - TIMELINE_QUERY, - TIMELINE_TITLE, - USER_KPI, -} from '../../../screens/timeline'; -import { - NOTE, - TIMELINES_USERNAME, - TIMELINE_NAME, - TIMELINES_DESCRIPTION, - TIMELINES_NOTES_COUNT, - TIMELINES_PINNED_EVENT_COUNT, -} from '../../../screens/timelines'; - -import { login, visitWithoutDateRange } from '../../../tasks/login'; -import { - closeTimeline, - deleteTimeline, - goToCorrelationTab, - goToNotesTab, - setKibanaTimezoneToUTC, -} from '../../../tasks/timeline'; -import { expandNotes, importTimeline, openTimeline } from '../../../tasks/timelines'; - -import { TIMELINES_URL } from '../../../urls/navigation'; - -const timeline = '7_15_timeline.ndjson'; -const username = 'elastic'; - -const timelineDetails = { - dateStart: 'Oct 10, 2020 @ 22:00:00.000', - dateEnd: 'Oct 11, 2030 @ 15:13:15.851', - queryTab: 'Query4', - queryTabAlt: 'Query2', - correlationTab: 'Correlation', - analyzerTab: 'Analyzer', - notesTab: 'Notes2', - pinnedTab: 'Pinned1', -}; - -const detectionAlert = { - message: '—', - eventCategory: 'file', - eventAction: 'initial_scan', - hostName: 'security-solution.local', - sourceIp: '127.0.0.1', - destinationIp: '127.0.0.2', - userName: 'Security Solution', -}; - -const event = { - timestamp: 'Nov 4, 2021 @ 10:09:29.438', - message: '—', - eventCategory: 'file', - eventAction: 'initial_scan', - hostName: 'security-solution.local', - sourceIp: '127.0.0.1', - destinationIp: '127.0.0.2', - userName: 'Security Solution', -}; - -describe('Import timeline after upgrade', () => { - before(() => { - login(); - visitWithoutDateRange(TIMELINES_URL); - importTimeline(timeline); - setKibanaTimezoneToUTC(); - }); - - after(() => { - closeTimeline(); - deleteTimeline(); - }); - - it('Displays the correct timeline details on the timelines page', () => { - cy.readFile(`cypress/fixtures/${timeline}`).then((file) => { - const timelineJson = JSON.parse(file); - const regex = new RegExp( - `\\S${timelineJson.globalNotes[0].createdBy}added a note\\S*\\s?(\\S*)?\\s?(\\S*)?${timelineJson.globalNotes[0].createdBy} added a note${timelineJson.globalNotes[0].note}` - ); - - cy.get(TIMELINE_NAME).should('have.text', timelineJson.title); - cy.get(TIMELINES_DESCRIPTION).should('have.text', timelineJson.description); - cy.get(TIMELINES_USERNAME).should('have.text', username); - cy.get(TIMELINES_NOTES_COUNT).should('have.text', timelineJson.globalNotes.length.toString()); - cy.get(TIMELINES_PINNED_EVENT_COUNT).should( - 'have.text', - timelineJson.pinnedEventIds.length.toString() - ); - - expandNotes(); - - cy.get(NOTE).invoke('text').should('match', regex); - }); - }); - - it('Displays the correct timeline details inside the query tab', () => { - let expectedQueryTab = timelineDetails.queryTab; - if (semver.lt(Cypress.env('ORIGINAL_VERSION'), '7.10.0')) { - expectedQueryTab = timelineDetails.queryTabAlt; - } - - openTimeline(); - - cy.readFile(`cypress/fixtures/${timeline}`).then((file) => { - const timelineJson = JSON.parse(file); - - cy.get(TIMELINE_TITLE).should('have.text', timelineJson.title); - cy.get(TIMELINE_DESCRIPTION).should('have.text', timelineJson.description); - cy.get(DATA_PROVIDERS).should('have.length', timelineJson.dataProviders.length.toString()); - cy.get(DATA_PROVIDERS) - .invoke('text') - .then((value) => { - expect(value.replace(/"/g, '')).to.eq(timelineJson.dataProviders[0].name); - }); - cy.get(PROCESS_KPI).should('contain', '0'); - cy.get(USER_KPI).should('contain', '0'); - cy.get(HOST_KPI).should('contain', '1'); - cy.get(SOURCE_IP_KPI).should('contain', '1'); - cy.get(DESTINATION_IP_KPI).should('contain', '1'); - cy.get(DATE_PICKER_START).should('contain', timelineDetails.dateStart); - cy.get(DATE_PICKER_END).should('contain', timelineDetails.dateEnd); - cy.get(TIMELINE_QUERY).should( - 'have.text', - timelineJson.kqlQuery.filterQuery.kuery.expression - ); - cy.get(QUERY_TAB_BUTTON).should('have.text', expectedQueryTab); - cy.get(TIMELINE_CORRELATION_TAB).should('have.text', timelineDetails.correlationTab); - cy.get(GRAPH_TAB_BUTTON).should('have.text', timelineDetails.analyzerTab).and('be.disabled'); - cy.get(NOTES_TAB_BUTTON).should('have.text', timelineDetails.notesTab); - cy.get(PINNED_TAB_BUTTON).should('have.text', timelineDetails.pinnedTab); - - cy.get(QUERY_EVENT_TABLE_CELL).eq(1).should('contain', detectionAlert.message); - cy.get(QUERY_EVENT_TABLE_CELL).eq(2).should('contain', detectionAlert.eventCategory); - cy.get(QUERY_EVENT_TABLE_CELL).eq(3).should('contain', detectionAlert.eventAction); - cy.get(QUERY_EVENT_TABLE_CELL).eq(4).should('contain', detectionAlert.hostName); - cy.get(QUERY_EVENT_TABLE_CELL).eq(5).should('contain', detectionAlert.sourceIp); - cy.get(QUERY_EVENT_TABLE_CELL).eq(6).should('contain', detectionAlert.destinationIp); - cy.get(QUERY_EVENT_TABLE_CELL).eq(7).should('contain', detectionAlert.userName); - - cy.get(QUERY_EVENT_TABLE_CELL).eq(8).should('contain', event.timestamp); - cy.get(QUERY_EVENT_TABLE_CELL).eq(9).should('contain', event.message); - cy.get(QUERY_EVENT_TABLE_CELL).eq(10).should('contain', event.eventCategory); - cy.get(QUERY_EVENT_TABLE_CELL).eq(11).should('contain', event.eventAction); - cy.get(QUERY_EVENT_TABLE_CELL).eq(12).should('contain', event.hostName); - cy.get(QUERY_EVENT_TABLE_CELL).eq(13).should('contain', event.sourceIp); - cy.get(QUERY_EVENT_TABLE_CELL).eq(14).should('contain', event.destinationIp); - cy.get(QUERY_EVENT_TABLE_CELL).eq(15).should('contain', event.userName); - }); - }); - - it('Displays the correct timeline details inside the correlation tab', () => { - goToCorrelationTab(); - - cy.get(TIMELINE_CORRELATION_INPUT).should('be.empty'); - cy.get(CORRELATION_EVENT_TABLE_CELL).should('not.exist'); - }); - - it('Displays the correct timeline details inside the notes tab', () => { - goToNotesTab(); - - cy.readFile(`cypress/fixtures/${timeline}`).then((file) => { - const timelineJson = JSON.parse(file); - const descriptionRegex = new RegExp( - `\\S${username}added description\\S*\\s?(\\S*)?\\s?(\\S*)?${timelineJson.description}` - ); - const noteRegex = new RegExp( - `\\S${timelineJson.globalNotes[0].createdBy}added a note\\S*\\s?(\\S*)?\\s?(\\S*)?${timelineJson.globalNotes[0].createdBy} added a note${timelineJson.globalNotes[0].note}` - ); - - cy.get(NOTE_DESCRIPTION).invoke('text').should('match', descriptionRegex); - cy.get(NOTE_PREVIEW).last().invoke('text').should('match', noteRegex); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index a564416154176..19c02e030d391 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -8,23 +8,19 @@ "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ./public/detections/mitre/mitre_tactics_techniques.ts --fix", "build-beat-doc": "node scripts/beat_docs/build.js && node ../../../scripts/eslint ../timelines/server/utils/beat_schema/fields.ts --fix", "cypress": "../../../node_modules/.bin/cypress", + "cypress:burn": "yarn cypress:run:reporter --env burn=2 --concurrency=1 --headed", + "cypress:changed-specs-only": "yarn cypress:run:reporter --changed-specs-only --env burn=2", "cypress:open": "TZ=UTC node ./scripts/start_cypress_parallel open --spec './cypress/e2e/**/*.cy.ts' --config-file ./cypress/cypress.config.ts --ftr-config-file ../../../../../../x-pack/test/security_solution_cypress/cli_config", - "cypress:open:ccs": "yarn cypress:open --config specPattern=./cypress/ccs_e2e/**/*.cy.ts", - "cypress:open:upgrade": "yarn cypress:open --config specPattern=./cypress/upgrade_e2e/**/*.cy.ts", - "cypress:run": "yarn cypress:run:reporter --browser chrome --spec './cypress/e2e/{,!(investigations,explore)/**/}*.cy.ts'; status=$?; yarn junit:merge && exit $status", - "cypress:run:cases": "yarn cypress:run:reporter --browser chrome --spec './cypress/e2e/explore/cases/*.cy.ts' --ftr-config-file ../../../../../../x-pack/test/security_solution_cypress/cli_config; status=$?; yarn junit:merge && exit $status", - "cypress:run:firefox": "yarn cypress:run:reporter --browser firefox --spec './cypress/e2e/**/*.cy.ts'; status=$?; yarn junit:merge && exit $status", - "cypress:run:reporter": "TZ=UTC node ./scripts/start_cypress_parallel run --config-file ./cypress/cypress_ci.config.ts --ftr-config-file ../../../../../../x-pack/test/security_solution_cypress/cli_config --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json", - "cypress:run:respops": "yarn cypress:run:reporter --browser chrome --spec './cypress/e2e/(detection_alerts|detection_rules|exceptions)/*.cy.ts' --ftr-config-file ../../../../../../x-pack/test/security_solution_cypress/cli_config; status=$?; yarn junit:merge && exit $status", - "cypress:run:ccs": "yarn cypress:run:reporter --browser chrome --config specPattern=./cypress/ccs_e2e/**/*.cy.ts; status=$?; yarn junit:merge && exit $status", - "cypress:run-as-ci:firefox": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/security_solution_cypress/config.firefox.ts", - "cypress:run:upgrade": "yarn cypress:run:reporter --browser chrome --config specPattern=./cypress/upgrade_e2e/**/*.cy.ts", + "cypress:run": "yarn cypress:run:reporter --spec './cypress/e2e/{,!(investigations,explore)/**/}*.cy.ts'; status=$?; yarn junit:merge && exit $status", + "cypress:run:cases": "yarn cypress:run:reporter --spec './cypress/e2e/explore/cases/*.cy.ts' --ftr-config-file ../../../../../../x-pack/test/security_solution_cypress/cli_config; status=$?; yarn junit:merge && exit $status", + "cypress:run:reporter": "TZ=UTC node ./scripts/start_cypress_parallel run --browser chrome --config-file ./cypress/cypress_ci.config.ts --ftr-config-file ../../../../../../x-pack/test/security_solution_cypress/cli_config --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json", + "cypress:run:respops": "yarn cypress:run:reporter --spec './cypress/e2e/(detection_alerts|detection_rules|exceptions)/*.cy.ts' --ftr-config-file ../../../../../../x-pack/test/security_solution_cypress/cli_config; status=$?; yarn junit:merge && exit $status", "cypress:dw:open": "node ./scripts/start_cypress_parallel open --config-file ./public/management/cypress.config.ts ts --ftr-config-file ../../../../../../x-pack/test/defend_workflows_cypress/cli_config", "cypress:dw:run": "node ./scripts/start_cypress_parallel run --config-file ./public/management/cypress.config.ts --ftr-config-file ../../../../../../x-pack/test/defend_workflows_cypress/cli_config --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; yarn junit:merge && exit $status", "cypress:dw:endpoint:run": "node ./scripts/start_cypress_parallel run --config-file ./public/management/cypress_endpoint.config.ts --ftr-config-file ../../../../../../x-pack/test/defend_workflows_cypress/endpoint_config --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json --concurrency 1; status=$?; yarn junit:merge && exit $status", "cypress:dw:endpoint:open": "node ./scripts/start_cypress_parallel open --config-file ./public/management/cypress_endpoint.config.ts ts --ftr-config-file ../../../../../../x-pack/test/defend_workflows_cypress/endpoint_config", - "cypress:investigations:run": "yarn cypress:run:reporter --browser chrome --spec './cypress/e2e/investigations/**/*.cy.ts' --ftr-config-file ../../../../../../x-pack/test/security_solution_cypress/cli_config; status=$?; yarn junit:merge && exit $status", - "cypress:explore:run": "yarn cypress:run:reporter --browser chrome --spec './cypress/e2e/explore/**/*.cy.ts' --ftr-config-file ../../../../../../x-pack/test/security_solution_cypress/cli_config; status=$?; yarn junit:merge && exit $status", + "cypress:investigations:run": "yarn cypress:run:reporter --spec './cypress/e2e/investigations/**/*.cy.ts' --ftr-config-file ../../../../../../x-pack/test/security_solution_cypress/cli_config; status=$?; yarn junit:merge && exit $status", + "cypress:explore:run": "yarn cypress:run:reporter --spec './cypress/e2e/explore/**/*.cy.ts' --ftr-config-file ../../../../../../x-pack/test/security_solution_cypress/cli_config; status=$?; yarn junit:merge && exit $status", "junit:merge": "../../../node_modules/.bin/mochawesome-merge ../../../target/kibana-security-solution/cypress/results/mochawesome*.json > ../../../target/kibana-security-solution/cypress/results/output.json && ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results && yarn junit:transform && mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/", "test:generate": "node scripts/endpoint/resolver_generator", "mappings:generate": "node scripts/mappings/mappings_generator", diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx index eadf94552794e..943bf4dab0d80 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx @@ -187,7 +187,7 @@ const LensEmbeddableComponent: React.FC = ({ const onFilterCallback = useCallback( async (e: ClickTriggerEvent['data'] | MultiClickTriggerEvent['data']) => { - if (!Array.isArray(e.data) || preferredSeriesType !== 'area') { + if (!isClickTriggerEvent(e) || preferredSeriesType !== 'area') { return; } // Update timerange when clicking on a dot in an area chart @@ -301,4 +301,10 @@ const LensEmbeddableComponent: React.FC = ({ ); }; +const isClickTriggerEvent = ( + e: ClickTriggerEvent['data'] | MultiClickTriggerEvent['data'] +): e is ClickTriggerEvent['data'] => { + return Array.isArray(e.data) && 'column' in e.data[0]; +}; + export const LensEmbeddable = React.memo(LensEmbeddableComponent); diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx index 84c94f81d026f..c5251dba05744 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx @@ -67,6 +67,7 @@ describe('source/index.tsx', () => { }); }, getFieldsForWildcard: async () => Promise.resolve(), + getExistingIndices: async (indices: string[]) => Promise.resolve(indices), }, search: { search: jest.fn().mockReturnValue({ diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/get_sourcerer_data_view.test.ts b/x-pack/plugins/security_solution/public/common/containers/sourcerer/get_sourcerer_data_view.test.ts new file mode 100644 index 0000000000000..cc4921380fac8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/get_sourcerer_data_view.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getSourcererDataView } from './get_sourcerer_data_view'; +import type { DataViewsService } from '@kbn/data-views-plugin/common'; + +const dataViewId = 'test-id'; +const dataViewsService = { + get: jest.fn().mockResolvedValue({ + toSpec: jest.fn().mockReturnValue({ + id: 'test-id', + fields: {}, + runtimeFieldMap: {}, + }), + getIndexPattern: jest.fn().mockReturnValue('test-pattern'), + fields: {}, + }), + getExistingIndices: jest.fn().mockResolvedValue(['test-pattern']), +} as unknown as jest.Mocked; +describe('getSourcererDataView', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + // Tests that the function returns a SourcererDataView object with the expected properties + it('should return a SourcererDataView object with the expected properties', async () => { + const result = await getSourcererDataView(dataViewId, dataViewsService); + expect(result).toEqual({ + loading: false, + id: 'test-id', + title: 'test-pattern', + indexFields: {}, + fields: {}, + patternList: ['test-pattern'], + dataView: { + id: 'test-id', + fields: {}, + runtimeFieldMap: {}, + }, + browserFields: {}, + runtimeMappings: {}, + }); + }); + it('should call dataViewsService.get with the correct arguments', async () => { + await getSourcererDataView(dataViewId, dataViewsService); + expect(dataViewsService.get).toHaveBeenCalledWith(dataViewId, true, false); + }); + + it('should call dataViewsService.getExistingIndices with the correct arguments', async () => { + await getSourcererDataView(dataViewId, dataViewsService); + expect(dataViewsService.getExistingIndices).toHaveBeenCalledWith(['test-pattern']); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/get_sourcerer_data_view.ts b/x-pack/plugins/security_solution/public/common/containers/sourcerer/get_sourcerer_data_view.ts index 7f40819bfb41d..a3cc1a8041b4c 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/get_sourcerer_data_view.ts +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/get_sourcerer_data_view.ts @@ -18,30 +18,7 @@ export const getSourcererDataView = async ( const dataView = await dataViewsService.get(dataViewId, true, refreshFields); const dataViewData = dataView.toSpec(); const defaultPatternsList = ensurePatternFormat(dataView.getIndexPattern().split(',')); - - // typeguard used to assert that pattern is a string, otherwise - // typescript expects patternList to be (string | null)[] - // but we want it to always be string[] - const filterTypeGuard = (str: unknown): str is string => str != null; - const patternList = await Promise.all( - defaultPatternsList.map(async (pattern) => { - try { - await dataViewsService.getFieldsForWildcard({ - type: dataViewData.type, - rollupIndex: dataViewData?.typeMeta?.params?.rollup_index, - allowNoIndex: false, - pattern, - }); - return pattern; - } catch { - return null; - } - }) - ) - .then((allPatterns) => - allPatterns.filter((pattern): pattern is string => filterTypeGuard(pattern)) - ) - .catch(() => defaultPatternsList); + const patternList = await dataViewsService.getExistingIndices(defaultPatternsList); return { loading: false, diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index cf00693623c43..d3007d0d6346a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -46,8 +46,9 @@ const FlyoutPaneComponent: React.FC = ({ data-test-subj="timeline-flyout" css={css` min-width: 150px; - height: calc(100% - 96px); - top: 96px; + height: 100%; + top: 0; + left: 0; background: ${useEuiBackgroundColor('plain')}; position: fixed; width: 100%; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/use_sub_action.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/use_sub_action.tsx new file mode 100644 index 0000000000000..44f9099a5b816 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/use_sub_action.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { executeAction } from '@kbn/triggers-actions-ui-plugin/public'; +import { useQuery } from '@tanstack/react-query'; +import { useKibana } from '../../../../../common/lib/kibana/kibana_react'; + +export interface UseSubActionParams

{ + connectorId: string; + subAction: string; + subActionParams?: P; + disabled?: boolean; +} + +export const useSubAction = ({ + connectorId, + subAction, + subActionParams, + disabled = false, + ...rest +}: UseSubActionParams

) => { + const { http } = useKibana().services; + + return useQuery({ + queryKey: ['useSubAction', connectorId, subAction, subActionParams], + queryFn: ({ signal }) => + executeAction({ + id: connectorId, + params: { + subAction, + subActionParams, + }, + http, + signal, + }), + enabled: !disabled && !!connectorId && !!subAction, + ...rest, + }); +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/use_sub_action_mutation.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/use_sub_action_mutation.tsx new file mode 100644 index 0000000000000..78c48ccca1491 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/use_sub_action_mutation.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { executeAction } from '@kbn/triggers-actions-ui-plugin/public'; +import { useMutation } from '@tanstack/react-query'; +import { useKibana } from '../../../../../common/lib/kibana/kibana_react'; + +export interface UseSubActionParams

{ + connectorId: string; + subAction: string; + subActionParams?: P; + disabled?: boolean; +} + +export const useSubActionMutation = ({ + connectorId, + subAction, + subActionParams, + disabled = false, +}: UseSubActionParams

) => { + const { http } = useKibana().services; + + return useMutation({ + mutationFn: () => + executeAction({ + id: connectorId, + params: { + subAction, + subActionParams, + }, + http, + }), + }); +}; diff --git a/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts b/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts index 351b38f91f47e..cf2f3897bd5ab 100644 --- a/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts +++ b/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts @@ -13,6 +13,9 @@ import pMap from 'p-map'; import { ToolingLog } from '@kbn/tooling-log'; import { withProcRunner } from '@kbn/dev-proc-runner'; import cypress from 'cypress'; +import { findChangedFiles } from 'find-cypress-specs'; +import minimatch from 'minimatch'; +import path from 'path'; import { EsVersion, @@ -68,13 +71,39 @@ const retrieveIntegrations = ( export const cli = () => { run( async () => { - const { argv } = yargs(process.argv.slice(2)); + const { argv } = yargs(process.argv.slice(2)).coerce('env', (arg: string) => + arg.split(',').reduce((acc, curr) => { + const [key, value] = curr.split('='); + if (key === 'burn') { + acc[key] = parseInt(value, 10); + } else { + acc[key] = value; + } + return acc; + }, {} as Record) + ); const isOpen = argv._[0] === 'open'; const cypressConfigFilePath = require.resolve(`../../${argv.configFile}`) as string; const cypressConfigFile = await import(require.resolve(`../../${argv.configFile}`)); const spec: string | undefined = argv?.spec as string; - const files = retrieveIntegrations(spec ? [spec] : cypressConfigFile?.e2e?.specPattern); + let files = retrieveIntegrations(spec ? [spec] : cypressConfigFile?.e2e?.specPattern); + + if (argv.changedSpecsOnly) { + const basePath = process.cwd().split('kibana/')[1]; + files = findChangedFiles('main', false) + .filter( + minimatch.filter(path.join(basePath, cypressConfigFile?.e2e?.specPattern), { + matchBase: true, + }) + ) + .map((filePath: string) => filePath.replace(basePath, '.')); + + if (!files?.length) { + // eslint-disable-next-line no-process-exit + return process.exit(0); + } + } if (!files?.length) { throw new Error('No files found'); @@ -323,8 +352,8 @@ ${JSON.stringify(config.getAll(), null, 2)} type: 'elasticsearch' | 'kibana' | 'fleetserver', withAuth: boolean = false ): string => { - const getKeyPath = (path: string = ''): string => { - return `servers.${type}${path ? `.${path}` : ''}`; + const getKeyPath = (keyPath: string = ''): string => { + return `servers.${type}${keyPath ? `.${keyPath}` : ''}`; }; if (!config.get(getKeyPath())) { @@ -361,7 +390,7 @@ ${JSON.stringify(config.getAll(), null, 2)} ...ftrEnv, // NOTE: - // ELASTICSEARCH_URL needs to be crated here with auth because SIEM cypress setup depends on it. At some + // ELASTICSEARCH_URL needs to be created here with auth because SIEM cypress setup depends on it. At some // points we should probably try to refactor that code to use `ELASTICSEARCH_URL_WITH_AUTH` instead ELASTICSEARCH_URL: ftrEnv.ELASTICSEARCH_URL ?? createUrlFromFtrConfig('elasticsearch', true), @@ -377,6 +406,8 @@ ${JSON.stringify(config.getAll(), null, 2)} KIBANA_URL_WITH_AUTH: createUrlFromFtrConfig('kibana', true), KIBANA_USERNAME: config.get('servers.kibana.username'), KIBANA_PASSWORD: config.get('servers.kibana.password'), + + ...argv.env, }; log.info(` @@ -407,6 +438,7 @@ ${JSON.stringify(cyCustomEnv, null, 2)} configFile: cypressConfigFilePath, reporter: argv.reporter as string, reporterOptions: argv.reporterOptions, + headed: argv.headed as boolean, config: { e2e: { baseUrl, diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_dashboards_by_tags.ts b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_dashboards_by_tags.ts index 7e0f8bfdaa4ce..356e4c1996e24 100644 --- a/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_dashboards_by_tags.ts +++ b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_dashboards_by_tags.ts @@ -6,7 +6,6 @@ */ import type { Logger } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; -import { schema } from '@kbn/config-schema'; import type { DashboardAttributes } from '@kbn/dashboard-plugin/common'; import { transformError } from '@kbn/securitysolution-es-utils'; @@ -15,10 +14,8 @@ import type { SetupPlugins } from '../../../plugin'; import type { SecuritySolutionPluginRouter } from '../../../types'; import { buildSiemResponse } from '../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../timeline/utils/common'; - -const getDashboardsParamsSchema = schema.object({ - tagIds: schema.arrayOf(schema.string()), -}); +import { getDashboardsRequest } from '../../../../common/api/tags'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; export const getDashboardsByTagsRoute = ( router: SecuritySolutionPluginRouter, @@ -28,7 +25,7 @@ export const getDashboardsByTagsRoute = ( router.post( { path: INTERNAL_DASHBOARDS_URL, - validate: { body: getDashboardsParamsSchema }, + validate: { body: buildRouteValidationWithExcess(getDashboardsRequest) }, options: { tags: ['access:securitySolution'], }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/strip_non_ecs_fields.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/strip_non_ecs_fields.test.ts index 1d9d349dd0b5a..de4e9982c852d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/strip_non_ecs_fields.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/strip_non_ecs_fields.test.ts @@ -551,4 +551,72 @@ describe('stripNonEcsFields', () => { expect(removed).toEqual([]); }); }); + + // geo_point is too complex so we going to skip its validation + describe('geo_point field', () => { + it('should not strip invalid geo_point field', () => { + const { result, removed } = stripNonEcsFields({ + 'client.location.geo': 'invalid geo_point', + }); + + expect(result).toEqual({ + 'client.location.geo': 'invalid geo_point', + }); + expect(removed).toEqual([]); + }); + + it('should not strip valid geo_point fields', () => { + expect( + stripNonEcsFields({ + 'client.geo.location': [0, 90], + }).result + ).toEqual({ + 'client.geo.location': [0, 90], + }); + + expect( + stripNonEcsFields({ + 'client.geo.location': { + type: 'Point', + coordinates: [-88.34, 20.12], + }, + }).result + ).toEqual({ + 'client.geo.location': { + type: 'Point', + coordinates: [-88.34, 20.12], + }, + }); + + expect( + stripNonEcsFields({ + 'client.geo.location': 'POINT (-71.34 41.12)', + }).result + ).toEqual({ + 'client.geo.location': 'POINT (-71.34 41.12)', + }); + + expect( + stripNonEcsFields({ + client: { + geo: { + location: { + lat: 41.12, + lon: -71.34, + }, + }, + }, + }).result + ).toEqual({ + client: { + geo: { + location: { + lat: 41.12, + lon: -71.34, + }, + }, + }, + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/strip_non_ecs_fields.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/strip_non_ecs_fields.ts index 975b2b643a4e7..62e5c9211c1be 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/strip_non_ecs_fields.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/strip_non_ecs_fields.ts @@ -57,10 +57,11 @@ const ecsObjectFields = getEcsObjectFields(); /** * checks if path is a valid Ecs object type (object or flattened) + * geo_point also can be object */ const getIsEcsFieldObject = (path: string) => { const ecsField = ecsFieldMap[path as keyof typeof ecsFieldMap]; - return ['object', 'flattened'].includes(ecsField?.type) || ecsObjectFields[path]; + return ['object', 'flattened', 'geo_point'].includes(ecsField?.type) || ecsObjectFields[path]; }; /** @@ -117,6 +118,11 @@ const computeIsEcsCompliant = (value: SourceField, path: string) => { const ecsField = ecsFieldMap[path as keyof typeof ecsFieldMap]; const isEcsFieldObject = getIsEcsFieldObject(path); + // do not validate geo_point, since it's very complex type that can be string/array/object + if (ecsField?.type === 'geo_point') { + return true; + } + // validate if value is a long type if (ecsField?.type === 'long') { return isValidLongType(value); diff --git a/x-pack/plugins/security_solution/server/lib/tags/routes/create_tag.ts b/x-pack/plugins/security_solution/server/lib/tags/routes/create_tag.ts index 4eaed27813214..6ddc14d64d53f 100644 --- a/x-pack/plugins/security_solution/server/lib/tags/routes/create_tag.ts +++ b/x-pack/plugins/security_solution/server/lib/tags/routes/create_tag.ts @@ -6,22 +6,17 @@ */ import type { Logger } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; -import { schema } from '@kbn/config-schema'; import { transformError } from '@kbn/securitysolution-es-utils'; +import { createTagRequest } from '../../../../common/api/tags'; import { INTERNAL_TAGS_URL } from '../../../../common/constants'; import type { SetupPlugins } from '../../../plugin'; import type { SecuritySolutionPluginRouter } from '../../../types'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { buildSiemResponse } from '../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../timeline/utils/common'; import { createTag } from '../saved_objects'; -const createTagBodySchema = schema.object({ - name: schema.string(), - description: schema.string(), - color: schema.maybe(schema.string()), -}); - export const createTagRoute = ( router: SecuritySolutionPluginRouter, logger: Logger, @@ -30,7 +25,7 @@ export const createTagRoute = ( router.put( { path: INTERNAL_TAGS_URL, - validate: { body: createTagBodySchema }, + validate: { body: buildRouteValidationWithExcess(createTagRequest) }, options: { tags: ['access:securitySolution'], }, diff --git a/x-pack/plugins/security_solution/server/lib/tags/routes/get_tags_by_name.ts b/x-pack/plugins/security_solution/server/lib/tags/routes/get_tags_by_name.ts index 05d20d90e09b2..a88578365f037 100644 --- a/x-pack/plugins/security_solution/server/lib/tags/routes/get_tags_by_name.ts +++ b/x-pack/plugins/security_solution/server/lib/tags/routes/get_tags_by_name.ts @@ -6,20 +6,17 @@ */ import type { Logger } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; -import { schema } from '@kbn/config-schema'; import { transformError } from '@kbn/securitysolution-es-utils'; +import { getTagsByNameRequest } from '../../../../common/api/tags'; import { INTERNAL_TAGS_URL } from '../../../../common/constants'; import type { SetupPlugins } from '../../../plugin'; import type { SecuritySolutionPluginRouter } from '../../../types'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { buildSiemResponse } from '../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../timeline/utils/common'; import { findTagsByName } from '../saved_objects'; -const getTagsParamsSchema = schema.object({ - name: schema.string(), -}); - export const getTagsByNameRoute = ( router: SecuritySolutionPluginRouter, logger: Logger, @@ -28,7 +25,7 @@ export const getTagsByNameRoute = ( router.get( { path: INTERNAL_TAGS_URL, - validate: { query: getTagsParamsSchema }, + validate: { query: buildRouteValidationWithExcess(getTagsByNameRequest) }, options: { tags: ['access:securitySolution'], }, diff --git a/x-pack/plugins/stack_connectors/common/sentinelone/constants.ts b/x-pack/plugins/stack_connectors/common/sentinelone/constants.ts new file mode 100644 index 0000000000000..a77e070a71056 --- /dev/null +++ b/x-pack/plugins/stack_connectors/common/sentinelone/constants.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const SENTINELONE_TITLE = 'Sentinel One'; +export const SENTINELONE_CONNECTOR_ID = '.sentinelone'; +export const API_MAX_RESULTS = 1000; + +export enum SUB_ACTION { + KILL_PROCESS = 'killProcess', + EXECUTE_SCRIPT = 'executeScript', + GET_AGENTS = 'getAgents', + ISOLATE_AGENT = 'isolateAgent', + RELEASE_AGENT = 'releaseAgent', + GET_REMOTE_SCRIPTS = 'getRemoteScripts', + GET_REMOTE_SCRIPT_STATUS = 'getRemoteScriptStatus', + GET_REMOTE_SCRIPT_RESULTS = 'getRemoteScriptResults', +} diff --git a/x-pack/plugins/stack_connectors/common/sentinelone/schema.ts b/x-pack/plugins/stack_connectors/common/sentinelone/schema.ts new file mode 100644 index 0000000000000..f475a9e6a83f6 --- /dev/null +++ b/x-pack/plugins/stack_connectors/common/sentinelone/schema.ts @@ -0,0 +1,495 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { schema } from '@kbn/config-schema'; +import { SUB_ACTION } from './constants'; + +// Connector schema +export const SentinelOneConfigSchema = schema.object({ url: schema.string() }); +export const SentinelOneSecretsSchema = schema.object({ + token: schema.string(), +}); + +export const SentinelOneBaseApiResponseSchema = schema.object({}, { unknowns: 'allow' }); + +export const SentinelOneGetAgentsResponseSchema = schema.object({ + pagination: schema.object({ + totalItems: schema.number(), + nextCursor: schema.nullable(schema.string()), + }), + errors: schema.nullable(schema.arrayOf(schema.string())), + data: schema.arrayOf( + schema.object({ + modelName: schema.string(), + firewallEnabled: schema.boolean(), + totalMemory: schema.number(), + osName: schema.string(), + cloudProviders: schema.recordOf(schema.string(), schema.any()), + siteName: schema.string(), + cpuId: schema.string(), + isPendingUninstall: schema.boolean(), + isUpToDate: schema.boolean(), + osArch: schema.string(), + accountId: schema.string(), + locationEnabled: schema.boolean(), + consoleMigrationStatus: schema.string(), + scanFinishedAt: schema.nullable(schema.string()), + operationalStateExpiration: schema.nullable(schema.string()), + agentVersion: schema.string(), + isActive: schema.boolean(), + locationType: schema.string(), + activeThreats: schema.number(), + inRemoteShellSession: schema.boolean(), + allowRemoteShell: schema.boolean(), + serialNumber: schema.nullable(schema.string()), + updatedAt: schema.string(), + lastActiveDate: schema.string(), + firstFullModeTime: schema.nullable(schema.string()), + operationalState: schema.string(), + externalId: schema.string(), + mitigationModeSuspicious: schema.string(), + licenseKey: schema.string(), + cpuCount: schema.number(), + mitigationMode: schema.string(), + networkStatus: schema.string(), + installerType: schema.string(), + uuid: schema.string(), + detectionState: schema.nullable(schema.string()), + infected: schema.boolean(), + registeredAt: schema.string(), + lastIpToMgmt: schema.string(), + storageName: schema.nullable(schema.string()), + osUsername: schema.string(), + groupIp: schema.string(), + createdAt: schema.string(), + remoteProfilingState: schema.string(), + groupUpdatedAt: schema.nullable(schema.string()), + scanAbortedAt: schema.nullable(schema.string()), + isUninstalled: schema.boolean(), + networkQuarantineEnabled: schema.boolean(), + tags: schema.object({ + sentinelone: schema.arrayOf( + schema.object({ + assignedBy: schema.string(), + assignedAt: schema.string(), + assignedById: schema.string(), + key: schema.string(), + value: schema.string(), + id: schema.string(), + }) + ), + }), + externalIp: schema.string(), + siteId: schema.string(), + machineType: schema.string(), + domain: schema.string(), + scanStatus: schema.string(), + osStartTime: schema.string(), + accountName: schema.string(), + lastLoggedInUserName: schema.string(), + showAlertIcon: schema.boolean(), + rangerStatus: schema.string(), + groupName: schema.string(), + threatRebootRequired: schema.boolean(), + remoteProfilingStateExpiration: schema.nullable(schema.string()), + policyUpdatedAt: schema.nullable(schema.string()), + activeDirectory: schema.object({ + userPrincipalName: schema.nullable(schema.string()), + lastUserDistinguishedName: schema.nullable(schema.string()), + computerMemberOf: schema.arrayOf(schema.object({ type: schema.string() })), + lastUserMemberOf: schema.arrayOf(schema.object({ type: schema.string() })), + mail: schema.nullable(schema.string()), + computerDistinguishedName: schema.nullable(schema.string()), + }), + isDecommissioned: schema.boolean(), + rangerVersion: schema.string(), + userActionsNeeded: schema.arrayOf( + schema.object({ + type: schema.string(), + example: schema.string(), + enum: schema.arrayOf(schema.string()), + }) + ), + locations: schema.nullable( + schema.arrayOf( + schema.object({ name: schema.string(), scope: schema.string(), id: schema.string() }) + ) + ), + id: schema.string(), + coreCount: schema.number(), + osRevision: schema.string(), + osType: schema.string(), + groupId: schema.string(), + computerName: schema.string(), + scanStartedAt: schema.string(), + encryptedApplications: schema.boolean(), + storageType: schema.nullable(schema.string()), + networkInterfaces: schema.arrayOf( + schema.object({ + gatewayMacAddress: schema.nullable(schema.string()), + inet6: schema.arrayOf(schema.string()), + name: schema.string(), + inet: schema.arrayOf(schema.string()), + physical: schema.string(), + gatewayIp: schema.nullable(schema.string()), + id: schema.string(), + }) + ), + fullDiskScanLastUpdatedAt: schema.string(), + appsVulnerabilityStatus: schema.string(), + }) + ), +}); + +export const SentinelOneIsolateAgentResponseSchema = schema.object({ + errors: schema.nullable(schema.arrayOf(schema.string())), + data: schema.object({ + affected: schema.number(), + }), +}); + +export const SentinelOneGetRemoteScriptsParamsSchema = schema.object({ + query: schema.nullable(schema.string()), + osTypes: schema.nullable(schema.arrayOf(schema.string())), +}); + +export const SentinelOneGetRemoteScriptsResponseSchema = schema.object({ + errors: schema.nullable(schema.arrayOf(schema.string())), + pagination: schema.object({ + nextCursor: schema.nullable(schema.string()), + totalItems: schema.number(), + }), + data: schema.arrayOf( + schema.object({ + id: schema.string(), + updater: schema.nullable(schema.string()), + isAvailableForLite: schema.boolean(), + isAvailableForArs: schema.boolean(), + fileSize: schema.number(), + mgmtId: schema.number(), + scopeLevel: schema.string(), + shortFileName: schema.string(), + scriptName: schema.string(), + creator: schema.string(), + package: schema.nullable( + schema.object({ + id: schema.string(), + bucketName: schema.string(), + endpointExpiration: schema.string(), + fileName: schema.string(), + endpointExpirationSeconds: schema.nullable(schema.number()), + fileSize: schema.number(), + signatureType: schema.string(), + signature: schema.string(), + }) + ), + bucketName: schema.string(), + inputRequired: schema.boolean(), + fileName: schema.string(), + supportedDestinations: schema.nullable(schema.arrayOf(schema.string())), + scopeName: schema.nullable(schema.string()), + signatureType: schema.string(), + outputFilePaths: schema.nullable(schema.arrayOf(schema.string())), + scriptDescription: schema.nullable(schema.string()), + createdByUserId: schema.string(), + scopeId: schema.string(), + updatedAt: schema.string(), + scriptType: schema.string(), + scopePath: schema.string(), + creatorId: schema.string(), + osTypes: schema.arrayOf(schema.string()), + scriptRuntimeTimeoutSeconds: schema.number(), + version: schema.string(), + updaterId: schema.nullable(schema.string()), + createdAt: schema.string(), + inputExample: schema.nullable(schema.string()), + inputInstructions: schema.nullable(schema.string()), + signature: schema.string(), + createdByUser: schema.string(), + requiresApproval: schema.maybe(schema.boolean()), + }) + ), +}); + +export const SentinelOneExecuteScriptParamsSchema = schema.object({ + computerName: schema.maybe(schema.string()), + script: schema.object({ + scriptId: schema.string(), + scriptName: schema.maybe(schema.string()), + apiKey: schema.maybe(schema.string()), + outputDirectory: schema.maybe(schema.string()), + requiresApproval: schema.maybe(schema.boolean()), + taskDescription: schema.maybe(schema.string()), + singularityxdrUrl: schema.maybe(schema.string()), + inputParams: schema.maybe(schema.string()), + singularityxdrKeyword: schema.maybe(schema.string()), + scriptRuntimeTimeoutSeconds: schema.maybe(schema.number()), + passwordFromScope: schema.maybe( + schema.object({ + scopeLevel: schema.maybe(schema.string()), + scopeId: schema.maybe(schema.string()), + }) + ), + password: schema.maybe(schema.string()), + }), +}); + +export const SentinelOneGetRemoteScriptStatusParamsSchema = schema.object( + { + parentTaskId: schema.string(), + }, + { unknowns: 'allow' } +); + +export const SentinelOneGetRemoteScriptStatusResponseSchema = schema.object({ + pagination: schema.object({ + totalItems: schema.number(), + nextCursor: schema.nullable(schema.string()), + }), + errors: schema.arrayOf(schema.object({ type: schema.string() })), + data: schema.arrayOf( + schema.object({ + agentIsDecommissioned: schema.boolean(), + agentComputerName: schema.string(), + status: schema.string(), + groupName: schema.string(), + initiatedById: schema.string(), + parentTaskId: schema.string(), + updatedAt: schema.string(), + createdAt: schema.string(), + agentIsActive: schema.boolean(), + agentOsType: schema.string(), + agentMachineType: schema.string(), + id: schema.string(), + siteName: schema.string(), + detailedStatus: schema.string(), + siteId: schema.string(), + scriptResultsSignature: schema.nullable(schema.string()), + initiatedBy: schema.string(), + accountName: schema.string(), + groupId: schema.string(), + statusDescription: schema.object({ + readOnly: schema.boolean(), + description: schema.string(), + }), + agentUuid: schema.string(), + accountId: schema.string(), + type: schema.string(), + scriptResultsPath: schema.string(), + scriptResultsBucket: schema.string(), + description: schema.string(), + agentId: schema.string(), + }) + ), +}); + +export const SentinelOneBaseFilterSchema = schema.object({ + K8SNodeName__contains: schema.nullable(schema.string()), + coreCount__lt: schema.nullable(schema.string()), + rangerStatuses: schema.nullable(schema.string()), + adUserQuery__contains: schema.nullable(schema.string()), + rangerVersionsNin: schema.nullable(schema.string()), + rangerStatusesNin: schema.nullable(schema.string()), + coreCount__gte: schema.nullable(schema.string()), + threatCreatedAt__gte: schema.nullable(schema.string()), + decommissionedAt__lte: schema.nullable(schema.string()), + operationalStatesNin: schema.nullable(schema.string()), + appsVulnerabilityStatusesNin: schema.nullable(schema.string()), + mitigationMode: schema.nullable(schema.string()), + createdAt__gte: schema.nullable(schema.string()), + gatewayIp: schema.nullable(schema.string()), + cloudImage__contains: schema.nullable(schema.string()), + registeredAt__between: schema.nullable(schema.string()), + threatMitigationStatus: schema.nullable(schema.string()), + installerTypesNin: schema.nullable(schema.string()), + appsVulnerabilityStatuses: schema.nullable(schema.string()), + threatResolved: schema.nullable(schema.string()), + mitigationModeSuspicious: schema.nullable(schema.string()), + isUpToDate: schema.nullable(schema.string()), + adComputerQuery__contains: schema.nullable(schema.string()), + updatedAt__gte: schema.nullable(schema.string()), + azureResourceGroup__contains: schema.nullable(schema.string()), + scanStatus: schema.nullable(schema.string()), + threatContentHash: schema.nullable(schema.string()), + osTypesNin: schema.nullable(schema.string()), + threatRebootRequired: schema.nullable(schema.string()), + totalMemory__between: schema.nullable(schema.string()), + firewallEnabled: schema.nullable(schema.string()), + gcpServiceAccount__contains: schema.nullable(schema.string()), + updatedAt__gt: schema.nullable(schema.string()), + remoteProfilingStates: schema.nullable(schema.string()), + filteredGroupIds: schema.nullable(schema.string()), + agentVersions: schema.nullable(schema.string()), + activeThreats: schema.nullable(schema.string()), + machineTypesNin: schema.nullable(schema.string()), + lastActiveDate__gt: schema.nullable(schema.string()), + awsSubnetIds__contains: schema.nullable(schema.string()), + installerTypes: schema.nullable(schema.string()), + registeredAt__gte: schema.nullable(schema.string()), + migrationStatus: schema.nullable(schema.string()), + cloudTags__contains: schema.nullable(schema.string()), + totalMemory__gte: schema.nullable(schema.string()), + decommissionedAt__lt: schema.nullable(schema.string()), + threatCreatedAt__lt: schema.nullable(schema.string()), + updatedAt__lte: schema.nullable(schema.string()), + osArch: schema.nullable(schema.string()), + registeredAt__gt: schema.nullable(schema.string()), + registeredAt__lt: schema.nullable(schema.string()), + siteIds: schema.nullable(schema.string()), + networkInterfaceInet__contains: schema.nullable(schema.string()), + groupIds: schema.nullable(schema.string()), + uuids: schema.nullable(schema.string()), + accountIds: schema.nullable(schema.string()), + scanStatusesNin: schema.nullable(schema.string()), + cpuCount__lte: schema.nullable(schema.string()), + locationIds: schema.nullable(schema.string()), + awsSecurityGroups__contains: schema.nullable(schema.string()), + networkStatusesNin: schema.nullable(schema.string()), + activeThreats__gt: schema.nullable(schema.string()), + infected: schema.nullable(schema.string()), + osVersion__contains: schema.nullable(schema.string()), + machineTypes: schema.nullable(schema.string()), + agentPodName__contains: schema.nullable(schema.string()), + computerName__like: schema.nullable(schema.string()), + threatCreatedAt__gt: schema.nullable(schema.string()), + consoleMigrationStatusesNin: schema.nullable(schema.string()), + computerName: schema.nullable(schema.string()), + decommissionedAt__between: schema.nullable(schema.string()), + cloudInstanceId__contains: schema.nullable(schema.string()), + createdAt__lte: schema.nullable(schema.string()), + coreCount__between: schema.nullable(schema.string()), + totalMemory__lte: schema.nullable(schema.string()), + remoteProfilingStatesNin: schema.nullable(schema.string()), + adComputerMember__contains: schema.nullable(schema.string()), + threatCreatedAt__between: schema.nullable(schema.string()), + totalMemory__gt: schema.nullable(schema.string()), + ids: schema.nullable(schema.string()), + agentVersionsNin: schema.nullable(schema.string()), + updatedAt__between: schema.nullable(schema.string()), + locationEnabled: schema.nullable(schema.string()), + locationIdsNin: schema.nullable(schema.string()), + osTypes: schema.nullable(schema.string()), + encryptedApplications: schema.nullable(schema.string()), + filterId: schema.nullable(schema.string()), + decommissionedAt__gt: schema.nullable(schema.string()), + adUserMember__contains: schema.nullable(schema.string()), + uuid: schema.nullable(schema.string()), + coreCount__lte: schema.nullable(schema.string()), + coreCount__gt: schema.nullable(schema.string()), + cloudNetwork__contains: schema.nullable(schema.string()), + clusterName__contains: schema.nullable(schema.string()), + cpuCount__gte: schema.nullable(schema.string()), + query: schema.nullable(schema.string()), + lastActiveDate__between: schema.nullable(schema.string()), + rangerStatus: schema.nullable(schema.string()), + domains: schema.nullable(schema.string()), + cloudProvider: schema.nullable(schema.string()), + lastActiveDate__lt: schema.nullable(schema.string()), + scanStatuses: schema.nullable(schema.string()), + hasLocalConfiguration: schema.nullable(schema.string()), + networkStatuses: schema.nullable(schema.string()), + isPendingUninstall: schema.nullable(schema.string()), + createdAt__gt: schema.nullable(schema.string()), + cpuCount__lt: schema.nullable(schema.string()), + consoleMigrationStatuses: schema.nullable(schema.string()), + adQuery: schema.nullable(schema.string()), + updatedAt__lt: schema.nullable(schema.string()), + createdAt__lt: schema.nullable(schema.string()), + adComputerName__contains: schema.nullable(schema.string()), + cloudInstanceSize__contains: schema.nullable(schema.string()), + registeredAt__lte: schema.nullable(schema.string()), + networkQuarantineEnabled: schema.nullable(schema.string()), + cloudAccount__contains: schema.nullable(schema.string()), + cloudLocation__contains: schema.nullable(schema.string()), + rangerVersions: schema.nullable(schema.string()), + networkInterfaceGatewayMacAddress__contains: schema.nullable(schema.string()), + uuid__contains: schema.nullable(schema.string()), + agentNamespace__contains: schema.nullable(schema.string()), + K8SNodeLabels__contains: schema.nullable(schema.string()), + adQuery__contains: schema.nullable(schema.string()), + K8SType__contains: schema.nullable(schema.string()), + countsFor: schema.nullable(schema.string()), + totalMemory__lt: schema.nullable(schema.string()), + externalId__contains: schema.nullable(schema.string()), + filteredSiteIds: schema.nullable(schema.string()), + decommissionedAt__gte: schema.nullable(schema.string()), + cpuCount__gt: schema.nullable(schema.string()), + threatHidden: schema.nullable(schema.string()), + isUninstalled: schema.nullable(schema.string()), + computerName__contains: schema.nullable(schema.string()), + lastActiveDate__lte: schema.nullable(schema.string()), + adUserName__contains: schema.nullable(schema.string()), + isActive: schema.nullable(schema.string()), + userActionsNeeded: schema.nullable(schema.string()), + threatCreatedAt__lte: schema.nullable(schema.string()), + domainsNin: schema.nullable(schema.string()), + operationalStates: schema.nullable(schema.string()), + externalIp__contains: schema.nullable(schema.string()), + isDecommissioned: schema.nullable(schema.string()), + networkInterfacePhysical__contains: schema.nullable(schema.string()), + lastActiveDate__gte: schema.nullable(schema.string()), + createdAt__between: schema.nullable(schema.string()), + cpuCount__between: schema.nullable(schema.string()), + lastLoggedInUserName__contains: schema.nullable(schema.string()), + awsRole__contains: schema.nullable(schema.string()), + K8SVersion__contains: schema.nullable(schema.string()), +}); + +export const SentinelOneKillProcessParamsSchema = SentinelOneBaseFilterSchema.extends({ + processName: schema.string(), +}); + +export const SentinelOneIsolateAgentParamsSchema = SentinelOneBaseFilterSchema; + +export const SentinelOneGetAgentsParamsSchema = SentinelOneBaseFilterSchema; + +export const SentinelOneGetRemoteScriptsStatusParams = schema.object({ + parentTaskId: schema.string(), +}); + +export const SentinelOneExecuteScriptResponseSchema = schema.object({ + errors: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + data: schema.nullable( + schema.object({ + pendingExecutionId: schema.nullable(schema.string()), + affected: schema.nullable(schema.number()), + parentTaskId: schema.nullable(schema.string()), + pending: schema.nullable(schema.boolean()), + }) + ), +}); + +export const SentinelOneKillProcessResponseSchema = SentinelOneExecuteScriptResponseSchema; + +export const SentinelOneKillProcessSchema = schema.object({ + subAction: schema.literal(SUB_ACTION.KILL_PROCESS), + subActionParams: SentinelOneKillProcessParamsSchema, +}); + +export const SentinelOneIsolateAgentSchema = schema.object({ + subAction: schema.literal(SUB_ACTION.ISOLATE_AGENT), + subActionParams: SentinelOneIsolateAgentParamsSchema, +}); + +export const SentinelOneReleaseAgentSchema = schema.object({ + subAction: schema.literal(SUB_ACTION.RELEASE_AGENT), + subActionParams: SentinelOneIsolateAgentParamsSchema, +}); + +export const SentinelOneExecuteScriptSchema = schema.object({ + subAction: schema.literal(SUB_ACTION.EXECUTE_SCRIPT), + subActionParams: SentinelOneExecuteScriptParamsSchema, +}); + +export const SentinelOneActionParamsSchema = schema.oneOf([ + SentinelOneKillProcessSchema, + SentinelOneIsolateAgentSchema, + SentinelOneReleaseAgentSchema, + SentinelOneExecuteScriptSchema, +]); diff --git a/x-pack/plugins/stack_connectors/common/sentinelone/types.ts b/x-pack/plugins/stack_connectors/common/sentinelone/types.ts new file mode 100644 index 0000000000000..ab50e316d03f7 --- /dev/null +++ b/x-pack/plugins/stack_connectors/common/sentinelone/types.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TypeOf } from '@kbn/config-schema'; +import { + SentinelOneBaseApiResponseSchema, + SentinelOneConfigSchema, + SentinelOneExecuteScriptParamsSchema, + SentinelOneGetAgentsParamsSchema, + SentinelOneGetAgentsResponseSchema, + SentinelOneGetRemoteScriptsParamsSchema, + SentinelOneGetRemoteScriptsResponseSchema, + SentinelOneGetRemoteScriptsStatusParams, + SentinelOneIsolateAgentParamsSchema, + SentinelOneKillProcessParamsSchema, + SentinelOneSecretsSchema, + SentinelOneActionParamsSchema, +} from './schema'; + +export type SentinelOneConfig = TypeOf; +export type SentinelOneSecrets = TypeOf; + +export type SentinelOneBaseApiResponse = TypeOf; + +export type SentinelOneGetAgentsParams = TypeOf; +export type SentinelOneGetAgentsResponse = TypeOf; + +export type SentinelOneKillProcessParams = TypeOf; + +export type SentinelOneExecuteScriptParams = TypeOf; + +export type SentinelOneGetRemoteScriptStatusParams = TypeOf< + typeof SentinelOneGetRemoteScriptsStatusParams +>; + +export type SentinelOneGetRemoteScriptsParams = TypeOf< + typeof SentinelOneGetRemoteScriptsParamsSchema +>; + +export type SentinelOneGetRemoteScriptsResponse = TypeOf< + typeof SentinelOneGetRemoteScriptsResponseSchema +>; + +export type SentinelOneIsolateAgentParams = TypeOf; + +export type SentinelOneActionParams = TypeOf; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/index.ts b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/index.ts new file mode 100644 index 0000000000000..7bb5159f87525 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getConnectorType as getSentinelOneConnectorType } from './sentinelone'; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/logo.tsx b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/logo.tsx new file mode 100644 index 0000000000000..656e75d07d67c --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/logo.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +const Logo = () => ( + + + + + + + + + + + + + + + + + + + + +); + +// eslint-disable-next-line import/no-default-export +export { Logo as default }; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone.ts b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone.ts new file mode 100644 index 0000000000000..469613621bf05 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { + ActionTypeModel as ConnectorTypeModel, + GenericValidationResult, +} from '@kbn/triggers-actions-ui-plugin/public'; +import { + SENTINELONE_CONNECTOR_ID, + SENTINELONE_TITLE, + SUB_ACTION, +} from '../../../common/sentinelone/constants'; +import type { + SentinelOneConfig, + SentinelOneSecrets, + SentinelOneActionParams, +} from '../../../common/sentinelone/types'; + +interface ValidationErrors { + subAction: string[]; +} + +export function getConnectorType(): ConnectorTypeModel< + SentinelOneConfig, + SentinelOneSecrets, + SentinelOneActionParams +> { + return { + id: SENTINELONE_CONNECTOR_ID, + actionTypeTitle: SENTINELONE_TITLE, + iconClass: lazy(() => import('./logo')), + selectMessage: i18n.translate( + 'xpack.stackConnectors.security.sentinelone.config.selectMessageText', + { + defaultMessage: 'Execute SentinelOne scripts', + } + ), + validateParams: async ( + actionParams: SentinelOneActionParams + ): Promise> => { + const translations = await import('./translations'); + const errors: ValidationErrors = { + subAction: [], + }; + const { subAction } = actionParams; + + // The internal "subAction" param should always be valid, ensure it is only if "subActionParams" are valid + if (!subAction) { + errors.subAction.push(translations.ACTION_REQUIRED); + } else if (!(subAction in SUB_ACTION)) { + errors.subAction.push(translations.INVALID_ACTION); + } + return { errors }; + }, + actionConnectorFields: lazy(() => import('./sentinelone_connector')), + actionParamsFields: lazy(() => import('./sentinelone_params')), + }; +} diff --git a/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone_connector.tsx b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone_connector.tsx new file mode 100644 index 0000000000000..785dc5d05832d --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone_connector.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + ActionConnectorFieldsProps, + ConfigFieldSchema, + SecretsFieldSchema, + SimpleConnectorForm, +} from '@kbn/triggers-actions-ui-plugin/public'; +import * as i18n from './translations'; + +const configFormSchema: ConfigFieldSchema[] = [ + { + id: 'url', + label: i18n.URL_LABEL, + isUrlField: true, + }, +]; + +const secretsFormSchema: SecretsFieldSchema[] = [ + { + id: 'token', + label: i18n.TOKEN_LABEL, + isPasswordField: true, + }, +]; + +const SentinelOneActionConnectorFields: React.FunctionComponent = ({ + readOnly, + isEdit, +}) => ( + +); + +// eslint-disable-next-line import/no-default-export +export { SentinelOneActionConnectorFields as default }; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone_params.tsx b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone_params.tsx new file mode 100644 index 0000000000000..f74e1c897b97b --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/sentinelone_params.tsx @@ -0,0 +1,333 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useMemo, useState, ReactNode } from 'react'; +import { reduce } from 'lodash'; +import { + EuiButtonIcon, + EuiComboBox, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiInMemoryTable, + EuiSuperSelect, +} from '@elastic/eui'; +import { + ActionConnectorMode, + ActionParamsProps, + TextAreaWithMessageVariables, +} from '@kbn/triggers-actions-ui-plugin/public'; +import { useSubAction, useKibana } from '@kbn/triggers-actions-ui-plugin/public'; +import { EuiBasicTableColumn, EuiSearchBarProps, EuiLink } from '@elastic/eui'; +import { SUB_ACTION } from '../../../common/sentinelone/constants'; +import type { + SentinelOneGetAgentsParams, + SentinelOneGetAgentsResponse, + SentinelOneGetRemoteScriptsParams, + SentinelOneGetRemoteScriptsResponse, + SentinelOneActionParams, +} from '../../../common/sentinelone/types'; +import type { SentinelOneExecuteSubActionParams } from './types'; +import * as i18n from './translations'; + +type ScriptOption = SentinelOneGetRemoteScriptsResponse['data'][0]; + +const SentinelOneParamsFields: React.FunctionComponent< + ActionParamsProps +> = ({ actionConnector, actionParams, editAction, index, executionMode, errors, ...rest }) => { + const { toasts } = useKibana().notifications; + const { subAction, subActionParams } = actionParams; + const [selectedScript, setSelectedScript] = useState(); + + const [selectedAgent, setSelectedAgent] = useState>(() => { + if (subActionParams?.computerName) { + return [{ label: subActionParams?.computerName }]; + } + return []; + }); + const [connectorId] = useState(actionConnector?.id); + + const isTest = useMemo(() => executionMode === ActionConnectorMode.Test, [executionMode]); + + const editSubActionParams = useCallback( + (params: Partial) => { + editAction('subActionParams', { ...subActionParams, ...params }, index); + }, + [editAction, index, subActionParams] + ); + + const { + response: { data: agents } = {}, + isLoading: isLoadingAgents, + error: agentsError, + } = useSubAction({ + connectorId, + subAction: SUB_ACTION.GET_AGENTS, + disabled: isTest, + }); + + const agentOptions = useMemo( + () => + reduce( + agents, + (acc, item) => { + acc.push({ + label: item.computerName, + }); + return acc; + }, + [] as Array<{ label: string }> + ), + [agents] + ); + + const { + response: { data: remoteScripts } = {}, + isLoading: isLoadingScripts, + error: scriptsError, + } = useSubAction({ + connectorId, + subAction: SUB_ACTION.GET_REMOTE_SCRIPTS, + }); + + useEffect(() => { + if (agentsError) { + toasts.danger({ title: i18n.AGENTS_ERROR, body: agentsError.message }); + } + if (scriptsError) { + toasts.danger({ title: i18n.REMOTE_SCRIPTS_ERROR, body: scriptsError.message }); + } + }, [toasts, scriptsError, agentsError]); + + const pagination = { + initialPageSize: 10, + pageSizeOptions: [10, 20, 50], + }; + + const search: EuiSearchBarProps = { + defaultQuery: 'scriptType:action', + box: { + incremental: true, + }, + filters: [ + { + type: 'field_value_selection', + field: 'scriptType', + name: i18n.SCRIPT_TYPE_FILTER_LABEL, + multiSelect: true, + options: [ + { + value: 'action', + }, + { value: 'dataCollection' }, + ], + }, + { + type: 'field_value_selection', + field: 'osTypes', + name: i18n.OS_TYPES_FILTER_LABEL, + multiSelect: true, + options: [ + { + value: 'Windows', + }, + { + value: 'macos', + }, + { + value: 'linux', + }, + ], + }, + ], + }; + + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( + {} + ); + + const toggleDetails = (script: ScriptOption) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; + + if (script.id) { + if (itemIdToExpandedRowMapValues[script.id]) { + delete itemIdToExpandedRowMapValues[script.id]; + } else { + itemIdToExpandedRowMapValues[script.id] = <>More details true; + } + } + setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); + }; + + const columns: Array> = [ + { + field: 'scriptName', + name: 'Script name', + }, + { + field: 'scriptType', + name: 'Script type', + }, + { + field: 'osTypes', + name: 'OS types', + }, + { + actions: [ + { + name: 'Choose', + description: 'Choose this script', + isPrimary: true, + onClick: (item) => { + setSelectedScript(item); + editSubActionParams({ + script: { + scriptId: item.id, + scriptRuntimeTimeoutSeconds: 3600, + taskDescription: item.scriptName, + requiresApproval: item.requiresApproval ?? false, + }, + }); + }, + }, + ], + }, + { + align: 'right', + width: '40px', + isExpander: true, + render: (script: ScriptOption) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; + + return ( + toggleDetails(script)} + aria-label={itemIdToExpandedRowMapValues[script.id] ? 'Collapse' : 'Expand'} + iconType={itemIdToExpandedRowMapValues[script.id] ? 'arrowDown' : 'arrowRight'} + /> + ); + }, + }, + ]; + + const actionTypeOptions = [ + { + value: SUB_ACTION.KILL_PROCESS, + inputDisplay: i18n.KILL_PROCESS_ACTION_LABEL, + }, + { + value: SUB_ACTION.ISOLATE_AGENT, + inputDisplay: i18n.ISOLATE_AGENT_ACTION_LABEL, + }, + { + value: SUB_ACTION.RELEASE_AGENT, + inputDisplay: i18n.RELEASE_AGENT_ACTION_LABEL, + }, + ]; + + const handleEditSubAction = useCallback( + (payload) => { + if (subAction !== payload) { + editSubActionParams({}); + editAction('subAction', payload, index); + } + }, + [editAction, editSubActionParams, index, subAction] + ); + + return ( + + {isTest && ( + + + { + setSelectedAgent(item); + editSubActionParams({ computerName: item[0].label }); + }} + isDisabled={isLoadingAgents} + /> + + + )} + + + + + + {subAction === SUB_ACTION.EXECUTE_SCRIPT && ( + <> + + setSelectedScript(undefined)}> + {i18n.CHANGE_ACTION_LABEL} + + ) : null + } + > + {selectedScript?.scriptName ? ( + + ) : ( + + items={remoteScripts ?? []} + itemId="scriptId" + loading={isLoadingScripts} + columns={columns} + search={search} + pagination={pagination} + sorting + hasActions + itemIdToExpandedRowMap={itemIdToExpandedRowMap} + /> + )} + + + + <> + {selectedScript && ( + + + + )} + + + )} + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SentinelOneParamsFields as default }; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/translations.ts b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/translations.ts new file mode 100644 index 0000000000000..a5b9a274857c3 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/translations.ts @@ -0,0 +1,270 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { API_MAX_RESULTS } from '../../../common/sentinelone/constants'; + +// config form +export const URL_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.config.urlTextFieldLabel', + { + defaultMessage: 'SentinelOne tenant URL', + } +); + +export const TOKEN_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.config.tokenTextFieldLabel', + { + defaultMessage: 'API token', + } +); + +// params form +export const ASC = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.storyFieldLabel', + { + defaultMessage: 'SentinelOne Script', + } +); + +export const SCRIPT_TYPE_FILTER_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.scriptTypeFilterLabel', + { + defaultMessage: 'Script type', + } +); + +export const OS_TYPES_FILTER_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.osTypesFilterLabel', + { + defaultMessage: 'OS', + } +); + +export const STORY_ARIA_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.storyFieldAriaLabel', + { + defaultMessage: 'Select a SentinelOne script', + } +); + +export const KILL_PROCESS_ACTION_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.killProcessActionLabel', + { + defaultMessage: 'Kill process', + } +); + +export const ISOLATE_AGENT_ACTION_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.isolateAgentActionLabel', + { + defaultMessage: 'Isolate agent', + } +); + +export const RELEASE_AGENT_ACTION_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.releaseAgentActionLabel', + { + defaultMessage: 'Release agent', + } +); + +export const AGENTS_FIELD_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.agentsFieldLabel', + { + defaultMessage: 'SentinelOne agent', + } +); + +export const AGENTS_FIELD_PLACEHOLDER = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.agentsFieldPlaceholder', + { + defaultMessage: 'Select a single agent', + } +); + +export const ACTION_TYPE_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.actionTypeFieldLabel', + { + defaultMessage: 'Action Type', + } +); + +export const COMMAND_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.commandFieldLabel', + { + defaultMessage: 'Command', + } +); + +export const CHANGE_ACTION_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.changeActionButton', + { + defaultMessage: 'Change action', + } +); + +export const WEBHOOK_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookFieldLabel', + { + defaultMessage: 'SentinelOne Webhook action', + } +); +export const WEBHOOK_HELP = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookHelp', + { + defaultMessage: 'The data entry action in the story', + } +); +export const WEBHOOK_PLACEHOLDER = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookPlaceholder', + { + defaultMessage: 'Select a webhook action', + } +); +export const WEBHOOK_DISABLED_PLACEHOLDER = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookDisabledPlaceholder', + { + defaultMessage: 'Select a story first', + } +); +export const WEBHOOK_ARIA_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookFieldAriaLabel', + { + defaultMessage: 'Select a SentinelOne webhook action', + } +); + +export const WEBHOOK_URL_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookUrlFieldLabel', + { + defaultMessage: 'Webhook URL', + } +); +export const WEBHOOK_URL_FALLBACK_TITLE = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookUrlFallbackTitle', + { + defaultMessage: 'SentinelOne API results limit reached', + } +); +export const WEBHOOK_URL_FALLBACK_TEXT = (entity: 'Story' | 'Webhook') => + i18n.translate('xpack.stackConnectors.security.sentinelone.params.webhookUrlFallbackText', { + values: { entity, limit: API_MAX_RESULTS }, + defaultMessage: `Not possible to retrieve more than {limit} results from the SentinelOne {entity} API. If your {entity} does not appear in the list, please fill the Webhook URL below`, + }); +export const WEBHOOK_URL_HELP = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookUrlHelp', + { + defaultMessage: 'The Story and Webhook selectors will be ignored if the Webhook URL is defined', + } +); +export const WEBHOOK_URL_PLACEHOLDER = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.webhookUrlPlaceholder', + { + defaultMessage: 'Paste the Webhook URL here', + } +); +export const DISABLED_BY_WEBHOOK_URL_PLACEHOLDER = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.disabledByWebhookUrlPlaceholder', + { + defaultMessage: 'Remove the Webhook URL to use this selector', + } +); + +export const BODY_LABEL = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.bodyFieldLabel', + { + defaultMessage: 'Body', + } +); +export const AGENTS_ERROR = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.componentError.storiesRequestFailed', + { + defaultMessage: 'Error retrieving agent from SentinelOne', + } +); + +export const REMOTE_SCRIPTS_ERROR = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.componentError.remoteScriptsRequestFailed', + { + defaultMessage: 'Error retrieving remote scripts from SentinelOne', + } +); + +export const WEBHOOKS_ERROR = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.componentError.webhooksRequestFailed', + { + defaultMessage: 'Error retrieving webhook actions from SentinelOne', + } +); + +export const STORY_NOT_FOUND_WARNING = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.componentWarning.storyNotFound', + { + defaultMessage: 'Cannot find the saved story. Please select a valid story from the selector', + } +); +export const WEBHOOK_NOT_FOUND_WARNING = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.componentWarning.webhookNotFound', + { + defaultMessage: + 'Cannot find the saved webhook. Please select a valid webhook from the selector', + } +); + +export const ACTION_REQUIRED = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.error.requiredActionText', + { + defaultMessage: 'Action is required.', + } +); + +export const INVALID_ACTION = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.error.invalidActionText', + { + defaultMessage: 'Invalid action name.', + } +); + +export const BODY_REQUIRED = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.error.requiredBodyText', + { + defaultMessage: 'Body is required.', + } +); + +export const BODY_INVALID = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.error.invalidBodyText', + { + defaultMessage: 'Body does not have a valid JSON format.', + } +); + +export const STORY_REQUIRED = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.error.requiredStoryText', + { + defaultMessage: 'Story is required.', + } +); +export const WEBHOOK_REQUIRED = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.error.requiredWebhookText', + { + defaultMessage: 'Webhook is required.', + } +); +export const WEBHOOK_PATH_REQUIRED = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.error.requiredWebhookPathText', + { + defaultMessage: 'Webhook action path is missing.', + } +); +export const WEBHOOK_SECRET_REQUIRED = i18n.translate( + 'xpack.stackConnectors.security.sentinelone.params.error.requiredWebhookSecretText', + { + defaultMessage: 'Webhook action secret is missing.', + } +); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/types.ts b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/types.ts new file mode 100644 index 0000000000000..ac97bdaf224f4 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/sentinelone/types.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + SentinelOneKillProcessParams, + SentinelOneExecuteScriptParams, + SentinelOneIsolateAgentParams, +} from '../../../common/sentinelone/types'; +import type { SUB_ACTION } from '../../../common/sentinelone/constants'; + +export type SentinelOneExecuteSubActionParams = + | SentinelOneKillProcessParams + | SentinelOneExecuteScriptParams + | SentinelOneIsolateAgentParams; + +export interface SentinelOneExecuteActionParams { + subAction: SUB_ACTION; + subActionParams: SentinelOneExecuteSubActionParams; +} diff --git a/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/index.ts new file mode 100644 index 0000000000000..1ce534079e829 --- /dev/null +++ b/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + SubActionConnectorType, + ValidatorType, +} from '@kbn/actions-plugin/server/sub_action_framework/types'; +import { SecurityConnectorFeatureId } from '@kbn/actions-plugin/common'; +import { urlAllowListValidator } from '@kbn/actions-plugin/server'; +import { SENTINELONE_CONNECTOR_ID, SENTINELONE_TITLE } from '../../../common/sentinelone/constants'; +import { + SentinelOneConfigSchema, + SentinelOneSecretsSchema, +} from '../../../common/sentinelone/schema'; +import { SentinelOneConfig, SentinelOneSecrets } from '../../../common/sentinelone/types'; +import { SentinelOneConnector } from './sentinelone'; +import { renderParameterTemplates } from './render'; + +export const getSentinelOneConnectorType = (): SubActionConnectorType< + SentinelOneConfig, + SentinelOneSecrets +> => ({ + id: SENTINELONE_CONNECTOR_ID, + name: SENTINELONE_TITLE, + Service: SentinelOneConnector, + schema: { + config: SentinelOneConfigSchema, + secrets: SentinelOneSecretsSchema, + }, + validators: [{ type: ValidatorType.CONFIG, validator: urlAllowListValidator('url') }], + supportedFeatureIds: [SecurityConnectorFeatureId], + minimumLicenseRequired: 'enterprise' as const, + renderParameterTemplates, +}); diff --git a/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/render.ts b/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/render.ts new file mode 100644 index 0000000000000..9d852510ea7b7 --- /dev/null +++ b/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/render.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { set } from '@kbn/safer-lodash-set/fp'; +import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; +import { ExecutorParams } from '@kbn/actions-plugin/server/sub_action_framework/types'; +import { SUB_ACTION } from '../../../common/sentinelone/constants'; + +interface Context { + alerts: Ecs[]; +} + +export const renderParameterTemplates = ( + params: ExecutorParams, + variables: Record +) => { + const context = variables?.context as Context; + if (params?.subAction === SUB_ACTION.KILL_PROCESS) { + return { + subAction: SUB_ACTION.KILL_PROCESS, + subActionParams: { + processName: context.alerts[0].process?.name, + computerName: context.alerts[0].host?.name, + }, + }; + } + + if (params?.subAction === SUB_ACTION.ISOLATE_AGENT) { + return { + subAction: SUB_ACTION.ISOLATE_AGENT, + subActionParams: { + computerName: context.alerts[0].host?.name, + }, + }; + } + + if (params?.subAction === SUB_ACTION.RELEASE_AGENT) { + return { + subAction: SUB_ACTION.RELEASE_AGENT, + subActionParams: { + computerName: context.alerts[0].host?.name, + }, + }; + } + + if (params?.subAction === SUB_ACTION.EXECUTE_SCRIPT) { + return { + subAction: SUB_ACTION.EXECUTE_SCRIPT, + subActionParams: { + computerName: context.alerts[0].host?.name, + ...params.subActionParams, + }, + }; + } + + let body: string; + try { + let bodyObject; + const alerts = context.alerts; + if (alerts) { + // Remove the "kibana" entry from all alerts to reduce weight, the same data can be found in other parts of the alert object. + bodyObject = set( + 'context.alerts', + alerts.map(({ kibana, ...alert }) => alert), + variables + ); + } else { + bodyObject = variables; + } + body = JSON.stringify(bodyObject); + } catch (err) { + body = JSON.stringify({ error: { message: err.message } }); + } + return set('subActionParams.body', body, params); +}; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/sentinelone.ts b/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/sentinelone.ts new file mode 100644 index 0000000000000..d27f7dcc5588b --- /dev/null +++ b/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/sentinelone.ts @@ -0,0 +1,276 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ServiceParams, SubActionConnector } from '@kbn/actions-plugin/server'; +import type { AxiosError } from 'axios'; +import { SubActionRequestParams } from '@kbn/actions-plugin/server/sub_action_framework/types'; +import type { + SentinelOneConfig, + SentinelOneSecrets, + SentinelOneGetAgentsResponse, + SentinelOneGetAgentsParams, + SentinelOneGetRemoteScriptStatusParams, + SentinelOneBaseApiResponse, + SentinelOneGetRemoteScriptsParams, + SentinelOneGetRemoteScriptsResponse, + SentinelOneIsolateAgentParams, + SentinelOneKillProcessParams, + SentinelOneExecuteScriptParams, +} from '../../../common/sentinelone/types'; +import { + SentinelOneKillProcessResponseSchema, + SentinelOneExecuteScriptParamsSchema, + SentinelOneGetRemoteScriptsParamsSchema, + SentinelOneGetRemoteScriptsResponseSchema, + SentinelOneGetAgentsResponseSchema, + SentinelOneIsolateAgentResponseSchema, + SentinelOneIsolateAgentParamsSchema, + SentinelOneGetRemoteScriptStatusParamsSchema, + SentinelOneGetRemoteScriptStatusResponseSchema, + SentinelOneGetAgentsParamsSchema, + SentinelOneExecuteScriptResponseSchema, +} from '../../../common/sentinelone/schema'; +import { SUB_ACTION } from '../../../common/sentinelone/constants'; + +export const API_MAX_RESULTS = 1000; +export const API_PATH = '/web/api/v2.1'; + +export class SentinelOneConnector extends SubActionConnector< + SentinelOneConfig, + SentinelOneSecrets +> { + private urls: { + agents: string; + isolateAgent: string; + releaseAgent: string; + remoteScripts: string; + remoteScriptStatus: string; + remoteScriptsExecute: string; + }; + + constructor(params: ServiceParams) { + super(params); + + this.urls = { + isolateAgent: `${this.config.url}${API_PATH}/agents/actions/disconnect`, + releaseAgent: `${this.config.url}${API_PATH}/agents/actions/connect`, + remoteScripts: `${this.config.url}${API_PATH}/remote-scripts`, + remoteScriptStatus: `${this.config.url}${API_PATH}/remote-scripts/status`, + remoteScriptsExecute: `${this.config.url}${API_PATH}/remote-scripts/execute`, + agents: `${this.config.url}${API_PATH}/agents`, + }; + + this.registerSubActions(); + } + + private registerSubActions() { + this.registerSubAction({ + name: SUB_ACTION.GET_REMOTE_SCRIPTS, + method: 'getRemoteScripts', + schema: SentinelOneGetRemoteScriptsParamsSchema, + }); + + this.registerSubAction({ + name: SUB_ACTION.GET_REMOTE_SCRIPT_STATUS, + method: 'getRemoteScriptStatus', + schema: SentinelOneGetRemoteScriptStatusParamsSchema, + }); + + this.registerSubAction({ + name: SUB_ACTION.GET_AGENTS, + method: 'getAgents', + schema: SentinelOneGetAgentsParamsSchema, + }); + + this.registerSubAction({ + name: SUB_ACTION.ISOLATE_AGENT, + method: 'isolateAgent', + schema: SentinelOneIsolateAgentParamsSchema, + }); + + this.registerSubAction({ + name: SUB_ACTION.RELEASE_AGENT, + method: 'releaseAgent', + schema: SentinelOneIsolateAgentParamsSchema, + }); + + this.registerSubAction({ + name: SUB_ACTION.KILL_PROCESS, + method: 'killProcess', + schema: SentinelOneKillProcessResponseSchema, + }); + + this.registerSubAction({ + name: SUB_ACTION.EXECUTE_SCRIPT, + method: 'executeScript', + schema: SentinelOneExecuteScriptParamsSchema, + }); + } + + public async executeScript(payload: SentinelOneExecuteScriptParams) { + return this.sentinelOneApiRequest({ + url: this.urls.remoteScriptsExecute, + method: 'post', + data: { + data: { + outputDestination: 'SentinelCloud', + ...payload.script, + }, + filter: { + computerName: payload.computerName, + }, + }, + responseSchema: SentinelOneExecuteScriptResponseSchema, + }); + } + + public async killProcess({ processName, ...payload }: SentinelOneKillProcessParams) { + const agentData = await this.getAgents(payload); + + const agentId = agentData.data[0]?.id; + + if (!agentId) { + throw new Error(`No agent found for filter ${JSON.stringify(payload)}`); + } + + const terminateScriptResponse = await this.getRemoteScripts({ + query: 'terminate', + osTypes: [agentData?.data[0]?.osType], + }); + + if (!processName) { + throw new Error('No process name provided'); + } + + return this.sentinelOneApiRequest({ + url: this.urls.remoteScriptsExecute, + method: 'post', + data: { + data: { + outputDestination: 'SentinelCloud', + scriptId: terminateScriptResponse.data[0].id, + scriptRuntimeTimeoutSeconds: terminateScriptResponse.data[0].scriptRuntimeTimeoutSeconds, + taskDescription: terminateScriptResponse.data[0].scriptName, + inputParams: `--terminate --processes ${processName}`, + }, + filter: { + ids: agentId, + }, + }, + responseSchema: SentinelOneKillProcessResponseSchema, + }); + } + + public async isolateAgent(payload: SentinelOneIsolateAgentParams) { + const response = await this.getAgents(payload); + + if (response.data.length === 0) { + throw new Error('No agents found'); + } + + if (response.data[0].networkStatus === 'disconnected') { + throw new Error('Agent already isolated'); + } + + const agentId = response.data[0].id; + + return this.sentinelOneApiRequest({ + url: this.urls.isolateAgent, + method: 'post', + data: { + filter: { + ids: agentId, + }, + }, + responseSchema: SentinelOneIsolateAgentResponseSchema, + }); + } + + public async releaseAgent(payload: SentinelOneIsolateAgentParams) { + const response = await this.getAgents(payload); + + if (response.data.length === 0) { + throw new Error('No agents found'); + } + + if (response.data[0].networkStatus !== 'disconnected') { + throw new Error('Agent not isolated'); + } + + const agentId = response.data[0].id; + + return this.sentinelOneApiRequest({ + url: this.urls.releaseAgent, + method: 'post', + data: { + filter: { + ids: agentId, + }, + }, + responseSchema: SentinelOneIsolateAgentResponseSchema, + }); + } + + public async getAgents( + payload: SentinelOneGetAgentsParams + ): Promise { + return this.sentinelOneApiRequest({ + url: this.urls.agents, + params: { + ...payload, + }, + responseSchema: SentinelOneGetAgentsResponseSchema, + }); + } + + public async getRemoteScriptStatus(payload: SentinelOneGetRemoteScriptStatusParams) { + return this.sentinelOneApiRequest({ + url: this.urls.remoteScriptStatus, + params: { + parent_task_id: payload.parentTaskId, + }, + responseSchema: SentinelOneGetRemoteScriptStatusResponseSchema, + }); + } + + private async sentinelOneApiRequest( + req: SubActionRequestParams + ): Promise { + const response = await this.request({ + ...req, + params: { + ...req.params, + APIToken: this.secrets.token, + }, + }); + + return response.data; + } + + protected getResponseErrorMessage(error: AxiosError): string { + if (!error.response?.status) { + return 'Unknown API Error'; + } + if (error.response.status === 401) { + return 'Unauthorized API Error'; + } + return `API Error: ${error.response?.statusText}`; + } + + public async getRemoteScripts( + payload: SentinelOneGetRemoteScriptsParams + ): Promise { + return this.sentinelOneApiRequest({ + url: this.urls.remoteScripts, + params: { + limit: API_MAX_RESULTS, + ...payload, + }, + responseSchema: SentinelOneGetRemoteScriptsResponseSchema, + }); + } +} diff --git a/x-pack/plugins/stack_connectors/tsconfig.json b/x-pack/plugins/stack_connectors/tsconfig.json index 7cc6696f04368..f18dfaea77cca 100644 --- a/x-pack/plugins/stack_connectors/tsconfig.json +++ b/x-pack/plugins/stack_connectors/tsconfig.json @@ -33,6 +33,7 @@ "@kbn/core-saved-objects-common", "@kbn/core-http-browser-mocks", "@kbn/core-saved-objects-api-server-mocks", + "@kbn/securitysolution-ecs", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts index 16d63c60c4e1b..770fb102c5369 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts @@ -59,7 +59,7 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ const encryptedSavedObjectsClient = encryptedSavedObjects.getClient(); - return getSyntheticsMonitor({ + return await getSyntheticsMonitor({ monitorId, encryptedSavedObjectsClient, savedObjectsClient, diff --git a/x-pack/plugins/task_manager/server/queries/oldest_idle_action_task.ts b/x-pack/plugins/task_manager/server/queries/oldest_idle_action_task.ts index 49e7bc1e8d9e3..69947cb08fc8d 100644 --- a/x-pack/plugins/task_manager/server/queries/oldest_idle_action_task.ts +++ b/x-pack/plugins/task_manager/server/queries/oldest_idle_action_task.ts @@ -44,6 +44,7 @@ export const getOldestIdleActionTask = async ( 'actions:.jira', 'actions:.resilient', 'actions:.teams', + 'actions:.sentinelone', ], }, }, diff --git a/x-pack/plugins/task_manager/server/saved_objects/index.ts b/x-pack/plugins/task_manager/server/saved_objects/index.ts index 53d09d4baf131..0bb12906708de 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/index.ts @@ -50,6 +50,7 @@ export function setupSavedObjects( 'actions:.jira', 'actions:.resilient', 'actions:.teams', + 'actions:.sentinelone', ], }, }, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index fafb56aed9727..f70caee66aec9 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -18661,8 +18661,8 @@ "xpack.infra.homePage.toolbar.showingLastOneMinuteDataText": "Dernières {duration} de données pour l'heure sélectionnée", "xpack.infra.hostsViewPage.errorOnCreateOrLoadDataview": "Une erreur s’est produite lors de la création d’une vue de données : {metricAlias}. Essayez de recharger la page.", "xpack.infra.hostsViewPage.landing.calloutRoleClarificationWithDocsLink": "Un rôle avec accès aux paramètres avancés dans Kibana sera nécessaire. {docsLink}", - "xpack.infra.hostsViewPage.metricTrend.subtitle.average.limit": "Moyenne (de {limit} hôtes)", - "xpack.infra.hostsViewPage.metricTrend.subtitle.hostCount.limit": "Limité à {limit}", + "xpack.infra.hostsViewPage.kpi.subtitle.average.limit": "Moyenne (de {limit} hôtes)", + "xpack.infra.hostsViewPage.kpi.subtitle.hostCount.limit": "Limité à {limit}", "xpack.infra.hostsViewPage.table.selectedHostsButton": "Sélection effectuée de {selectedHostsCount} {selectedHostsCount, plural, =1 {hôte} one {hôtes} many {hôtes} other {hôtes}}", "xpack.infra.hostsViewPage.table.tooltip.documentationLabel": "Pour en savoir plus, consultez {documentation}", "xpack.infra.inventoryTimeline.header": "Moyenne {metricLabel}", @@ -18801,6 +18801,11 @@ "xpack.infra.assetDetailsEmbeddable.description": "Ajoutez une vue de détails de ressource.", "xpack.infra.assetDetailsEmbeddable.displayName": "Détails de ressource", "xpack.infra.assetDetailsEmbeddable.title": "Détails de ressource", + "xpack.infra.assetDetailsEmbeddable.overview.kpi.cpuUsage.title": "Utilisation CPU", + "xpack.infra.assetDetailsEmbeddable.overview.kpi.diskSpaceUsage.title": "Utilisation de l’espace disque", + "xpack.infra.assetDetailsEmbeddable.overview.kpi.memoryUsage.title": "Utilisation mémoire", + "xpack.infra.assetDetailsEmbeddable.overview.kpi.normalizedLoad1m.title": "Charge normalisée", + "xpack.infra.assetDetailsEmbeddable.overview.kpi.subtitle.average": "Moyenne", "xpack.infra.bottomDrawer.kubernetesDashboardsLink": "Tableaux de bord Kubernetes", "xpack.infra.chartSection.missingMetricDataBody": "Les données de ce graphique sont manquantes.", "xpack.infra.chartSection.missingMetricDataText": "Données manquantes", @@ -18885,6 +18890,7 @@ "xpack.infra.hostsViewPage.errorOnCreateOrLoadDataviewTitle": "Erreur lors de la création d'un Data View", "xpack.infra.hostsViewPage.hostLimit": "Limite de l'hôte", "xpack.infra.hostsViewPage.hostLimit.tooltip": "Pour garantir des performances de recherche plus rapides, le nombre d'hôtes retournés est limité.", + "xpack.infra.hostsViewPage.kpi.hostCount.title": "Hôtes", "xpack.infra.hostsViewPage.landing.calloutReachOutToYourKibanaAdministrator": "Votre rôle d'utilisateur ne dispose pas des privilèges suffisants pour activer cette fonctionnalité - veuillez \n contacter votre administrateur Kibana et lui demander de visiter cette page pour activer la fonctionnalité.", "xpack.infra.hostsViewPage.landing.enableHostsView": "Activer la vue des hôtes", "xpack.infra.hostsViewPage.landing.introMessage": "Bienvenue sur la fonctionnalité \"Hôtes\", désormais disponible en version bêta. Avec ce puissant outil, \n vous pouvez facilement voir et analyser vos hôtes et identifier tout problème afin de les corriger rapidement. \n Obtenez une vue détaillée des indicateurs pour vos hôtes afin de savoir lesquels déclenchent le plus d’alertes, et filtrez \n les hôtes que vous voulez analyser à l'aide de tout filtre KQL ainsi que de répartitions simples comme le fournisseur cloud et \n le système d'exploitation.", @@ -18901,12 +18907,6 @@ "xpack.infra.hostsViewPage.metrics.tooltip.normalizedLoad1m": "Moyenne de la charge sur 1 minute normalisée par le nombre de cœurs de processeur. ", "xpack.infra.hostsViewPage.metrics.tooltip.rx": "Nombre d'octets qui ont été reçus par seconde sur les interfaces publiques des hôtes.", "xpack.infra.hostsViewPage.metrics.tooltip.tx": "Nombre d'octets envoyés par seconde sur les interfaces publiques des hôtes.", - "xpack.infra.hostsViewPage.metricTrend.cpuUsage.title": "Utilisation CPU", - "xpack.infra.hostsViewPage.metricTrend.diskSpaceUsage.title": "Utilisation de l’espace disque", - "xpack.infra.hostsViewPage.metricTrend.hostCount.title": "Hôtes", - "xpack.infra.hostsViewPage.metricTrend.memoryUsage.title": "Utilisation mémoire", - "xpack.infra.hostsViewPage.metricTrend.normalizedLoad1m.title": "Charge normalisée", - "xpack.infra.hostsViewPage.metricTrend.subtitle.average": "Moyenne", "xpack.infra.hostsViewPage.table.addFilter": "Ajouter un filtre", "xpack.infra.hostsViewPage.table.cpuUsageColumnHeader": "Utilisation CPU (moy.)", "xpack.infra.hostsViewPage.table.diskSpaceUsageColumnHeader": "Utilisation de l’espace disque (moy.)", @@ -29231,10 +29231,7 @@ "xpack.security.accountManagement.userProfile.passwordLabel": "Mot de passe", "xpack.security.accountManagement.userProfile.prepopulatedImageUrlPromptText": "Sélectionner ou glisser-déposer une image de remplacement", "xpack.security.accountManagement.userProfile.randomizeButton": "Randomiser", - "xpack.security.accountManagement.userProfile.requiresPageReloadToastButtonLabel": "Actualiser la page", - "xpack.security.accountManagement.userProfile.requiresPageReloadToastDescription": "Un ou plusieurs paramètres nécessitent d’actualiser la page pour pouvoir prendre effet.", "xpack.security.accountManagement.userProfile.submitErrorTitle": "Impossible de mettre à jour le profil", - "xpack.security.accountManagement.userProfile.submitSuccessTitle": "Profil mis à jour", "xpack.security.accountManagement.userProfile.themeFormGroupDescription": "Sélectionnez l'apparence de votre interface.", "xpack.security.accountManagement.userProfile.title": "Profil", "xpack.security.accountManagement.userProfile.usernameHelpText": "Le nom de l'utilisateur ne peut pas être modifié après la création du compte.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 66ea13be17908..ca71e8c833cb5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -18675,8 +18675,8 @@ "xpack.infra.homePage.toolbar.showingLastOneMinuteDataText": "指定期間のデータの最後の{duration}", "xpack.infra.hostsViewPage.errorOnCreateOrLoadDataview": "データビューの作成中にエラーが発生しました:{metricAlias}。ページを再読み込みしてください。", "xpack.infra.hostsViewPage.landing.calloutRoleClarificationWithDocsLink": "Kibanaの高度な設定にアクセスできるロールが必要です。{docsLink}", - "xpack.infra.hostsViewPage.metricTrend.subtitle.average.limit": "({limit}ホストの)平均", - "xpack.infra.hostsViewPage.metricTrend.subtitle.hostCount.limit": "{limit}に制限", + "xpack.infra.hostsViewPage.kpi.subtitle.average.limit": "({limit}ホストの)平均", + "xpack.infra.hostsViewPage.kpi.subtitle.hostCount.limit": "{limit}に制限", "xpack.infra.hostsViewPage.table.selectedHostsButton": "{selectedHostsCount}件の{selectedHostsCount, plural, =1 {ホスト} other {ホスト}}が選択済み", "xpack.infra.hostsViewPage.table.tooltip.documentationLabel": "詳細については、{documentation}を参照してください。", "xpack.infra.inventoryTimeline.header": "平均{metricLabel}", @@ -18815,6 +18815,11 @@ "xpack.infra.assetDetailsEmbeddable.description": "アセット詳細ビューを追加します。", "xpack.infra.assetDetailsEmbeddable.displayName": "アセット詳細", "xpack.infra.assetDetailsEmbeddable.title": "アセット詳細", + "xpack.infra.assetDetailsEmbeddable.overview.kpi.cpuUsage.title": "CPU使用状況", + "xpack.infra.assetDetailsEmbeddable.overview.kpi.diskSpaceUsage.title": "ディスク容量使用状況", + "xpack.infra.assetDetailsEmbeddable.overview.kpi.memoryUsage.title": "メモリー使用状況", + "xpack.infra.assetDetailsEmbeddable.overview.kpi.normalizedLoad1m.title": "正規化された負荷", + "xpack.infra.assetDetailsEmbeddable.overview.kpi.subtitle.average": "平均", "xpack.infra.bottomDrawer.kubernetesDashboardsLink": "Kubernetesダッシュボード", "xpack.infra.chartSection.missingMetricDataBody": "このチャートはデータが欠けています。", "xpack.infra.chartSection.missingMetricDataText": "データが欠けています", @@ -18899,6 +18904,7 @@ "xpack.infra.hostsViewPage.errorOnCreateOrLoadDataviewTitle": "データビューの作成エラー", "xpack.infra.hostsViewPage.hostLimit": "ホスト制限", "xpack.infra.hostsViewPage.hostLimit.tooltip": "クエリパフォーマンスを確実に高めるために、返されるホスト数には制限があります", + "xpack.infra.hostsViewPage.kpi.hostCount.title": "ホスト", "xpack.infra.hostsViewPage.landing.calloutReachOutToYourKibanaAdministrator": "ユーザーロールには、この機能を有効にするための十分な権限がありません。 \n この機能を有効にするために、Kibana管理者に連絡して、このページにアクセスするように依頼してください。", "xpack.infra.hostsViewPage.landing.enableHostsView": "ホストビューを有効化", "xpack.infra.hostsViewPage.landing.introMessage": "「ホスト」機能へようこそ!ベータ版でご利用いただけるようになりました。この強力なツールを使用すると、\n ホストを簡単に表示、分析し、あらゆる問題を特定して、迅速に対処できます。\n ホストのメトリックを詳細に表示し、どのメトリックが最も多くのアラートをトリガーしているかを確認し、 \n 任意のKQLフィルターを使用して分析したいホストや、クラウドプロバイダーやオペレーティングシステムといった簡単な内訳をフィルターできます \n 。", @@ -18915,12 +18921,6 @@ "xpack.infra.hostsViewPage.metrics.tooltip.normalizedLoad1m": "CPUコア数で正規化した1分間の負荷平均。", "xpack.infra.hostsViewPage.metrics.tooltip.rx": "ホストのパブリックインターフェースで1秒間に受信したバイト数。", "xpack.infra.hostsViewPage.metrics.tooltip.tx": "ホストのパブリックインターフェースで1秒間に送信したバイト数。", - "xpack.infra.hostsViewPage.metricTrend.cpuUsage.title": "CPU使用状況", - "xpack.infra.hostsViewPage.metricTrend.diskSpaceUsage.title": "ディスク容量使用状況", - "xpack.infra.hostsViewPage.metricTrend.hostCount.title": "ホスト", - "xpack.infra.hostsViewPage.metricTrend.memoryUsage.title": "メモリー使用状況", - "xpack.infra.hostsViewPage.metricTrend.normalizedLoad1m.title": "正規化された負荷", - "xpack.infra.hostsViewPage.metricTrend.subtitle.average": "平均", "xpack.infra.hostsViewPage.table.addFilter": "フィルターを追加します", "xpack.infra.hostsViewPage.table.cpuUsageColumnHeader": "CPU使用状況(平均)", "xpack.infra.hostsViewPage.table.diskSpaceUsageColumnHeader": "ディスク容量使用状況(平均)", @@ -29230,10 +29230,7 @@ "xpack.security.accountManagement.userProfile.passwordLabel": "パスワード", "xpack.security.accountManagement.userProfile.prepopulatedImageUrlPromptText": "置換画像を選択するかドラッグアンドドロップします", "xpack.security.accountManagement.userProfile.randomizeButton": "ランダム化", - "xpack.security.accountManagement.userProfile.requiresPageReloadToastButtonLabel": "ページを再読み込み", - "xpack.security.accountManagement.userProfile.requiresPageReloadToastDescription": "設定を有効にするためにページの再読み込みが必要です。", "xpack.security.accountManagement.userProfile.submitErrorTitle": "プロファイルを更新できませんでした", - "xpack.security.accountManagement.userProfile.submitSuccessTitle": "プロファイルが更新されました", "xpack.security.accountManagement.userProfile.themeFormGroupDescription": "インターフェースの表示を選択します。", "xpack.security.accountManagement.userProfile.title": "プロフィール", "xpack.security.accountManagement.userProfile.usernameHelpText": "アカウントの作成後は、ユーザー名を変更できません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7b86064bc4322..ef71636689383 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -18675,8 +18675,8 @@ "xpack.infra.homePage.toolbar.showingLastOneMinuteDataText": "选定时间过去 {duration}的数据", "xpack.infra.hostsViewPage.errorOnCreateOrLoadDataview": "尝试创建以下数据视图时出错:{metricAlias}。尝试重新加载该页面。", "xpack.infra.hostsViewPage.landing.calloutRoleClarificationWithDocsLink": "他们将需要有权访问 Kibana 中的高级设置的角色。{docsLink}", - "xpack.infra.hostsViewPage.metricTrend.subtitle.average.limit": "平均值(属于 {limit} 台主机)", - "xpack.infra.hostsViewPage.metricTrend.subtitle.hostCount.limit": "限定为 {limit}", + "xpack.infra.hostsViewPage.kpi.subtitle.average.limit": "平均值(属于 {limit} 台主机)", + "xpack.infra.hostsViewPage.kpi.subtitle.hostCount.limit": "限定为 {limit}", "xpack.infra.hostsViewPage.table.selectedHostsButton": "已选定 {selectedHostsCount} 个{selectedHostsCount, plural, =1 {主机} other {主机}}", "xpack.infra.hostsViewPage.table.tooltip.documentationLabel": "请参阅 {documentation} 了解更多信息", "xpack.infra.inventoryTimeline.header": "平均值 {metricLabel}", @@ -18815,6 +18815,11 @@ "xpack.infra.assetDetailsEmbeddable.description": "添加资产详情视图。", "xpack.infra.assetDetailsEmbeddable.displayName": "资产详情", "xpack.infra.assetDetailsEmbeddable.title": "资产详情", + "xpack.infra.assetDetailsEmbeddable.overview.kpi.cpuUsage.title": "CPU 使用率", + "xpack.infra.assetDetailsEmbeddable.overview.kpi.diskSpaceUsage.title": "磁盘空间使用率", + "xpack.infra.assetDetailsEmbeddable.overview.kpi.memoryUsage.title": "内存利用率", + "xpack.infra.assetDetailsEmbeddable.overview.kpi.normalizedLoad1m.title": "标准化负载", + "xpack.infra.assetDetailsEmbeddable.overview.kpi.subtitle.average": "平均值", "xpack.infra.bottomDrawer.kubernetesDashboardsLink": "Kubernetes 仪表板", "xpack.infra.chartSection.missingMetricDataBody": "此图表的数据缺失。", "xpack.infra.chartSection.missingMetricDataText": "缺失数据", @@ -18899,6 +18904,7 @@ "xpack.infra.hostsViewPage.errorOnCreateOrLoadDataviewTitle": "创建数据视图时出错", "xpack.infra.hostsViewPage.hostLimit": "主机限制", "xpack.infra.hostsViewPage.hostLimit.tooltip": "为确保更快的查询性能,对返回的主机数量实施了限制", + "xpack.infra.hostsViewPage.kpi.hostCount.title": "主机", "xpack.infra.hostsViewPage.landing.calloutReachOutToYourKibanaAdministrator": "您的用户角色权限不足,无法启用此功能 - 请 \n 联系您的 Kibana 管理员,要求他们访问此页面以启用该功能。", "xpack.infra.hostsViewPage.landing.enableHostsView": "启用主机视图", "xpack.infra.hostsViewPage.landing.introMessage": "欢迎使用“主机”功能,该功能现在为公测版!使用这个强大的工具,\n 您可以轻松查看并分析主机,并确定任何问题以便快速予以解决。\n 获取您主机的详细指标视图,了解哪些指标触发了大多数告警, \n 并使用任何 KQL 筛选以及云提供商和操作系统等常见细目筛选 \n 您要分析的主机。", @@ -18915,12 +18921,6 @@ "xpack.infra.hostsViewPage.metrics.tooltip.normalizedLoad1m": "1 分钟负载平均值,按 CPU 核心数进行标准化。", "xpack.infra.hostsViewPage.metrics.tooltip.rx": "主机的公共接口上每秒接收的字节数。", "xpack.infra.hostsViewPage.metrics.tooltip.tx": "主机的公共接口上每秒发送的字节数。", - "xpack.infra.hostsViewPage.metricTrend.cpuUsage.title": "CPU 使用率", - "xpack.infra.hostsViewPage.metricTrend.diskSpaceUsage.title": "磁盘空间使用率", - "xpack.infra.hostsViewPage.metricTrend.hostCount.title": "主机", - "xpack.infra.hostsViewPage.metricTrend.memoryUsage.title": "内存利用率", - "xpack.infra.hostsViewPage.metricTrend.normalizedLoad1m.title": "标准化负载", - "xpack.infra.hostsViewPage.metricTrend.subtitle.average": "平均值", "xpack.infra.hostsViewPage.table.addFilter": "添加筛选", "xpack.infra.hostsViewPage.table.cpuUsageColumnHeader": "CPU 使用率(平均值)", "xpack.infra.hostsViewPage.table.diskSpaceUsageColumnHeader": "磁盘空间使用率(平均值)", @@ -29226,10 +29226,7 @@ "xpack.security.accountManagement.userProfile.passwordLabel": "密码", "xpack.security.accountManagement.userProfile.prepopulatedImageUrlPromptText": "选择或拖放替换图像", "xpack.security.accountManagement.userProfile.randomizeButton": "随机化", - "xpack.security.accountManagement.userProfile.requiresPageReloadToastButtonLabel": "重新加载页面", - "xpack.security.accountManagement.userProfile.requiresPageReloadToastDescription": "一个或多个设置需要您重新加载页面才能生效。", "xpack.security.accountManagement.userProfile.submitErrorTitle": "无法更新配置文件", - "xpack.security.accountManagement.userProfile.submitSuccessTitle": "配置文件已更新", "xpack.security.accountManagement.userProfile.themeFormGroupDescription": "选择您界面的外观。", "xpack.security.accountManagement.userProfile.title": "配置文件", "xpack.security.accountManagement.userProfile.usernameHelpText": "创建帐户后无法更改用户名。", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx index 3bb65037d5c93..346dce44af60b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx @@ -20,6 +20,7 @@ interface Props { isDisabled?: boolean; editAction: (property: string, value: any, index: number) => void; label: string; + helpText?: string; errors?: string[]; } @@ -32,6 +33,7 @@ export const TextAreaWithMessageVariables: React.FunctionComponent = ({ editAction, label, errors, + helpText, }) => { const [currentTextElement, setCurrentTextElement] = useState(null); @@ -64,6 +66,7 @@ export const TextAreaWithMessageVariables: React.FunctionComponent = ({ paramsProperty={paramsProperty} /> } + helpText={helpText} > { actions: { - selectTab: (tab: 'edit' | 'simulate') => void; - clickSubmitButton: () => void; - clickSimulateButton: () => void; + selectTab: (tab: 'edit' | 'simulate') => Promise; + clickSubmitButton: () => Promise; + clickSimulateButton: () => Promise; }; } export const setup = async (httpSetup: HttpSetup): Promise => { const initTestBed = registerTestBed(WithAppDependencies(WatchEditPage, httpSetup), testBedConfig); const testBed = await initTestBed(); + const { find, component } = testBed; /** * User Actions */ - const selectTab = (tab: 'edit' | 'simulate') => { + const selectTab = async (tab: 'edit' | 'simulate') => { const tabs = ['edit', 'simulate']; - testBed.find('tab').at(tabs.indexOf(tab)).simulate('click'); + await act(async () => { + find('tab').at(tabs.indexOf(tab)).simulate('click'); + }); + + component.update(); }; - const clickSubmitButton = () => { - testBed.find('saveWatchButton').simulate('click'); + const clickSubmitButton = async () => { + await act(async () => { + testBed.find('saveWatchButton').simulate('click'); + }); + + component.update(); }; - const clickSimulateButton = () => { - testBed.find('simulateWatchButton').simulate('click'); + const clickSimulateButton = async () => { + await act(async () => { + testBed.find('simulateWatchButton').simulate('click'); + }); + + component.update(); }; return { diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold_page.helpers.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold_page.helpers.ts index d2bb49a861167..b64ea8b003843 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold_page.helpers.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold_page.helpers.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { act } from 'react-dom/test-utils'; + import { registerTestBed, TestBed, AsyncTestBedConfig } from '@kbn/test-jest-helpers'; import { HttpSetup } from '@kbn/core/public'; @@ -24,39 +26,56 @@ const testBedConfig: AsyncTestBedConfig = { export interface WatchCreateThresholdTestBed extends TestBed { actions: { - clickSubmitButton: () => void; - clickAddActionButton: () => void; + clickSubmitButton: () => Promise; + clickAddActionButton: () => Promise; clickActionLink: ( actionType: 'logging' | 'email' | 'webhook' | 'index' | 'slack' | 'jira' | 'pagerduty' - ) => void; - clickSimulateButton: () => void; + ) => Promise; + clickSimulateButton: () => Promise; }; } export const setup = async (httpSetup: HttpSetup): Promise => { const initTestBed = registerTestBed(WithAppDependencies(WatchEditPage, httpSetup), testBedConfig); const testBed = await initTestBed(); + const { find, component } = testBed; /** * User Actions */ - const clickSubmitButton = () => { - testBed.find('saveWatchButton').simulate('click'); + const clickSubmitButton = async () => { + await act(async () => { + find('saveWatchButton').simulate('click'); + }); + + component.update(); }; - const clickAddActionButton = () => { - testBed.find('addWatchActionButton').simulate('click'); + const clickAddActionButton = async () => { + await act(async () => { + find('addWatchActionButton').simulate('click'); + }); + + component.update(); }; - const clickSimulateButton = () => { - testBed.find('simulateActionButton').simulate('click'); + const clickSimulateButton = async () => { + await act(async () => { + find('simulateActionButton').simulate('click'); + }); + + component.update(); }; - const clickActionLink = ( + const clickActionLink = async ( actionType: 'logging' | 'email' | 'webhook' | 'index' | 'slack' | 'jira' | 'pagerduty' ) => { - testBed.find(`${actionType}ActionButton`).simulate('click'); + await act(async () => { + find(`${actionType}ActionButton`).simulate('click'); + }); + + component.update(); }; return { diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_edit_page.helpers.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_edit_page.helpers.ts index 20f8389c0fec7..8e528af2b3366 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_edit_page.helpers.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_edit_page.helpers.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBed, AsyncTestBedConfig } from '@kbn/test-jest-helpers'; import { HttpSetup } from '@kbn/core/public'; @@ -25,7 +26,7 @@ const testBedConfig: AsyncTestBedConfig = { export interface WatchEditTestBed extends TestBed { actions: { - clickSubmitButton: () => void; + clickSubmitButton: () => Promise; }; } @@ -37,8 +38,12 @@ export const setup = async (httpSetup: HttpSetup): Promise => * User Actions */ - const clickSubmitButton = () => { - testBed.find('saveWatchButton').simulate('click'); + const clickSubmitButton = async () => { + await act(async () => { + testBed.find('saveWatchButton').simulate('click'); + }); + + testBed.component.update(); }; return { diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list_page.helpers.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list_page.helpers.ts index 3361619ee9dbb..cd4fd2ab2cfba 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list_page.helpers.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list_page.helpers.ts @@ -27,9 +27,9 @@ const testBedConfig: AsyncTestBedConfig = { export interface WatchListTestBed extends TestBed { actions: { - selectWatchAt: (index: number) => void; - clickWatchActionAt: (index: number, action: 'delete' | 'edit') => void; - searchWatches: (term: string) => void; + selectWatchAt: (index: number) => Promise; + clickWatchActionAt: (index: number, action: 'delete' | 'edit') => Promise; + searchWatches: (term: string) => Promise; advanceTimeToTableRefresh: () => Promise; }; } @@ -42,11 +42,15 @@ export const setup = async (httpSetup: HttpSetup): Promise => * User Actions */ - const selectWatchAt = (index: number) => { + const selectWatchAt = async (index: number) => { const { rows } = testBed.table.getMetaData('watchesTable'); const row = rows[index]; const checkBox = row.reactWrapper.find('input').hostNodes(); - checkBox.simulate('change', { target: { checked: true } }); + + await act(async () => { + checkBox.simulate('change', { target: { checked: true } }); + }); + testBed.component.update(); }; const clickWatchActionAt = async (index: number, action: 'delete' | 'edit') => { @@ -58,18 +62,21 @@ export const setup = async (httpSetup: HttpSetup): Promise => await act(async () => { button.simulate('click'); - component.update(); }); + component.update(); }; - const searchWatches = (term: string) => { + const searchWatches = async (term: string) => { const { find, component } = testBed; const searchInput = find('watchesTableContainer').find('.euiFieldSearch'); // Enter input into the search box // @ts-ignore searchInput.instance().value = term; - searchInput.simulate('keyup', { key: 'Enter', keyCode: 13, which: 13 }); + + await act(async () => { + searchInput.simulate('keyup', { key: 'Enter', keyCode: 13, which: 13 }); + }); component.update(); }; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_status_page.helpers.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_status_page.helpers.ts index 601857ca941f0..7c6da257b1700 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_status_page.helpers.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_status_page.helpers.ts @@ -32,11 +32,11 @@ const testBedConfig: AsyncTestBedConfig = { export interface WatchStatusTestBed extends TestBed { actions: { - selectTab: (tab: 'execution history' | 'action statuses') => void; - clickToggleActivationButton: () => void; - clickAcknowledgeButton: (index: number) => void; - clickDeleteWatchButton: () => void; - clickWatchExecutionAt: (index: number, tableCellText: string) => void; + selectTab: (tab: 'execution history' | 'action statuses') => Promise; + clickToggleActivationButton: () => Promise; + clickAcknowledgeButton: (index: number) => Promise; + clickDeleteWatchButton: () => Promise; + clickWatchExecutionAt: (index: number, tableCellText: string) => Promise; }; } @@ -51,10 +51,14 @@ export const setup = async (httpSetup: HttpSetup): Promise = * User Actions */ - const selectTab = (tab: 'execution history' | 'action statuses') => { + const selectTab = async (tab: 'execution history' | 'action statuses') => { + const { component, find } = testBed; const tabs = ['execution history', 'action statuses']; - testBed.find('tab').at(tabs.indexOf(tab)).simulate('click'); + await act(async () => { + find('tab').at(tabs.indexOf(tab)).simulate('click'); + }); + component.update(); }; const clickToggleActivationButton = async () => { @@ -63,8 +67,8 @@ export const setup = async (httpSetup: HttpSetup): Promise = await act(async () => { button.simulate('click'); - component.update(); }); + component.update(); }; const clickAcknowledgeButton = async (index: number) => { @@ -76,8 +80,8 @@ export const setup = async (httpSetup: HttpSetup): Promise = await act(async () => { button.simulate('click'); - component.update(); }); + component.update(); }; const clickDeleteWatchButton = async () => { @@ -86,8 +90,8 @@ export const setup = async (httpSetup: HttpSetup): Promise = await act(async () => { button.simulate('click'); - component.update(); }); + component.update(); }; const clickWatchExecutionAt = async (index: number, tableCellText: string) => { diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json_page.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json_page.test.tsx similarity index 88% rename from x-pack/plugins/watcher/__jest__/client_integration/watch_create_json_page.test.ts rename to x-pack/plugins/watcher/__jest__/client_integration/watch_create_json_page.test.tsx index f79a72b456afb..f6231ab328d25 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json_page.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json_page.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import React from 'react'; import { act } from 'react-dom/test-utils'; import { getExecuteDetails } from '../../__fixtures__'; @@ -16,12 +17,29 @@ import { WATCH } from './helpers/jest_constants'; const { setup } = pageHelpers.watchCreateJsonPage; +jest.mock('@kbn/kibana-react-plugin/public', () => { + const original = jest.requireActual('@kbn/kibana-react-plugin/public'); + return { + ...original, + // Mocking CodeEditor, which uses React Monaco under the hood + CodeEditor: (props: any) => ( + { + props.onChange(e.jsonContent); + }} + /> + ), + }; +}); + describe(' create route', () => { const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchCreateJsonTestBed; beforeAll(() => { - jest.useFakeTimers({ legacyFakeTimers: true }); + jest.useFakeTimers(); }); afterAll(() => { @@ -30,7 +48,10 @@ describe(' create route', () => { describe('on component mount', () => { beforeEach(async () => { - testBed = await setup(httpSetup); + await act(async () => { + testBed = await setup(httpSetup); + }); + testBed.component.update(); }); @@ -47,13 +68,13 @@ describe(' create route', () => { expect(find('tab').map((t) => t.text())).toEqual(['Edit', 'Simulate']); }); - test('should navigate to the "Simulate" tab', () => { + test('should navigate to the "Simulate" tab', async () => { const { exists, actions } = testBed; expect(exists('jsonWatchForm')).toBe(true); expect(exists('jsonWatchSimulateForm')).toBe(false); - actions.selectTab('simulate'); + await actions.selectTab('simulate'); expect(exists('jsonWatchForm')).toBe(false); expect(exists('jsonWatchSimulateForm')).toBe(true); @@ -62,19 +83,19 @@ describe(' create route', () => { describe('create', () => { describe('form validation', () => { - test('should not allow empty ID field', () => { + test('should not allow empty ID field', async () => { const { form, actions } = testBed; form.setInputValue('idInput', ''); - actions.clickSubmitButton(); + await actions.clickSubmitButton(); expect(form.getErrorsMessages()).toContain('ID is required'); }); - test('should not allow invalid characters for ID field', () => { + test('should not allow invalid characters for ID field', async () => { const { form, actions } = testBed; form.setInputValue('idInput', 'invalid$id*field/'); - actions.clickSubmitButton(); + await actions.clickSubmitButton(); expect(form.getErrorsMessages()).toContain( 'ID can only contain letters, underscores, dashes, periods and numbers.' @@ -90,9 +111,7 @@ describe(' create route', () => { form.setInputValue('nameInput', watch.name); form.setInputValue('idInput', watch.id); - await act(async () => { - actions.clickSubmitButton(); - }); + await actions.clickSubmitButton(); const DEFAULT_LOGGING_ACTION_ID = 'logging_1'; const DEFAULT_LOGGING_ACTION_TYPE = 'logging'; @@ -125,7 +144,7 @@ describe(' create route', () => { }); test('should surface the API errors from the "save" HTTP request', async () => { - const { form, actions, component, exists, find } = testBed; + const { form, actions, exists, find } = testBed; const { watch } = WATCH; form.setInputValue('nameInput', watch.name); @@ -140,10 +159,7 @@ describe(' create route', () => { httpRequestsMockHelpers.setSaveWatchResponse(watch.id, undefined, error); - await act(async () => { - actions.clickSubmitButton(); - }); - component.update(); + await actions.clickSubmitButton(); expect(exists('sectionError')).toBe(true); expect(find('sectionError').text()).toContain(error.message); @@ -152,12 +168,13 @@ describe(' create route', () => { }); describe('simulate', () => { - beforeEach(() => { + beforeEach(async () => { const { actions, form } = testBed; // Set watch id (required field) and switch to simulate tab form.setInputValue('idInput', WATCH.watch.id); - actions.selectTab('simulate'); + + await actions.selectTab('simulate'); }); describe('form payload & API errors', () => { @@ -167,9 +184,7 @@ describe(' create route', () => { watch: { id, type }, } = WATCH; - await act(async () => { - actions.clickSimulateButton(); - }); + await actions.clickSimulateButton(); const actionModes = Object.keys(defaultWatch.actions).reduce( (actionAccum: any, action) => { @@ -202,7 +217,7 @@ describe(' create route', () => { }); test('should execute a watch with a valid payload', async () => { - const { actions, form, find, exists, component } = testBed; + const { actions, form, find, exists } = testBed; const { watch: { id, type }, } = WATCH; @@ -228,10 +243,7 @@ describe(' create route', () => { }, }); - await act(async () => { - actions.clickSimulateButton(); - }); - component.update(); + await actions.clickSimulateButton(); const actionModes = Object.keys(defaultWatch.actions).reduce( (actionAccum: any, action) => { @@ -303,7 +315,7 @@ describe(' create route', () => { conditionMet ? 'when the condition is met' : 'when the condition is not met', () => { beforeEach(async () => { - const { actions, component, form } = testBed; + const { actions, form } = testBed; form.setInputValue('actionModesSelect', actionMode); httpRequestsMockHelpers.setLoadExecutionResultResponse({ @@ -335,10 +347,7 @@ describe(' create route', () => { }, }); - await act(async () => { - actions.clickSimulateButton(); - }); - component.update(); + await actions.clickSimulateButton(); }); test('should set the correct condition met status', () => { diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold_page.test.tsx b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold_page.test.tsx index d7df7efd7be12..8260bd75ebb04 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold_page.test.tsx +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold_page.test.tsx @@ -23,16 +23,6 @@ const MATCH_INDICES = ['index1']; const ES_FIELDS = [{ name: '@timestamp', type: 'date' }]; -// Since watchID's are dynamically created, we have to mock -// the function that generates them in order to be able to match -// against it. -jest.mock('uuid', () => ({ - v4: () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - return require('./helpers/jest_constants').WATCH_ID; - }, -})); - const SETTINGS = { action_types: { email: { enabled: true }, @@ -55,6 +45,37 @@ const WATCH_VISUALIZE_DATA = { }, }; +// Since watchID's are dynamically created, we have to mock +// the function that generates them in order to be able to match +// against it. +jest.mock('uuid', () => ({ + v4: () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require('./helpers/jest_constants').WATCH_ID; + }, + v1: () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require('./helpers/jest_constants').WATCH_ID; + }, +})); + +jest.mock('@kbn/kibana-react-plugin/public', () => { + const original = jest.requireActual('@kbn/kibana-react-plugin/public'); + return { + ...original, + // Mocking CodeEditor, which uses React Monaco under the hood + CodeEditor: (props: any) => ( + { + props.onChange(e.jsonContent); + }} + /> + ), + }; +}); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -82,22 +103,27 @@ describe(' create route', () => { let testBed: WatchCreateThresholdTestBed; beforeAll(() => { - jest.useFakeTimers({ legacyFakeTimers: true }); + jest.useFakeTimers(); }); afterAll(() => { jest.useRealTimers(); }); - describe('on component mount', () => { - beforeEach(async () => { - await act(async () => { - testBed = await setup(httpSetup); - }); + httpRequestsMockHelpers.setLoadMatchingIndicesResponse({ indices: MATCH_INDICES }); + httpRequestsMockHelpers.setLoadEsFieldsResponse({ fields: ES_FIELDS }); + httpRequestsMockHelpers.setLoadSettingsResponse(SETTINGS); + httpRequestsMockHelpers.setLoadWatchVisualizeResponse(WATCH_VISUALIZE_DATA); - testBed.component.update(); + beforeEach(async () => { + await act(async () => { + testBed = await setup(httpSetup); }); + testBed.component.update(); + }); + + describe('on component mount', () => { test('should set the correct page title', () => { const { find } = testBed; @@ -105,13 +131,6 @@ describe(' create route', () => { }); describe('create', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadMatchingIndicesResponse({ indices: MATCH_INDICES }); - httpRequestsMockHelpers.setLoadEsFieldsResponse({ fields: ES_FIELDS }); - httpRequestsMockHelpers.setLoadSettingsResponse(SETTINGS); - httpRequestsMockHelpers.setLoadWatchVisualizeResponse(WATCH_VISUALIZE_DATA); - }); - describe('form validation', () => { test('should not allow empty name field', () => { const { form } = testBed; @@ -216,14 +235,21 @@ describe(' create route', () => { describe('actions', () => { beforeEach(async () => { + await act(async () => { + testBed = await setup(httpSetup); + }); + const { form, find, component } = testBed; + component.update(); + // Set up valid fields needed for actions component to render await act(async () => { form.setInputValue('nameInput', WATCH_NAME); find('indicesComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); form.setInputValue('watchTimeFieldSelect', WATCH_TIME_FIELD); }); + component.update(); }); @@ -232,8 +258,8 @@ describe(' create route', () => { const LOGGING_MESSAGE = 'test log message'; - actions.clickAddActionButton(); - actions.clickActionLink('logging'); + await actions.clickAddActionButton(); + await actions.clickActionLink('logging'); expect(exists('watchActionAccordion')).toBe(true); @@ -246,9 +272,7 @@ describe(' create route', () => { // Next, provide valid field and verify form.setInputValue('loggingTextInput', LOGGING_MESSAGE); - await act(async () => { - actions.clickSimulateButton(); - }); + await actions.clickSimulateButton(); const thresholdWatch = { id: WATCH_ID, @@ -300,17 +324,15 @@ describe(' create route', () => { test('should simulate an index action', async () => { const { form, actions, exists } = testBed; - actions.clickAddActionButton(); - actions.clickActionLink('index'); + await actions.clickAddActionButton(); + await actions.clickActionLink('index'); expect(exists('watchActionAccordion')).toBe(true); // Verify an empty index is allowed form.setInputValue('indexInput', ''); - await act(async () => { - actions.clickSimulateButton(); - }); + await actions.clickSimulateButton(); const thresholdWatch = { id: WATCH_ID, @@ -363,16 +385,14 @@ describe(' create route', () => { const SLACK_MESSAGE = 'test slack message'; - actions.clickAddActionButton(); - actions.clickActionLink('slack'); + await actions.clickAddActionButton(); + await actions.clickActionLink('slack'); expect(exists('watchActionAccordion')).toBe(true); form.setInputValue('slackMessageTextarea', SLACK_MESSAGE); - await act(async () => { - actions.clickSimulateButton(); - }); + await actions.clickSimulateButton(); const thresholdWatch = { id: WATCH_ID, @@ -430,8 +450,8 @@ describe(' create route', () => { const EMAIL_SUBJECT = 'test email subject'; const EMAIL_BODY = 'this is a test email body'; - actions.clickAddActionButton(); - actions.clickActionLink('email'); + await actions.clickAddActionButton(); + await actions.clickActionLink('email'); expect(exists('watchActionAccordion')).toBe(true); @@ -442,9 +462,7 @@ describe(' create route', () => { form.setInputValue('emailSubjectInput', EMAIL_SUBJECT); form.setInputValue('emailBodyInput', EMAIL_BODY); - await act(async () => { - actions.clickSimulateButton(); - }); + await actions.clickSimulateButton(); const thresholdWatch = { id: WATCH_ID, @@ -510,8 +528,8 @@ describe(' create route', () => { const USERNAME = 'test_user'; const PASSWORD = 'test_password'; - actions.clickAddActionButton(); - actions.clickActionLink('webhook'); + await actions.clickAddActionButton(); + await actions.clickActionLink('webhook'); expect(exists('watchActionAccordion')).toBe(true); @@ -534,9 +552,7 @@ describe(' create route', () => { form.setInputValue('webhookUsernameInput', USERNAME); form.setInputValue('webhookPasswordInput', PASSWORD); - await act(async () => { - actions.clickSimulateButton(); - }); + await actions.clickSimulateButton(); const thresholdWatch = { id: WATCH_ID, @@ -600,14 +616,18 @@ describe(' create route', () => { const ISSUE_TYPE = 'Bug'; const SUMMARY = 'test Jira summary'; - actions.clickAddActionButton(); - actions.clickActionLink('jira'); + await actions.clickAddActionButton(); + await actions.clickActionLink('jira'); expect(exists('watchActionAccordion')).toBe(true); - // First, provide invalid fields and verify - form.setInputValue('jiraProjectKeyInput', ''); + // Set issue type value and clear it to trigger required error message + form.setInputValue('jiraIssueTypeInput', 'myissue'); form.setInputValue('jiraIssueTypeInput', ''); + // Set project type value and clear it to trigger required error message + form.setInputValue('jiraProjectKeyInput', 'my key'); + form.setInputValue('jiraProjectKeyInput', ''); + // Clear default summary to trigger required error message form.setInputValue('jiraSummaryInput', ''); expect(form.getErrorsMessages()).toEqual([ @@ -622,9 +642,7 @@ describe(' create route', () => { form.setInputValue('jiraIssueTypeInput', ISSUE_TYPE); form.setInputValue('jiraSummaryInput', SUMMARY); - await act(async () => { - actions.clickSimulateButton(); - }); + await actions.clickSimulateButton(); const thresholdWatch = { id: WATCH_ID, @@ -688,8 +706,8 @@ describe(' create route', () => { const DESCRIPTION = 'test pagerduty description'; - actions.clickAddActionButton(); - actions.clickActionLink('pagerduty'); + await actions.clickAddActionButton(); + await actions.clickActionLink('pagerduty'); expect(exists('watchActionAccordion')).toBe(true); @@ -702,9 +720,7 @@ describe(' create route', () => { // Next, provide valid fields and verify form.setInputValue('pagerdutyDescriptionInput', DESCRIPTION); - await act(async () => { - actions.clickSimulateButton(); - }); + await actions.clickSimulateButton(); const thresholdWatch = { id: WATCH_ID, @@ -757,7 +773,6 @@ describe(' create route', () => { describe('watch visualize data payload', () => { test('should send the correct payload', async () => { const { form, find, component } = testBed; - // Set up required fields await act(async () => { form.setInputValue('nameInput', WATCH_NAME); @@ -807,9 +822,7 @@ describe(' create route', () => { }); component.update(); - await act(async () => { - actions.clickSubmitButton(); - }); + await actions.clickSubmitButton(); expect(httpSetup.put).toHaveBeenLastCalledWith( `${API_BASE_PATH}/watch/${WATCH_ID}`, diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_edit_page.test.tsx b/x-pack/plugins/watcher/__jest__/client_integration/watch_edit_page.test.tsx index ba38fbd5b3cc7..b00f9916970b9 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_edit_page.test.tsx +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_edit_page.test.tsx @@ -39,7 +39,7 @@ describe('', () => { let testBed: WatchEditTestBed; beforeAll(() => { - jest.useFakeTimers({ legacyFakeTimers: true }); + jest.useFakeTimers(); }); afterAll(() => { @@ -50,7 +50,10 @@ describe('', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadWatchResponse(WATCH_ID, WATCH); - testBed = await setup(httpSetup); + await act(async () => { + testBed = await setup(httpSetup); + }); + testBed.component.update(); }); @@ -82,9 +85,7 @@ describe('', () => { form.setInputValue('nameInput', EDITED_WATCH_NAME); - await act(async () => { - actions.clickSubmitButton(); - }); + await actions.clickSubmitButton(); const DEFAULT_LOGGING_ACTION_ID = 'logging_1'; const DEFAULT_LOGGING_ACTION_TYPE = 'logging'; @@ -137,7 +138,10 @@ describe('', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadWatchResponse(WATCH_ID, { watch }); - testBed = await setup(httpSetup); + await act(async () => { + testBed = await setup(httpSetup); + }); + testBed.component.update(); }); @@ -162,9 +166,7 @@ describe('', () => { form.setInputValue('nameInput', EDITED_WATCH_NAME); - await act(async () => { - actions.clickSubmitButton(); - }); + await actions.clickSubmitButton(); const { id, diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_list_page.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_list_page.test.ts index c9116f45c9314..8eacd4841c666 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_list_page.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_list_page.test.ts @@ -18,7 +18,7 @@ describe('', () => { let testBed: WatchListTestBed; beforeAll(() => { - jest.useFakeTimers({ legacyFakeTimers: true }); + jest.useFakeTimers(); }); afterAll(() => { @@ -79,7 +79,7 @@ describe('', () => { test('should show error callout if search is invalid', async () => { const { exists, actions } = testBed; - actions.searchWatches('or'); + await actions.searchWatches('or'); expect(exists('watcherListSearchError')).toBe(true); }); @@ -87,7 +87,7 @@ describe('', () => { test('should retain the search query', async () => { const { table, actions } = testBed; - actions.searchWatches(watch1.name); + await actions.searchWatches(watch1.name); const { tableCellsValues } = table.getMetaData('watchesTable'); diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_status_page.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_status_page.test.ts index 20b503d82ad1d..d40015776065f 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_status_page.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_status_page.test.ts @@ -45,7 +45,7 @@ describe('', () => { let testBed: WatchStatusTestBed; beforeAll(() => { - jest.useFakeTimers({ legacyFakeTimers: true }); + jest.useFakeTimers(); }); afterAll(() => { @@ -57,7 +57,9 @@ describe('', () => { httpRequestsMockHelpers.setLoadWatchResponse(WATCH_ID, { watch }); httpRequestsMockHelpers.setLoadWatchHistoryResponse(WATCH_ID, watchHistoryItems); - testBed = await setup(httpSetup); + await act(async () => { + testBed = await setup(httpSetup); + }); testBed.component.update(); }); @@ -75,13 +77,13 @@ describe('', () => { expect(find('tab').map((t) => t.text())).toEqual(['Execution history', 'Action statuses']); }); - test('should navigate to the "Action statuses" tab', () => { + test('should navigate to the "Action statuses" tab', async () => { const { exists, actions } = testBed; expect(exists('watchHistorySection')).toBe(true); expect(exists('watchDetailSection')).toBe(false); - actions.selectTab('action statuses'); + await actions.selectTab('action statuses'); expect(exists('watchHistorySection')).toBe(false); expect(exists('watchDetailSection')).toBe(true); @@ -222,10 +224,10 @@ describe('', () => { }); describe('action statuses', () => { - beforeEach(() => { + beforeEach(async () => { const { actions } = testBed; - actions.selectTab('action statuses'); + await actions.selectTab('action statuses'); }); test('should list the watch actions in a table', () => { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/install_resources.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/install_resources.ts index b6c86b49c7fba..e80a8f94d93b6 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/install_resources.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/install_resources.ts @@ -163,6 +163,7 @@ export default function createAlertsAsDataInstallResourcesTest({ getService }: F rollover_alias: '.alerts-test.patternfiring.alerts-default', }, mapping: { + ignore_malformed: 'true', total_fields: { limit: '2500', }, @@ -196,6 +197,7 @@ export default function createAlertsAsDataInstallResourcesTest({ getService }: F }); expect(contextIndex[indexName].settings?.index?.mapping).to.eql({ + ignore_malformed: 'true', total_fields: { limit: '2500', }, diff --git a/x-pack/test/api_integration/apis/maps/fonts_api.js b/x-pack/test/api_integration/apis/maps/fonts_api.js index c8c636e8f6ddc..ae4891c4d7b95 100644 --- a/x-pack/test/api_integration/apis/maps/fonts_api.js +++ b/x-pack/test/api_integration/apis/maps/fonts_api.js @@ -6,7 +6,10 @@ */ import expect from '@kbn/expect'; -import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import path from 'path'; import { copyFile, rm } from 'fs/promises'; @@ -38,6 +41,7 @@ export default function ({ getService }) { const resp = await supertest .get(`/internal/maps/fonts/Open%20Sans%20Regular,Arial%20Unicode%20MS%20Regular/0-255`) .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .expect(200); expect(resp.body.length).to.be(74696); @@ -49,6 +53,7 @@ export default function ({ getService }) { `/internal/maps/fonts/Open%20Sans%20Regular,Arial%20Unicode%20MS%20Regular/noGonaFindMe` ) .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .expect(404); }); @@ -56,6 +61,7 @@ export default function ({ getService }) { await supertest .get(`/internal/maps/fonts/open_sans/..%2f0-255`) .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .expect(404); }); @@ -63,6 +69,7 @@ export default function ({ getService }) { await supertest .get(`/internal/maps/fonts/open_sans/.%2f..%2f0-255`) .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .expect(404); }); }); diff --git a/x-pack/test/api_integration/apis/maps/get_grid_tile.js b/x-pack/test/api_integration/apis/maps/get_grid_tile.js index 5b44b43e50e43..2a627e3793ee2 100644 --- a/x-pack/test/api_integration/apis/maps/get_grid_tile.js +++ b/x-pack/test/api_integration/apis/maps/get_grid_tile.js @@ -9,7 +9,10 @@ import { VectorTile } from '@mapbox/vector-tile'; import Protobuf from 'pbf'; import expect from '@kbn/expect'; import { getTileUrlParams } from '@kbn/maps-vector-tile-utils'; -import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; function findFeature(layer, callbackFn) { for (let i = 0; i < layer.length; i++) { @@ -75,6 +78,7 @@ export default function ({ getService }) { .get('/internal/maps/mvt/getGridTile/3/2/3.pbf?' + getTileUrlParams(defaultParams)) .set('kbn-xsrf', 'kibana') .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .responseType('blob') .expect(200); @@ -89,6 +93,7 @@ export default function ({ getService }) { .get('/internal/maps/mvt/getGridTile/3/2/3.pbf?' + getTileUrlParams(defaultParams)) .set('kbn-xsrf', 'kibana') .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .responseType('blob') .expect(200); @@ -120,6 +125,7 @@ export default function ({ getService }) { .get('/internal/maps/mvt/getGridTile/3/2/3.pbf?' + tileUrlParams) .set('kbn-xsrf', 'kibana') .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .responseType('blob') .expect(200); @@ -151,6 +157,7 @@ export default function ({ getService }) { .get('/internal/maps/mvt/getGridTile/3/2/3.pbf?' + tileUrlParams) .set('kbn-xsrf', 'kibana') .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .responseType('blob') .expect(200); @@ -189,6 +196,7 @@ export default function ({ getService }) { .get('/internal/maps/mvt/getGridTile/3/2/3.pbf?' + tileUrlParams) .set('kbn-xsrf', 'kibana') .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .responseType('blob') .expect(200); @@ -230,6 +238,7 @@ export default function ({ getService }) { .get('/internal/maps/mvt/getGridTile/3/2/3.pbf?' + tileUrlParams) .set('kbn-xsrf', 'kibana') .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .responseType('blob') .expect(200); @@ -258,6 +267,7 @@ export default function ({ getService }) { .get('/internal/maps/mvt/getGridTile/3/2/3.pbf?' + getTileUrlParams(defaultParams)) .set('kbn-xsrf', 'kibana') .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .responseType('blob') .expect(200); @@ -304,6 +314,7 @@ export default function ({ getService }) { .get('/internal/maps/mvt/getGridTile/3/2/3.pbf?' + tileUrlParams) .set('kbn-xsrf', 'kibana') .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .responseType('blob') .expect(404); }); diff --git a/x-pack/test/api_integration/apis/maps/get_tile.js b/x-pack/test/api_integration/apis/maps/get_tile.js index 8b466fe1554b5..4246141dd7c61 100644 --- a/x-pack/test/api_integration/apis/maps/get_tile.js +++ b/x-pack/test/api_integration/apis/maps/get_tile.js @@ -8,7 +8,10 @@ import { VectorTile } from '@mapbox/vector-tile'; import Protobuf from 'pbf'; import expect from '@kbn/expect'; -import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import { getTileUrlParams } from '@kbn/maps-vector-tile-utils'; function findFeature(layer, callbackFn) { @@ -75,6 +78,7 @@ export default function ({ getService }) { .get(`/internal/maps/mvt/getTile/2/1/1.pbf?${getTileUrlParams(defaultParams)}`) .set('kbn-xsrf', 'kibana') .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .responseType('blob') .expect(200); @@ -138,6 +142,7 @@ export default function ({ getService }) { .get(`/internal/maps/mvt/getTile/2/1/1.pbf?${tileUrlParams}`) .set('kbn-xsrf', 'kibana') .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .responseType('blob') .expect(200); @@ -182,6 +187,7 @@ export default function ({ getService }) { .get(`/internal/maps/mvt/getTile/2/1/1.pbf?${tileUrlParams}`) .set('kbn-xsrf', 'kibana') .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .responseType('blob') .expect(404); }); diff --git a/x-pack/test/api_integration/apis/metrics_ui/log_entry_highlights.ts b/x-pack/test/api_integration/apis/metrics_ui/log_entry_highlights.ts index fd87af88ebc8b..f1be3edf0f4ac 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/log_entry_highlights.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/log_entry_highlights.ts @@ -45,7 +45,8 @@ export default function ({ getService }: FtrProviderContext) { after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/simple_logs')); describe('/log_entries/highlights', () => { - describe('with the default source', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/163486 + describe.skip('with the default source', () => { before(() => kibanaServer.savedObjects.cleanStandardList()); after(() => kibanaServer.savedObjects.cleanStandardList()); diff --git a/x-pack/test/cases_api_integration/common/lib/api/user_profiles.ts b/x-pack/test/cases_api_integration/common/lib/api/user_profiles.ts index 25c7f7a7a9ba4..bc78feab4070c 100644 --- a/x-pack/test/cases_api_integration/common/lib/api/user_profiles.ts +++ b/x-pack/test/cases_api_integration/common/lib/api/user_profiles.ts @@ -10,8 +10,11 @@ import { parse as parseCookie, Cookie } from 'tough-cookie'; import { INTERNAL_SUGGEST_USER_PROFILES_URL } from '@kbn/cases-plugin/common/constants'; import { UserProfileService } from '@kbn/cases-plugin/server/services'; -import { UserProfileAvatarData } from '@kbn/security-plugin/common'; -import { UserProfile, UserProfileWithAvatar } from '@kbn/user-profile-components'; +import type { + UserProfile, + UserProfileAvatarData, + UserProfileWithAvatar, +} from '@kbn/user-profile-components'; import { SuggestUserProfilesRequest } from '@kbn/cases-plugin/common/types/api'; import { superUser } from '../authentication/users'; import { User } from '../authentication/types'; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/double.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/double.ts index 05d821b5932fb..39a878a82f896 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/double.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/double.ts @@ -33,8 +33,7 @@ export default ({ getService }: FtrProviderContext) => { const log = getService('log'); const es = getService('es'); - // Failing: See https://github.com/elastic/kibana/issues/155122 - describe.skip('Rule exception operators for data type double', () => { + describe('Rule exception operators for data type double', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/rule_exceptions/double'); await esArchiver.load('x-pack/test/functional/es_archives/rule_exceptions/double_as_string'); @@ -490,7 +489,6 @@ export default ({ getService }: FtrProviderContext) => { expect(hits).to.eql([]); }); }); - describe('working against string values in the data set', () => { it('will return 3 results if we have a list that includes 1 double', async () => { await importFile(supertest, log, 'double', ['1.0'], 'list_items.txt'); @@ -509,7 +507,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccess({ supertest, log, id }); - await waitForSignalsToBePresent(supertest, log, 1, [id]); + await waitForSignalsToBePresent(supertest, log, 3, [id]); const signalsOpen = await getSignalsById(supertest, log, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.double).sort(); expect(hits).to.eql(['1.1', '1.2', '1.3']); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts index be0d4876b59af..9450c32210009 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts @@ -66,8 +66,7 @@ export default ({ getService }: FtrProviderContext) => { rule_id: 'ml-rule-id', }; - // FLAKY: https://github.com/elastic/kibana/issues/145776 - describe.skip('Machine learning type rules', () => { + describe('Machine learning type rules', () => { before(async () => { // Order is critical here: auditbeat data must be loaded before attempting to start the ML job, // as the job looks for certain indices on start diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/non_ecs_fields.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/non_ecs_fields.ts index 32ae758b20807..2f2115333d5c2 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/non_ecs_fields.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/non_ecs_fields.ts @@ -56,7 +56,6 @@ export default ({ getService }: FtrProviderContext) => { }; }; - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/154277 describe('Non ECS fields in alert document source', () => { before(async () => { await esArchiver.load( @@ -259,7 +258,8 @@ export default ({ getService }: FtrProviderContext) => { // we don't validate it because geo_point is very complex type with many various representations: array, different object, string with few valid patterns // more on geo_point type https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-point.html - it('should fail creating alert when ECS field mapping is geo_point', async () => { + // since .alerts-* indices allow _ignore_malformed option, alert will be created for this document + it('should not fail creating alert when ECS field mapping is geo_point', async () => { const document = { client: { geo: { @@ -269,12 +269,11 @@ export default ({ getService }: FtrProviderContext) => { }, }; - const { errors } = await indexAndCreatePreviewAlert(document); + const { errors, alertSource } = await indexAndCreatePreviewAlert(document); - expect(errors[0]).toContain('Bulk Indexing of signals failed'); - expect(errors[0]).toContain( - 'failed to parse field [client.geo.location] of type [geo_point]' - ); + expect(errors).toEqual([]); + + expect(alertSource).toHaveProperty('client.geo.location', 'test test'); }); it('should strip invalid boolean values and left valid ones', async () => { diff --git a/x-pack/test/detection_engine_api_integration/utils/machine_learning_setup.ts b/x-pack/test/detection_engine_api_integration/utils/machine_learning_setup.ts index 84eeeea137cb0..47b870496642b 100644 --- a/x-pack/test/detection_engine_api_integration/utils/machine_learning_setup.ts +++ b/x-pack/test/detection_engine_api_integration/utils/machine_learning_setup.ts @@ -6,6 +6,7 @@ */ import type SuperTest from 'supertest'; +import { getCommonRequestHeader } from '../../functional/services/ml/common_api'; export const executeSetupModuleRequest = async ({ module, @@ -18,7 +19,7 @@ export const executeSetupModuleRequest = async ({ }) => { const { body } = await supertest .post(`/internal/ml/modules/setup/${module}`) - .set('kbn-xsrf', 'true') + .set(getCommonRequestHeader('1')) .send({ prefix: '', groups: ['auditbeat'], @@ -42,8 +43,8 @@ export const forceStartDatafeeds = async ({ supertest: SuperTest.SuperTest; }) => { const { body } = await supertest - .post(`/supertest/ml/jobs/force_start_datafeeds`) - .set('kbn-xsrf', 'true') + .post(`/internal/ml/jobs/force_start_datafeeds`) + .set(getCommonRequestHeader('1')) .send({ datafeedIds: [`datafeed-${jobId}`], start: new Date().getUTCMilliseconds(), diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts index 3744c2aa9d2f0..aaf31e54798db 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts @@ -437,61 +437,75 @@ const expectAssetsInstalled = ({ id: 'sample_dashboard', }); expect(resDashboard.id).equal('sample_dashboard'); + expect(resDashboard.managed).be(true); expect(resDashboard.references.map((ref: any) => ref.id).includes('sample_tag')).equal(true); const resDashboard2 = await kibanaServer.savedObjects.get({ type: 'dashboard', id: 'sample_dashboard2', }); expect(resDashboard2.id).equal('sample_dashboard2'); + expect(resDashboard2.managed).be(true); const resVis = await kibanaServer.savedObjects.get({ type: 'visualization', id: 'sample_visualization', }); + expect(resVis.id).equal('sample_visualization'); + expect(resVis.managed).be(true); const resSearch = await kibanaServer.savedObjects.get({ type: 'search', id: 'sample_search', }); expect(resSearch.id).equal('sample_search'); + expect(resSearch.managed).be(true); const resLens = await kibanaServer.savedObjects.get({ type: 'lens', id: 'sample_lens', }); + expect(resLens.id).equal('sample_lens'); + expect(resLens.managed).be(true); const resMlModule = await kibanaServer.savedObjects.get({ type: 'ml-module', id: 'sample_ml_module', }); expect(resMlModule.id).equal('sample_ml_module'); + expect(resMlModule.managed).be(true); const resSecurityRule = await kibanaServer.savedObjects.get({ type: 'security-rule', id: 'sample_security_rule', }); expect(resSecurityRule.id).equal('sample_security_rule'); + expect(resSecurityRule.managed).be(true); const resOsqueryPackAsset = await kibanaServer.savedObjects.get({ type: 'osquery-pack-asset', id: 'sample_osquery_pack_asset', }); expect(resOsqueryPackAsset.id).equal('sample_osquery_pack_asset'); + expect(resOsqueryPackAsset.managed).be(true); const resOsquerySavedObject = await kibanaServer.savedObjects.get({ type: 'osquery-saved-query', id: 'sample_osquery_saved_query', }); expect(resOsquerySavedObject.id).equal('sample_osquery_saved_query'); + expect(resOsquerySavedObject.managed).be(true); const resCloudSecurityPostureRuleTemplate = await kibanaServer.savedObjects.get({ type: 'csp-rule-template', id: 'sample_csp_rule_template', }); expect(resCloudSecurityPostureRuleTemplate.id).equal('sample_csp_rule_template'); + expect(resCloudSecurityPostureRuleTemplate.managed).be(true); const resTag = await kibanaServer.savedObjects.get({ type: 'tag', id: 'sample_tag', }); + expect(resTag.managed).be(true); expect(resTag.id).equal('sample_tag'); const resIndexPattern = await kibanaServer.savedObjects.get({ type: 'index-pattern', id: 'test-*', }); + expect(resIndexPattern.managed).be(true); expect(resIndexPattern.id).equal('test-*'); let resInvalidTypeIndexPattern; diff --git a/x-pack/test/fleet_api_integration/apis/outputs/crud.ts b/x-pack/test/fleet_api_integration/apis/outputs/crud.ts index c870e4bca0ab1..3cc860433eb69 100644 --- a/x-pack/test/fleet_api_integration/apis/outputs/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/outputs/crud.ts @@ -197,7 +197,7 @@ export default function (providerContext: FtrProviderContext) { .send({ name: 'My Kafka Output', type: 'kafka', - hosts: ['https://test.fr'], + hosts: ['test.fr:2000'], auth_type: 'user_pass', username: 'user', password: 'pass', @@ -565,7 +565,7 @@ export default function (providerContext: FtrProviderContext) { .send({ name: 'My Kafka Output', type: 'kafka', - hosts: ['https://test.fr'], + hosts: ['test.fr:2000'], auth_type: 'user_pass', username: 'user', password: 'pass', @@ -579,15 +579,14 @@ export default function (providerContext: FtrProviderContext) { is_default_monitoring: false, name: 'My Kafka Output', type: 'kafka', - hosts: ['https://test.fr'], + hosts: ['test.fr:2000'], auth_type: 'user_pass', username: 'user', password: 'pass', topics: [{ topic: 'topic1' }], broker_timeout: 10, - broker_ack_reliability: 'Wait for local commit', - broker_buffer_size: 256, - client_id: 'Elastic Agent', + required_acks: 1, + client_id: 'Elastic', compression: 'gzip', compression_level: 4, sasl: { @@ -606,7 +605,7 @@ export default function (providerContext: FtrProviderContext) { .send({ name: 'Default Kafka Output', type: 'kafka', - hosts: ['https://test.fr'], + hosts: ['test.fr:2000'], auth_type: 'user_pass', username: 'user', password: 'pass', @@ -619,7 +618,7 @@ export default function (providerContext: FtrProviderContext) { expect(itemWithoutId).to.eql({ name: 'Default Kafka Output', type: 'kafka', - hosts: ['https://test.fr'], + hosts: ['test.fr:2000'], auth_type: 'user_pass', username: 'user', password: 'pass', @@ -627,9 +626,8 @@ export default function (providerContext: FtrProviderContext) { is_default: true, is_default_monitoring: false, broker_timeout: 10, - broker_ack_reliability: 'Wait for local commit', - broker_buffer_size: 256, - client_id: 'Elastic Agent', + required_acks: 1, + client_id: 'Elastic', compression: 'gzip', compression_level: 4, sasl: { @@ -847,7 +845,7 @@ export default function (providerContext: FtrProviderContext) { .send({ name: 'Kafka Output', type: 'kafka', - hosts: ['https://test.fr'], + hosts: ['test.fr:2000'], auth_type: 'user_pass', username: 'user', password: 'pass', @@ -960,7 +958,7 @@ export default function (providerContext: FtrProviderContext) { const kafkaOutputPayload = { name: 'Output to delete test', type: 'kafka', - hosts: ['https://test.fr'], + hosts: ['test.fr:2000'], auth_type: 'user_pass', username: 'user', password: 'pass', diff --git a/x-pack/test/fleet_api_integration/apis/policy_secrets.ts b/x-pack/test/fleet_api_integration/apis/policy_secrets.ts index 34f20e88b0a81..52b614f389ba9 100644 --- a/x-pack/test/fleet_api_integration/apis/policy_secrets.ts +++ b/x-pack/test/fleet_api_integration/apis/policy_secrets.ts @@ -41,37 +41,43 @@ function createdPolicyToUpdatePolicy(policy: any) { return updatedPolicy; } +const SECRETS_INDEX_NAME = '.fleet-secrets'; export default function (providerContext: FtrProviderContext) { - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/162732 - describe.skip('fleet policy secrets', () => { + describe('fleet policy secrets', () => { const { getService } = providerContext; const es: Client = getService('es'); const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); - const getPackagePolicyById = async (id: string) => { - const { body } = await supertest.get(`/api/fleet/package_policies/${id}`); - return body.item; + const getSecrets = async (ids?: string[]) => { + const query = ids ? { terms: { _id: ids } } : { match_all: {} }; + return es.search({ + index: SECRETS_INDEX_NAME, + body: { + query, + }, + }); }; - const maybeCreateSecretsIndex = async () => { - // create mock .secrets index for testing - if (await es.indices.exists({ index: '.fleet-test-secrets' })) { - await es.indices.delete({ index: '.fleet-test-secrets' }); - } - await es.indices.create({ - index: '.fleet-test-secrets', - body: { - mappings: { - properties: { - value: { - type: 'keyword', - }, + const deleteAllSecrets = async () => { + try { + await es.deleteByQuery({ + index: SECRETS_INDEX_NAME, + body: { + query: { + match_all: {}, }, }, - }, - }); + }); + } catch (err) { + // index doesnt exis + } + }; + + const getPackagePolicyById = async (id: string) => { + const { body } = await supertest.get(`/api/fleet/package_policies/${id}`); + return body.item; }; const getFullAgentPolicyById = async (id: string) => { @@ -137,10 +143,8 @@ export default function (providerContext: FtrProviderContext) { let agentPolicyId: string; before(async () => { await kibanaServer.savedObjects.cleanStandardList(); - await getService('esArchiver').load( - 'x-pack/test/functional/es_archives/fleet/empty_fleet_server' - ); - await maybeCreateSecretsIndex(); + + await deleteAllSecrets(); }); setupFleetAndAgents(providerContext); @@ -261,16 +265,7 @@ export default function (providerContext: FtrProviderContext) { }); it('should have correctly created the secrets', async () => { - const searchRes = await es.search({ - index: '.fleet-test-secrets', - body: { - query: { - ids: { - values: [packageVarId, inputVarId, streamVarId], - }, - }, - }, - }); + const searchRes = await getSecrets([packageVarId, inputVarId, streamVarId]); expect(searchRes.hits.hits.length).to.eql(3); @@ -337,14 +332,7 @@ export default function (providerContext: FtrProviderContext) { }); it('should have correctly deleted unused secrets after update', async () => { - const searchRes = await es.search({ - index: '.fleet-test-secrets', - body: { - query: { - match_all: {}, - }, - }, - }); + const searchRes = await getSecrets(); expect(searchRes.hits.hits.length).to.eql(3); // should have created 1 and deleted 1 doc @@ -374,14 +362,7 @@ export default function (providerContext: FtrProviderContext) { expectCompiledPolicyVars(policyDoc, updatedPackageVarId); - const searchRes = await es.search({ - index: '.fleet-test-secrets', - body: { - query: { - match_all: {}, - }, - }, - }); + const searchRes = await getSecrets(); expect(searchRes.hits.hits.length).to.eql(3); @@ -413,53 +394,36 @@ export default function (providerContext: FtrProviderContext) { updatedPackagePolicy.vars.package_var_secret.value.id, updatedPackageVarId, ]; - - const searchRes = await es.search({ - index: '.fleet-test-secrets', - body: { - query: { - terms: { - _id: packageVarSecretIds, - }, - }, - }, - }); + const searchRes = await getSecrets(packageVarSecretIds); expect(searchRes.hits.hits.length).to.eql(2); }); it('should not delete used secrets on package policy delete', async () => { - return supertest + await supertest .delete(`/api/fleet/package_policies/${duplicatedPackagePolicyId}`) .set('kbn-xsrf', 'xxxx') .expect(200); - const searchRes = await es.search({ - index: '.fleet-test-secrets', - body: { - query: { - match_all: {}, - }, - }, - }); + // sleep to allow for secrets to be deleted + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const searchRes = await getSecrets(); + // should have deleted new_package_secret_val_2 expect(searchRes.hits.hits.length).to.eql(3); }); it('should delete all secrets on package policy delete', async () => { - return supertest + await supertest .delete(`/api/fleet/package_policies/${createdPackagePolicyId}`) .set('kbn-xsrf', 'xxxx') .expect(200); - const searchRes = await es.search({ - index: '.fleet-test-secrets', - body: { - query: { - match_all: {}, - }, - }, - }); + // sleep to allow for secrets to be deleted + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const searchRes = await getSecrets(); expect(searchRes.hits.hits.length).to.eql(0); }); diff --git a/x-pack/test/fleet_api_integration/config.base.ts b/x-pack/test/fleet_api_integration/config.base.ts index e5746278a26f9..3e4b35988efba 100644 --- a/x-pack/test/fleet_api_integration/config.base.ts +++ b/x-pack/test/fleet_api_integration/config.base.ts @@ -74,7 +74,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { 'secretsStorage', 'agentTamperProtectionEnabled', ])}`, - `--xpack.fleet.developer.testSecretsIndex=.fleet-test-secrets`, `--logging.loggers=${JSON.stringify([ ...getKibanaCliLoggers(xPackAPITestsConfig.get('kbnTestServer.serverArgs')), diff --git a/x-pack/test/functional/apps/discover/async_scripted_fields.js b/x-pack/test/functional/apps/discover/async_scripted_fields.js index 9a9d5e0d450f2..0d48f42c5ba1e 100644 --- a/x-pack/test/functional/apps/discover/async_scripted_fields.js +++ b/x-pack/test/functional/apps/discover/async_scripted_fields.js @@ -15,9 +15,17 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const log = getService('log'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'settings', 'discover', 'timePicker']); + const PageObjects = getPageObjects([ + 'common', + 'settings', + 'discover', + 'timePicker', + 'header', + 'dashboard', + ]); const queryBar = getService('queryBar'); const security = getService('security'); + const dashboardAddPanel = getService('dashboardAddPanel'); describe('async search with scripted fields', function () { this.tags(['skipFirefox']); @@ -43,7 +51,7 @@ export default function ({ getService, getPageObjects }) { await security.testUser.restoreDefaults(); }); - it('query should show failed shards pop up', async function () { + it('query should show failed shards callout', async function () { if (false) { /* If you had to modify the scripted fields, you could un-comment all this, run it, use es_archiver to update 'kibana_scripted_fields_on_logstash' */ @@ -69,12 +77,39 @@ export default function ({ getService, getPageObjects }) { await retry.tryForTime(20000, async function () { // wait for shards failed message - const shardMessage = await testSubjects.getVisibleText('euiToastHeader'); + const shardMessage = await testSubjects.getVisibleText( + 'dscNoResultsInterceptedWarningsCallout_warningTitle' + ); log.debug(shardMessage); expect(shardMessage).to.be('1 of 3 shards failed'); }); }); + it('query should show failed shards badge on dashboard', async function () { + await security.testUser.setRoles([ + 'test_logstash_reader', + 'global_discover_all', + 'global_dashboard_all', + ]); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.selectIndexPattern('logsta*'); + + await PageObjects.discover.saveSearch('search with warning'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + + await dashboardAddPanel.addSavedSearch('search with warning'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await retry.tryForTime(20000, async function () { + // wait for shards failed message + await testSubjects.existOrFail('savedSearchEmbeddableWarningsCallout_trigger'); + }); + }); + it('query return results with valid scripted field', async function () { if (false) { /* the skipped steps below were used to create the scripted fields in the logstash-* index pattern diff --git a/x-pack/test/functional/apps/infra/hosts_view.ts b/x-pack/test/functional/apps/infra/hosts_view.ts index f5dc470587c37..11d0a9675573b 100644 --- a/x-pack/test/functional/apps/infra/hosts_view.ts +++ b/x-pack/test/functional/apps/infra/hosts_view.ts @@ -155,8 +155,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { return !!currentUrl.match(path); }); - // Failing: See https://github.com/elastic/kibana/issues/162672 - describe.skip('Hosts View', function () { + const waitForPageToLoad = async () => + await retry.waitFor( + 'wait for table and KPI charts to load', + async () => + (await pageObjects.infraHostsView.isHostTableLoading()) && + (await pageObjects.infraHostsView.isKPIChartsLoaded()) + ); + + describe('Hosts View', function () { before(async () => { await Promise.all([ esArchiver.load('x-pack/test/functional/es_archives/infra/alerts'), @@ -247,189 +254,243 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - describe('#Single host Flyout', () => { + describe('#Single Host Flyout', () => { before(async () => { await setHostViewEnabled(true); await pageObjects.common.navigateToApp(HOSTS_VIEW_PATH); await pageObjects.header.waitUntilLoadingHasFinished(); - await pageObjects.timePicker.setAbsoluteRange( - START_HOST_PROCESSES_DATE.format(timepickerFormat), - END_HOST_PROCESSES_DATE.format(timepickerFormat) - ); - await pageObjects.infraHostsView.clickTableOpenFlyoutButton(); }); - after(async () => { - await retry.try(async () => { - await pageObjects.infraHostsView.clickCloseFlyoutButton(); + describe('Tabs', () => { + before(async () => { + await pageObjects.timePicker.setAbsoluteRange( + START_HOST_PROCESSES_DATE.format(timepickerFormat), + END_HOST_PROCESSES_DATE.format(timepickerFormat) + ); + + await waitForPageToLoad(); + + await pageObjects.infraHostsView.clickTableOpenFlyoutButton(); }); - }); - describe('Overview Tab', () => { - before(async () => { - await pageObjects.infraHostsView.clickOverviewFlyoutTab(); + after(async () => { + await retry.try(async () => { + await pageObjects.infraHostsView.clickCloseFlyoutButton(); + }); }); - [ - { metric: 'cpuUsage', value: '13.9%' }, - { metric: 'normalizedLoad1m', value: '18.8%' }, - { metric: 'memoryUsage', value: '94.9%' }, - { metric: 'diskSpaceUsage', value: 'N/A' }, - ].forEach(({ metric, value }) => { - it(`${metric} tile should show ${value}`, async () => { - await retry.try(async () => { - const tileValue = await pageObjects.infraHostsView.getAssetDetailsKPITileValue( - metric - ); - expect(tileValue).to.eql(value); + describe('Overview Tab', () => { + before(async () => { + await pageObjects.infraHostsView.clickOverviewFlyoutTab(); + }); + + [ + { metric: 'cpuUsage', value: '13.9%' }, + { metric: 'normalizedLoad1m', value: '18.8%' }, + { metric: 'memoryUsage', value: '94.9%' }, + { metric: 'diskSpaceUsage', value: 'N/A' }, + ].forEach(({ metric, value }) => { + it(`${metric} tile should show ${value}`, async () => { + await retry.try(async () => { + const tileValue = await pageObjects.infraHostsView.getAssetDetailsKPITileValue( + metric + ); + expect(tileValue).to.eql(value); + }); }); }); - }); - it('should render 8 charts in the Metrics section', async () => { - const hosts = await pageObjects.infraHostsView.getAssetDetailsMetricsCharts(); - expect(hosts.length).to.equal(8); - }); + it('should render 8 charts in the Metrics section', async () => { + const hosts = await pageObjects.infraHostsView.getAssetDetailsMetricsCharts(); + expect(hosts.length).to.equal(8); + }); - it('should navigate to metadata tab', async () => { - await pageObjects.infraHostsView.clickShowAllMetadataOverviewTab(); - await pageObjects.header.waitUntilLoadingHasFinished(); - await pageObjects.infraHostsView.metadataTableExist(); - await pageObjects.infraHostsView.clickOverviewFlyoutTab(); - }); + it('should navigate to metadata tab', async () => { + await pageObjects.infraHostsView.clickShowAllMetadataOverviewTab(); + await pageObjects.header.waitUntilLoadingHasFinished(); + await pageObjects.infraHostsView.metadataTableExist(); + await pageObjects.infraHostsView.clickOverviewFlyoutTab(); + }); - it('should show alerts', async () => { - await pageObjects.header.waitUntilLoadingHasFinished(); - await pageObjects.infraHostsView.overviewAlertsTitleExist(); - }); + it('should show alerts', async () => { + await pageObjects.header.waitUntilLoadingHasFinished(); + await pageObjects.infraHostsView.overviewAlertsTitleExist(); + }); - it('should open alerts flyout', async () => { - await pageObjects.header.waitUntilLoadingHasFinished(); - await pageObjects.infraHostsView.clickOverviewOpenAlertsFlyout(); - // There are 2 flyouts open (asset details and alerts) - // so we need a stricter selector - // to be sure that we are closing the alerts flyout - const closeAlertFlyout = await find.byCssSelector( - '[aria-labelledby="flyoutRuleAddTitle"] > [data-test-subj="euiFlyoutCloseButton"]' - ); - await closeAlertFlyout.click(); - }); + it('should open alerts flyout', async () => { + await pageObjects.header.waitUntilLoadingHasFinished(); + await pageObjects.infraHostsView.clickOverviewOpenAlertsFlyout(); + // There are 2 flyouts open (asset details and alerts) + // so we need a stricter selector + // to be sure that we are closing the alerts flyout + const closeAlertFlyout = await find.byCssSelector( + '[aria-labelledby="flyoutRuleAddTitle"] > [data-test-subj="euiFlyoutCloseButton"]' + ); + await closeAlertFlyout.click(); + }); - it('should navigate to alerts', async () => { - await pageObjects.infraHostsView.clickOverviewLinkToAlerts(); - await pageObjects.header.waitUntilLoadingHasFinished(); - const url = parse(await browser.getCurrentUrl()); + it('should navigate to alerts', async () => { + await pageObjects.infraHostsView.clickOverviewLinkToAlerts(); + await pageObjects.header.waitUntilLoadingHasFinished(); + const url = parse(await browser.getCurrentUrl()); - const query = decodeURIComponent(url.query ?? ''); + const query = decodeURIComponent(url.query ?? ''); - const alertsQuery = - "_a=(kuery:'host.name:\"Jennys-MBP.fritz.box\"',rangeFrom:'2023-03-28T18:20:00.000Z',rangeTo:'2023-03-28T18:21:00.000Z',status:all)"; + const alertsQuery = + "_a=(kuery:'host.name:\"Jennys-MBP.fritz.box\"',rangeFrom:'2023-03-28T18:20:00.000Z',rangeTo:'2023-03-28T18:21:00.000Z',status:all)"; - expect(url.pathname).to.eql('/app/observability/alerts'); - expect(query).to.contain(alertsQuery); + expect(url.pathname).to.eql('/app/observability/alerts'); + expect(query).to.contain(alertsQuery); - await returnTo(HOSTS_VIEW_PATH); + await returnTo(HOSTS_VIEW_PATH); + }); }); - }); - describe('Metadata Tab', () => { - before(async () => { - await pageObjects.infraHostsView.clickMetadataFlyoutTab(); - }); + describe('Metadata Tab', () => { + before(async () => { + await pageObjects.infraHostsView.clickMetadataFlyoutTab(); + }); - it('should render metadata tab, add and remove filter', async () => { - await pageObjects.infraHostsView.metadataTableExist(); + it('should render metadata tab, add and remove filter', async () => { + await pageObjects.infraHostsView.metadataTableExist(); - // Add Pin - await pageObjects.infraHostsView.clickAddMetadataPin(); - expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(true); + // Add Pin + await pageObjects.infraHostsView.clickAddMetadataPin(); + expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(true); - // Persist pin after refresh - await browser.refresh(); - await retry.try(async () => { - await pageObjects.infraHome.waitForLoading(); - const removePinExist = await pageObjects.infraHostsView.getRemovePinExist(); - expect(removePinExist).to.be(true); - }); + // Persist pin after refresh + await browser.refresh(); + await retry.try(async () => { + await pageObjects.infraHome.waitForLoading(); + const removePinExist = await pageObjects.infraHostsView.getRemovePinExist(); + expect(removePinExist).to.be(true); + }); - // Remove Pin - await pageObjects.infraHostsView.clickRemoveMetadataPin(); - expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(false); + // Remove Pin + await pageObjects.infraHostsView.clickRemoveMetadataPin(); + expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(false); + + await pageObjects.infraHostsView.clickAddMetadataFilter(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Add Filter + const addedFilter = await pageObjects.infraHostsView.getAppliedFilter(); + expect(addedFilter).to.contain('host.architecture: arm64'); + const removeFilterExists = await pageObjects.infraHostsView.getRemoveFilterExist(); + expect(removeFilterExists).to.be(true); + + // Remove filter + await pageObjects.infraHostsView.clickRemoveMetadataFilter(); + await pageObjects.header.waitUntilLoadingHasFinished(); + const removeFilterShouldNotExist = + await pageObjects.infraHostsView.getRemoveFilterExist(); + expect(removeFilterShouldNotExist).to.be(false); + }); - await pageObjects.infraHostsView.clickAddMetadataFilter(); - await pageObjects.header.waitUntilLoadingHasFinished(); + it('should render metadata tab, pin and unpin table row', async () => { + // Add Pin + await pageObjects.infraHostsView.clickAddMetadataPin(); + expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(true); - // Add Filter - const addedFilter = await pageObjects.infraHostsView.getAppliedFilter(); - expect(addedFilter).to.contain('host.architecture: arm64'); - const removeFilterExists = await pageObjects.infraHostsView.getRemoveFilterExist(); - expect(removeFilterExists).to.be(true); + // Persist pin after refresh + await browser.refresh(); + await retry.try(async () => { + await pageObjects.infraHome.waitForLoading(); + const removePinExist = await pageObjects.infraHostsView.getRemovePinExist(); + expect(removePinExist).to.be(true); + }); - // Remove filter - await pageObjects.infraHostsView.clickRemoveMetadataFilter(); - await pageObjects.header.waitUntilLoadingHasFinished(); - const removeFilterShouldNotExist = - await pageObjects.infraHostsView.getRemoveFilterExist(); - expect(removeFilterShouldNotExist).to.be(false); + // Remove Pin + await pageObjects.infraHostsView.clickRemoveMetadataPin(); + expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(false); + }); }); - it('should render metadata tab, pin and unpin table row', async () => { - // Add Pin - await pageObjects.infraHostsView.clickAddMetadataPin(); - expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(true); - - // Persist pin after refresh - await browser.refresh(); - await retry.try(async () => { - await pageObjects.infraHome.waitForLoading(); - const removePinExist = await pageObjects.infraHostsView.getRemovePinExist(); - expect(removePinExist).to.be(true); + describe('Processes Tab', () => { + before(async () => { + await pageObjects.infraHostsView.clickProcessesFlyoutTab(); + }); + it('should render processes tab and with Total Value summary', async () => { + const processesTotalValue = + await pageObjects.infraHostsView.getProcessesTabContentTotalValue(); + const processValue = await processesTotalValue.getVisibleText(); + expect(processValue).to.eql('313'); }); - // Remove Pin - await pageObjects.infraHostsView.clickRemoveMetadataPin(); - expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(false); + it('should expand processes table row', async () => { + await pageObjects.infraHostsView.getProcessesTable(); + await pageObjects.infraHostsView.getProcessesTableBody(); + await pageObjects.infraHostsView.clickProcessesTableExpandButton(); + }); }); - }); - describe('Processes Tab', () => { - before(async () => { - await pageObjects.infraHostsView.clickProcessesFlyoutTab(); - }); - it('should render processes tab and with Total Value summary', async () => { - const processesTotalValue = - await pageObjects.infraHostsView.getProcessesTabContentTotalValue(); - const processValue = await processesTotalValue.getVisibleText(); - expect(processValue).to.eql('313'); + describe('Logs Tab', () => { + before(async () => { + await pageObjects.infraHostsView.clickLogsFlyoutTab(); + }); + it('should render logs tab', async () => { + await testSubjects.existOrFail('infraAssetDetailsLogsTabContent'); + }); }); - it('should expand processes table row', async () => { - await pageObjects.infraHostsView.getProcessesTable(); - await pageObjects.infraHostsView.getProcessesTableBody(); - await pageObjects.infraHostsView.clickProcessesTableExpandButton(); + describe('Flyout links', () => { + it('should navigate to APM services after click', async () => { + await pageObjects.infraHostsView.clickFlyoutApmServicesLink(); + const url = parse(await browser.getCurrentUrl()); + const query = decodeURIComponent(url.query ?? ''); + const kuery = 'kuery=host.hostname:"Jennys-MBP.fritz.box"'; + + expect(url.pathname).to.eql('/app/apm/services'); + expect(query).to.contain(kuery); + + await returnTo(HOSTS_VIEW_PATH); + }); }); }); - describe('Logs Tab', () => { + describe('Host with alerts', () => { before(async () => { - await pageObjects.infraHostsView.clickLogsFlyoutTab(); + await pageObjects.timePicker.setAbsoluteRange( + START_DATE.format(timepickerFormat), + END_DATE.format(timepickerFormat) + ); + await pageObjects.infraHostsView.clickHostCheckbox('demo-stack-mysql-01', '-'); + await pageObjects.infraHostsView.clickSelectedHostsButton(); + await pageObjects.infraHostsView.clickSelectedHostsAddFilterButton(); + + await waitForPageToLoad(); + + await pageObjects.infraHostsView.clickTableOpenFlyoutButton(); }); - it('should render logs tab', async () => { - await testSubjects.existOrFail('infraAssetDetailsLogsTabContent'); + + after(async () => { + await retry.try(async () => { + await pageObjects.infraHostsView.clickCloseFlyoutButton(); + }); }); - }); - describe('Flyout links', () => { - it('should navigate to APM services after click', async () => { - await pageObjects.infraHostsView.clickFlyoutApmServicesLink(); - const url = parse(await browser.getCurrentUrl()); - const query = decodeURIComponent(url.query ?? ''); - const kuery = 'kuery=host.hostname:"Jennys-MBP.fritz.box"'; + it('should render alerts count for a host inside a flyout', async () => { + await pageObjects.infraHostsView.clickOverviewFlyoutTab(); - expect(url.pathname).to.eql('/app/apm/services'); - expect(query).to.contain(kuery); + retry.tryForTime(30 * 1000, async () => { + await observability.components.alertSummaryWidget.getFullSizeComponentSelectorOrFail(); + }); - await returnTo(HOSTS_VIEW_PATH); + const activeAlertsCount = + await observability.components.alertSummaryWidget.getActiveAlertCount(); + const totalAlertsCount = + await observability.components.alertSummaryWidget.getTotalAlertCount(); + + expect(activeAlertsCount.trim()).to.equal('2'); + expect(totalAlertsCount.trim()).to.equal('3'); + }); + + it('should render "N/A" when processes summary is not available in flyout', async () => { + await pageObjects.infraHostsView.clickProcessesFlyoutTab(); + const processesTotalValue = + await pageObjects.infraHostsView.getProcessesTabContentTotalValue(); + const processValue = await processesTotalValue.getVisibleText(); + expect(processValue).to.eql('N/A'); }); }); }); @@ -444,12 +505,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { END_DATE.format(timepickerFormat) ); - await retry.waitFor( - 'wait for table and KPI charts to load', - async () => - (await pageObjects.infraHostsView.isHostTableLoading()) && - (await pageObjects.infraHostsView.isKPIChartsLoaded()) - ); + await waitForPageToLoad(); }); it('should render the correct page title', async () => { @@ -509,37 +565,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - it('should render alerts count for a host inside a flyout', async () => { - await pageObjects.infraHostsView.clickHostCheckbox('demo-stack-mysql-01', '-'); - await pageObjects.infraHostsView.clickSelectedHostsButton(); - await pageObjects.infraHostsView.clickSelectedHostsAddFilterButton(); - await pageObjects.infraHostsView.clickTableOpenFlyoutButton(); - - const activeAlertsCount = await pageObjects.infraHostsView.getActiveAlertsCountText(); - const totalAlertsCount = await pageObjects.infraHostsView.getTotalAlertsCountText(); - - expect(activeAlertsCount).to.equal('2 '); - expect(totalAlertsCount).to.equal('3'); - - const deleteFilterButton = await find.byCssSelector( - `[title="Delete host.name: demo-stack-mysql-01"]` - ); - await deleteFilterButton.click(); - - await pageObjects.infraHostsView.clickCloseFlyoutButton(); - }); - - it('should render "N/A" when processes summary is not available in flyout', async () => { - await pageObjects.infraHostsView.clickTableOpenFlyoutButton(); - await pageObjects.infraHostsView.clickProcessesFlyoutTab(); - const processesTotalValue = - await pageObjects.infraHostsView.getProcessesTabContentTotalValue(); - const processValue = await processesTotalValue.getVisibleText(); - expect(processValue).to.eql('N/A'); - await pageObjects.infraHostsView.clickCloseFlyoutButton(); - }); - - describe('KPI tiles', () => { + describe('KPIs', () => { [ { metric: 'hostsCount', value: '6' }, { metric: 'cpuUsage', value: '0.8%' }, @@ -673,12 +699,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { before(async () => { await browser.scrollTop(); await pageObjects.infraHostsView.submitQuery(query); - await retry.waitFor( - 'wait for table and KPI charts to load', - async () => - (await pageObjects.infraHostsView.isHostTableLoading()) && - (await pageObjects.infraHostsView.isKPIChartsLoaded()) - ); + await await waitForPageToLoad(); }); after(async () => { diff --git a/x-pack/test/functional/apps/lens/group3/terms.ts b/x-pack/test/functional/apps/lens/group3/terms.ts index 3b93c39eb7a6b..f93df80d52589 100644 --- a/x-pack/test/functional/apps/lens/group3/terms.ts +++ b/x-pack/test/functional/apps/lens/group3/terms.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import moment from 'moment'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -15,6 +16,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const comboBox = getService('comboBox'); const find = getService('find'); const retry = getService('retry'); + const es = getService('es'); + const indexPatterns = getService('indexPatterns'); + const log = getService('log'); describe('lens terms', () => { describe('lens multi terms suite', () => { @@ -127,11 +131,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { // Can not use testSubjects because data-test-subj is placed range input and number input - const percentileInput = await find.byCssSelector( - `input[data-test-subj="lns-indexPattern-percentile-input"][type='number']` + const percentileInput = await PageObjects.lens.getNumericFieldReady( + 'lns-indexPattern-percentile-input' ); - await percentileInput.click(); - await percentileInput.clearValue(); await percentileInput.type('60'); const percentileValue = await percentileInput.getAttribute('value'); @@ -152,5 +154,150 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(data!.bars![0].bars[0].y).to.eql(19265); }); }); + + describe('Enable Other group', () => { + const esIndexPrefix = 'terms-empty-string-index'; + before(async () => { + log.info(`Creating index ${esIndexPrefix} with mappings`); + await es.indices.create({ + index: esIndexPrefix, + mappings: { + properties: { + '@timestamp': { + type: 'date', + }, + a: { + type: 'keyword', + }, + b: { + type: 'keyword', + }, + }, + }, + }); + + log.info(`Adding 100 documents to ${esIndexPrefix} at Sep 20, 2015 @ 06:31:44.000`); + const timestamp = moment + .utc('Sep 20, 2015 @ 06:31:44.000', 'MMM D, YYYY [@] HH:mm:ss.SSS') + .format(); + + await es.bulk({ + index: esIndexPrefix, + body: Array<{ + a: string; + b: string; + '@timestamp': string; + }>(100) + .fill({ a: '', b: '', '@timestamp': timestamp }) + .map((template, i) => { + return { + ...template, + a: i > 50 ? `${(i % 5) + 1}` : '', // generate 5 values for the index + empty string + b: i < 50 ? `${(i % 5) + 1}` : '', // generate 5 values for the index + empty string + }; + }) + .map((d) => `{"index": {}}\n${JSON.stringify(d)}\n`), + }); + + log.info(`Creating dataView ${esIndexPrefix}`); + await indexPatterns.create( + { + title: esIndexPrefix, + timeFieldName: '@timestamp', + }, + { override: true } + ); + }); + + after(async () => { + await es.indices.delete({ + index: esIndexPrefix, + }); + }); + it('should work with empty string values as buckets', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await elasticChart.setNewChartUiDebugFlag(true); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchDataPanelIndexPattern(esIndexPrefix); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'count', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'a', + }); + + await PageObjects.lens.waitForVisualization('xyVisChart'); + const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart'); + const seriesBar = data!.bars![0].bars; + expect(seriesBar[0].x).to.eql('(empty)'); + expect(seriesBar[seriesBar.length - 1].x).to.eql('Other'); + }); + + it('should work with empty string as breakdown', async () => { + await PageObjects.lens.removeDimension('lnsXY_xDimensionPanel'); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'a', + }); + + await PageObjects.lens.waitForVisualization('xyVisChart'); + const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart'); + expect(data!.bars![0].name).to.eql('(empty)'); + expect(data!.bars![data!.bars!.length - 1].name).to.eql('Other'); + }); + + it('should work with nested empty string values', async () => { + await PageObjects.lens.switchToVisualization('lnsDatatable'); + + await PageObjects.lens.removeLayer(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsDatatable_rows > lns-empty-dimension', + operation: 'terms', + field: 'a', + keepOpen: true, + }); + await PageObjects.lens.setTermsNumberOfValues(4); + await PageObjects.lens.closeDimensionEditor(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsDatatable_rows > lns-empty-dimension', + operation: 'terms', + field: 'b', + keepOpen: true, + }); + await PageObjects.lens.setTermsNumberOfValues(1); + await PageObjects.lens.closeDimensionEditor(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsDatatable_metrics > lns-empty-dimension', + operation: 'count', + }); + await PageObjects.lens.waitForVisualization(); + await PageObjects.common.sleep(20000); + // a empty value + expect(await PageObjects.lens.getDatatableCellText(1, 0)).to.eql('(empty)'); + // b Other value + expect(await PageObjects.lens.getDatatableCellText(1, 1)).to.eql('Other'); + // a Other value + expect(await PageObjects.lens.getDatatableCellText(5, 0)).to.eql('Other'); + // b empty value + expect(await PageObjects.lens.getDatatableCellText(5, 1)).to.eql('(empty)'); + }); + }); }); } diff --git a/x-pack/test/functional/apps/lens/group4/dashboard.ts b/x-pack/test/functional/apps/lens/group4/dashboard.ts index daa8a750ad1c8..e94f935323235 100644 --- a/x-pack/test/functional/apps/lens/group4/dashboard.ts +++ b/x-pack/test/functional/apps/lens/group4/dashboard.ts @@ -107,9 +107,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { // show the tooltip actions await rightClickInChart(30, 5); // hardcoded position of bar, depends heavy on data and charts implementation - await (await find.byCssSelector('.echTooltipActions__action')).click(); + await (await find.allByCssSelector('.echTooltipActions__action'))[1].click(); const hasIpFilter = await filterBar.hasFilter('ip', '97.220.3.248'); expect(hasIpFilter).to.be(true); + await rightClickInChart(35, 5); // hardcoded position of bar, depends heavy on data and charts implementation + await (await find.allByCssSelector('.echTooltipActions__action'))[0].click(); + const time = await PageObjects.timePicker.getTimeConfig(); + expect(time.start).to.equal('Sep 21, 2015 @ 09:00:00.000'); + expect(time.end).to.equal('Sep 21, 2015 @ 12:00:00.000'); }); }); diff --git a/x-pack/test/functional/apps/lens/group4/tsdb.ts b/x-pack/test/functional/apps/lens/group4/tsdb.ts index 7f6045b6f7cf4..3200c7a073dc4 100644 --- a/x-pack/test/functional/apps/lens/group4/tsdb.ts +++ b/x-pack/test/functional/apps/lens/group4/tsdb.ts @@ -248,6 +248,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const elasticChart = getService('elasticChart'); const indexPatterns = getService('indexPatterns'); const esArchiver = getService('esArchiver'); + const comboBox = getService('comboBox'); const createDocs = async ( esIndex: string, @@ -545,6 +546,49 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); } } + + describe('show time series dimension groups within breakdown', () => { + it('should show the time series dimension group on field picker when configuring a breakdown', async () => { + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'min', + field: 'bytes_counter', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', + operation: 'terms', + keepOpen: true, + }); + + const list = await comboBox.getOptionsList('indexPattern-dimension-field'); + expect(list).to.contain('Time series dimensions'); + await PageObjects.lens.closeDimensionEditor(); + }); + + it("should not show the time series dimension group on field picker if it's not a breakdown", async () => { + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'min', + field: 'bytes_counter', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + keepOpen: true, + }); + const list = await comboBox.getOptionsList('indexPattern-dimension-field'); + expect(list).to.not.contain('Time series dimensions'); + await PageObjects.lens.closeDimensionEditor(); + }); + }); }); describe('Scenarios with changing stream type', () => { diff --git a/x-pack/test/functional/page_objects/infra_hosts_view.ts b/x-pack/test/functional/page_objects/infra_hosts_view.ts index b56b57843cd3b..25e4b0302a763 100644 --- a/x-pack/test/functional/page_objects/infra_hosts_view.ts +++ b/x-pack/test/functional/page_objects/infra_hosts_view.ts @@ -12,14 +12,6 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); return { - async clickTryHostViewLink() { - return await testSubjects.click('inventory-hostsView-link'); - }, - - async clickTryHostViewBadge() { - return await testSubjects.click('inventory-hostsView-link-badge'); - }, - async clickTableOpenFlyoutButton() { return testSubjects.click('hostsView-flyout-button'); }, @@ -40,32 +32,47 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { return testSubjects.click('euiFlyoutCloseButton'); }, + async getBetaBadgeExists() { + return testSubjects.exists('infra-beta-badge'); + }, + + // Inventory UI + async clickTryHostViewLink() { + return await testSubjects.click('inventory-hostsView-link'); + }, + + async clickTryHostViewBadge() { + return await testSubjects.click('inventory-hostsView-link-badge'); + }, + + // Asset Details Flyout + async clickOverviewFlyoutTab() { - return testSubjects.click('hostsView-flyout-tabs-overview'); + return testSubjects.click('infraAssetDetailsOverviewTab'); }, async clickMetadataFlyoutTab() { - return testSubjects.click('hostsView-flyout-tabs-metadata'); + return testSubjects.click('infraAssetDetailsMetadataTab'); }, - async clickOverviewLinkToAlerts() { - return testSubjects.click('assetDetails-flyout-alerts-link'); + async clickProcessesFlyoutTab() { + return testSubjects.click('infraAssetDetailsProcessesTab'); }, - async clickOverviewOpenAlertsFlyout() { - return testSubjects.click('infraNodeContextPopoverCreateInventoryRuleButton'); + async clickLogsFlyoutTab() { + return testSubjects.click('infraAssetDetailsLogsTab'); }, - async clickProcessesFlyoutTab() { - return testSubjects.click('hostsView-flyout-tabs-processes'); + async clickOverviewLinkToAlerts() { + return testSubjects.click('infraAssetDetailsAlertsShowAllButton'); }, - async clickShowAllMetadataOverviewTab() { - return testSubjects.click('infraMetadataSummaryShowAllMetadataButton'); + async clickOverviewOpenAlertsFlyout() { + return testSubjects.click('infraAssetDetailsCreateAlertsRuleButton'); }, - async clickLogsFlyoutTab() { - return testSubjects.click('hostsView-flyout-tabs-logs'); + async clickShowAllMetadataOverviewTab() { + return testSubjects.click('infraAssetDetailsMetadataShowAllButton'); }, async clickProcessesTableExpandButton() { @@ -73,28 +80,26 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { }, async clickFlyoutApmServicesLink() { - return testSubjects.click('hostsView-flyout-apm-services-link'); + return testSubjects.click('infraAssetDetailsViewAPMServicesButton'); }, async clickAddMetadataPin() { - return testSubjects.click('infraMetadataEmbeddableAddPin'); + return testSubjects.click('infraAssetDetailsMetadataAddPin'); }, async clickRemoveMetadataPin() { - return testSubjects.click('infraMetadataEmbeddableRemovePin'); + return testSubjects.click('infraAssetDetailsMetadataRemovePin'); }, async clickAddMetadataFilter() { - return testSubjects.click('hostsView-flyout-metadata-add-filter'); + return testSubjects.click('infraAssetDetailsMetadataAddFilterButton'); }, async clickRemoveMetadataFilter() { - return testSubjects.click('hostsView-flyout-metadata-remove-filter'); + return testSubjects.click('infraAssetDetailsMetadataRemoveFilterButton'); }, - async getBetaBadgeExists() { - return testSubjects.exists('infra-beta-badge'); - }, + // Splash screen async getHostsLandingPageDisabled() { const container = await testSubjects.find('hostView-no-enable-access'); @@ -203,51 +208,29 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { return div.getAttribute('title'); }, - // Flyout Tabs + // Asset Details Flyout Tabs async getAssetDetailsKPITileValue(type: string) { - const container = await testSubjects.find('assetDetailsKPIGrid'); - const element = await container.findByTestSubject(`assetDetailsKPI-${type}`); + const container = await testSubjects.find('infraAssetDetailsKPIGrid'); + const element = await container.findByTestSubject(`infraAssetDetailsKPI${type}`); const div = await element.findByClassName('echMetricText__value'); return div.getAttribute('title'); }, overviewAlertsTitleExist() { - return testSubjects.exists('assetDetailsAlertsTitle'); - }, - - async getActiveAlertsCountText() { - const container = await testSubjects.find('activeAlertCount'); - const containerText = await container.getVisibleText(); - return containerText; - }, - - async getTotalAlertsCountText() { - const container = await testSubjects.find('totalAlertCount'); - const containerText = await container.getVisibleText(); - return containerText; + return testSubjects.exists('infraAssetDetailsAlertsTitle'); }, async getAssetDetailsMetricsCharts() { - const container = await testSubjects.find('assetDetailsMetricsChartGrid'); - return container.findAllByCssSelector('[data-test-subj*="assetDetailsMetricsChart"]'); - }, - - getMetadataTab() { - return testSubjects.find('hostsView-flyout-tabs-metadata'); + const container = await testSubjects.find('infraAssetDetailsMetricsChartGrid'); + return container.findAllByCssSelector('[data-test-subj*="infraAssetDetailsMetricsChart"]'); }, metadataTableExist() { - return testSubjects.exists('infraMetadataTable'); - }, - - async getMetadataTabName() { - const tabElement = await this.getMetadataTab(); - const tabTitle = await tabElement.findByClassName('euiTab__content'); - return tabTitle.getVisibleText(); + return testSubjects.exists('infraAssetDetailsMetadataTable'); }, async getRemovePinExist() { - return testSubjects.exists('infraMetadataEmbeddableRemovePin'); + return testSubjects.exists('infraAssetDetailsMetadataRemovePin'); }, async getAppliedFilter() { @@ -258,21 +241,25 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { }, async getRemoveFilterExist() { - return testSubjects.exists('hostsView-flyout-metadata-remove-filter'); + return testSubjects.exists('infraAssetDetailsMetadataRemoveFilterButton'); }, async getProcessesTabContentTitle(index: number) { - const processesListElements = await testSubjects.findAll('infraProcessesSummaryTableItem'); + const processesListElements = await testSubjects.findAll( + 'infraAssetDetailsProcessesSummaryTableItem' + ); return processesListElements[index].findByCssSelector('dt'); }, async getProcessesTabContentTotalValue() { - const processesListElements = await testSubjects.findAll('infraProcessesSummaryTableItem'); + const processesListElements = await testSubjects.findAll( + 'infraAssetDetailsProcessesSummaryTableItem' + ); return processesListElements[0].findByCssSelector('dd'); }, getProcessesTable() { - return testSubjects.find('infraProcessesTable'); + return testSubjects.find('infraAssetDetailsProcessesTable'); }, async getProcessesTableBody() { diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 7f95964e0e5d4..a46d90eb915e0 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -689,6 +689,20 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await this.selectOptionFromComboBox(`indexPattern-dimension-field-${lastIndex}`, field); }); }, + async getNumericFieldReady(testSubj: string) { + const numericInput = await find.byCssSelector( + `input[data-test-subj=${testSubj}][type='number']` + ); + await numericInput.click(); + await numericInput.clearValue(); + return numericInput; + }, + + async setTermsNumberOfValues(value: number) { + const valuesInput = await this.getNumericFieldReady('indexPattern-terms-values'); + await valuesInput.type(`${value}`); + await PageObjects.common.sleep(500); + }, async checkTermsAreNotAvailableToAgg(fields: string[]) { const lastIndex = ( diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts index ab4ed59336826..b44f62e71b9a0 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts @@ -724,6 +724,16 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await cases.api.deleteAllCases(); }); + it('initially renders user actions list correctly', async () => { + expect(testSubjects.missingOrFail('cases-show-more-user-actions')); + + const userActionsLists = await find.allByCssSelector( + '[data-test-subj="user-actions-list"]' + ); + + expect(userActionsLists).length(1); + }); + it('shows more actions on button click', async () => { await cases.api.generateUserActions({ caseId: createdCase.id, @@ -731,6 +741,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { totalUpdates: 4, }); + expect(testSubjects.missingOrFail('user-actions-loading')); + await header.waitUntilLoadingHasFinished(); await testSubjects.click('case-refresh'); diff --git a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts index 588ff9125a21b..e73bb06f0203a 100644 --- a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts +++ b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts @@ -431,8 +431,7 @@ export default function ({ getService }: FtrProviderContext) { }); }); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/163456 - describe.skip('API access with missing access token document or expired refresh token.', () => { + describe('API access with missing access token document or expired refresh token.', () => { let sessionCookie: Cookie; beforeEach(async () => { @@ -447,6 +446,9 @@ export default function ({ getService }: FtrProviderContext) { sessionCookie = parseCookie(cookies[0])!; checkCookieIsSet(sessionCookie); + // Let's make sure that created tokens are available for search. + await getService('es').indices.refresh({ index: '.security-tokens' }); + // Let's delete tokens from `.security-tokens` index directly to simulate the case when // Elasticsearch automatically removes access/refresh token document from the index after // some period of time. diff --git a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts index f1aef15c081f2..9ecdbd311a109 100644 --- a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts +++ b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts @@ -621,6 +621,9 @@ export default function ({ getService }: FtrProviderContext) { }); it('should properly set cookie and start new OIDC handshake', async function () { + // Let's make sure that created tokens are available for search. + await getService('es').indices.refresh({ index: '.security-tokens' }); + // Let's delete tokens from `.security-tokens` index directly to simulate the case when // Elasticsearch automatically removes access/refresh token document from the index // after some period of time. diff --git a/x-pack/test/security_api_integration/tests/saml/saml_login.ts b/x-pack/test/security_api_integration/tests/saml/saml_login.ts index fbb7aa25fa162..8358f24c5d028 100644 --- a/x-pack/test/security_api_integration/tests/saml/saml_login.ts +++ b/x-pack/test/security_api_integration/tests/saml/saml_login.ts @@ -593,8 +593,7 @@ export default function ({ getService }: FtrProviderContext) { }); }); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/163455 - describe.skip('API access with missing access token document.', () => { + describe('API access with missing access token document.', () => { let sessionCookie: Cookie; beforeEach(async () => { @@ -615,6 +614,9 @@ export default function ({ getService }: FtrProviderContext) { sessionCookie = parseCookie(samlAuthenticationResponse.headers['set-cookie'][0])!; + // Let's make sure that created tokens are available for search. + await getService('es').indices.refresh({ index: '.security-tokens' }); + // Let's delete tokens from `.security` index directly to simulate the case when // Elasticsearch automatically removes access/refresh token document from the index // after some period of time. @@ -700,6 +702,9 @@ export default function ({ getService }: FtrProviderContext) { [ 'when access token document is missing', async () => { + // Let's make sure that created tokens are available for search. + await getService('es').indices.refresh({ index: '.security-tokens' }); + const esResponse = await getService('es').deleteByQuery({ index: '.security-tokens', body: { query: { match: { doc_type: 'token' } } }, diff --git a/x-pack/test/security_api_integration/tests/token/session.ts b/x-pack/test/security_api_integration/tests/token/session.ts index f42d9cc93585f..3e8d44ce247cc 100644 --- a/x-pack/test/security_api_integration/tests/token/session.ts +++ b/x-pack/test/security_api_integration/tests/token/session.ts @@ -189,6 +189,9 @@ export default function ({ getService }: FtrProviderContext) { beforeEach(async () => (sessionCookie = await createSessionCookie())); it('should clear cookie and redirect to login', async function () { + // Let's make sure that created tokens are available for search. + await getService('es').indices.refresh({ index: '.security-tokens' }); + // Let's delete tokens from `.security` index directly to simulate the case when // Elasticsearch automatically removes access/refresh token document from the index // after some period of time. diff --git a/x-pack/test/security_solution_cypress/ccs_config.ts b/x-pack/test/security_solution_cypress/ccs_config.ts deleted file mode 100644 index 8e679c071ac0f..0000000000000 --- a/x-pack/test/security_solution_cypress/ccs_config.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; - -import { SecuritySolutionCypressCcsTestRunner } from './runner'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const securitySolutionCypressConfig = await readConfigFile(require.resolve('./config.ts')); - return { - ...securitySolutionCypressConfig.getAll(), - - testRunner: SecuritySolutionCypressCcsTestRunner, - }; -} diff --git a/x-pack/test/security_solution_cypress/config.firefox.ts b/x-pack/test/security_solution_cypress/config.firefox.ts deleted file mode 100644 index d7047c043443d..0000000000000 --- a/x-pack/test/security_solution_cypress/config.firefox.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; -import { CA_CERT_PATH } from '@kbn/dev-utils'; -import { SecuritySolutionConfigurableCypressTestRunner } from './runner'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const kibanaCommonTestsConfig = await readConfigFile( - require.resolve('../../../test/common/config.js') - ); - const xpackFunctionalTestsConfig = await readConfigFile( - require.resolve('../functional/config.base.js') - ); - - return { - ...kibanaCommonTestsConfig.getAll(), - - browser: { - type: 'firefox', - acceptInsecureCerts: true, - }, - - esTestCluster: { - ...xpackFunctionalTestsConfig.get('esTestCluster'), - serverArgs: [ - ...xpackFunctionalTestsConfig.get('esTestCluster.serverArgs'), - // define custom es server here - // API Keys is enabled at the top level - 'xpack.security.enabled=true', - ], - }, - - kbnTestServer: { - ...xpackFunctionalTestsConfig.get('kbnTestServer'), - serverArgs: [ - ...xpackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), - '--csp.strict=false', - // define custom kibana server args here - `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, - ], - }, - - testRunner: SecuritySolutionConfigurableCypressTestRunner, - }; -} diff --git a/x-pack/test/security_solution_cypress/runner.ts b/x-pack/test/security_solution_cypress/runner.ts index 3a82fc8e14a62..9514e63a12634 100644 --- a/x-pack/test/security_solution_cypress/runner.ts +++ b/x-pack/test/security_solution_cypress/runner.ts @@ -5,12 +5,8 @@ * 2.0. */ -import { resolve } from 'path'; import Url from 'url'; -import { withProcRunner } from '@kbn/dev-proc-runner'; -import semver from 'semver'; - import { FtrProviderContext } from '../common/ftr_provider_context'; export type { FtrProviderContext } from '../common/ftr_provider_context'; @@ -36,58 +32,3 @@ export async function SecuritySolutionConfigurableCypressTestRunner({ ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), }; } - -export async function SecuritySolutionCypressCcsTestRunner({ getService }: FtrProviderContext) { - const log = getService('log'); - - await withProcRunner(log, async (procs) => { - await procs.run('cypress', { - cmd: 'yarn', - args: ['cypress:run:ccs'], - cwd: resolve(__dirname, '../../plugins/security_solution'), - env: { - FORCE_COLOR: '1', - CYPRESS_BASE_URL: process.env.TEST_KIBANA_URL, - CYPRESS_ELASTICSEARCH_URL: process.env.TEST_ES_URL, - CYPRESS_ELASTICSEARCH_USERNAME: process.env.ELASTICSEARCH_USERNAME, - CYPRESS_ELASTICSEARCH_PASSWORD: process.env.ELASTICSEARCH_PASSWORD, - CYPRESS_CCS_KIBANA_URL: process.env.TEST_KIBANA_URLDATA, - CYPRESS_CCS_ELASTICSEARCH_URL: process.env.TEST_ES_URLDATA, - CYPRESS_CCS_REMOTE_NAME: process.env.TEST_CCS_REMOTE_NAME, - ...process.env, - }, - wait: true, - }); - }); -} - -export async function SecuritySolutionCypressUpgradeCliTestRunner({ - getService, -}: FtrProviderContext) { - const log = getService('log'); - let command = ''; - - if (semver.gt(process.env.ORIGINAL_VERSION!, '7.10.0')) { - command = 'cypress:run:upgrade'; - } else { - command = 'cypress:run:upgrade:old'; - } - - await withProcRunner(log, async (procs) => { - await procs.run('cypress', { - cmd: 'yarn', - args: [command], - cwd: resolve(__dirname, '../../plugins/security_solution'), - env: { - FORCE_COLOR: '1', - CYPRESS_BASE_URL: process.env.TEST_KIBANA_URL, - CYPRESS_ELASTICSEARCH_URL: process.env.TEST_ES_URL, - CYPRESS_ELASTICSEARCH_USERNAME: process.env.TEST_ES_USER, - CYPRESS_ELASTICSEARCH_PASSWORD: process.env.TEST_ES_PASS, - CYPRESS_ORIGINAL_VERSION: process.env.ORIGINAL_VERSION, - ...process.env, - }, - wait: true, - }); - }); -} diff --git a/x-pack/test/security_solution_cypress/upgrade_config.ts b/x-pack/test/security_solution_cypress/upgrade_config.ts deleted file mode 100644 index 221cf7b30e061..0000000000000 --- a/x-pack/test/security_solution_cypress/upgrade_config.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; - -import { SecuritySolutionCypressUpgradeCliTestRunner } from './runner'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const kibanaCommonTestsConfig = await readConfigFile( - require.resolve('../../../test/common/config.js') - ); - - return { - ...kibanaCommonTestsConfig.getAll(), - testRunner: SecuritySolutionCypressUpgradeCliTestRunner, - }; -} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/cases/find_cases.ts b/x-pack/test_serverless/api_integration/test_suites/observability/cases/find_cases.ts new file mode 100644 index 0000000000000..6f88c0ded2fdd --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/observability/cases/find_cases.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { + findCases, + createCase, + deleteAllCaseItems, + postCaseReq, + findCasesResp, +} from './helpers/api'; + +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('find_cases', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return empty response', async () => { + const cases = await findCases({ supertest }); + expect(cases).to.eql(findCasesResp); + }); + + it('should return cases', async () => { + const a = await createCase(supertest, postCaseReq); + const b = await createCase(supertest, postCaseReq); + const c = await createCase(supertest, postCaseReq); + + const cases = await findCases({ supertest }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 3, + cases: [a, b, c], + count_open_cases: 3, + }); + }); + + it('returns empty response when trying to find cases with owner as cases', async () => { + const cases = await findCases({ supertest, query: { owner: 'cases' } }); + expect(cases).to.eql(findCasesResp); + }); + + it('returns empty response when trying to find cases with owner as securitySolution', async () => { + const cases = await findCases({ supertest, query: { owner: 'securitySolution' } }); + expect(cases).to.eql(findCasesResp); + }); + }); +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/cases/get_case.ts b/x-pack/test_serverless/api_integration/test_suites/observability/cases/get_case.ts new file mode 100644 index 0000000000000..fffc529d2df30 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/observability/cases/get_case.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { + getCase, + createCase, + deleteCasesByESQuery, + getPostCaseRequest, + postCaseResp, +} from './helpers/api'; +import { removeServerGeneratedPropertiesFromCase } from './helpers/omit'; + +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('get_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should return a case', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const theCase = await getCase({ supertest, caseId: postedCase.id, includeComments: true }); + + const data = removeServerGeneratedPropertiesFromCase(theCase); + expect(data).to.eql(postCaseResp()); + expect(data.comments?.length).to.eql(0); + }); + }); +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/cases/helpers/api.ts b/x-pack/test_serverless/api_integration/test_suites/observability/cases/helpers/api.ts new file mode 100644 index 0000000000000..5f196ef3e3372 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/observability/cases/helpers/api.ts @@ -0,0 +1,247 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Client } from '@elastic/elasticsearch'; +import type SuperTest from 'supertest'; +import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server/src/saved_objects_index_pattern'; +import { CASES_URL } from '@kbn/cases-plugin/common'; +import { Case, CaseSeverity, CaseStatuses } from '@kbn/cases-plugin/common/types/domain'; +import type { CasePostRequest } from '@kbn/cases-plugin/common/types/api'; +import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain'; +import { CasesFindResponse } from '@kbn/cases-plugin/common/types/api'; + +export interface User { + username: string; + password: string; + description?: string; + roles: string[]; +} + +export const superUser: User = { + username: 'superuser', + password: 'superuser', + roles: ['superuser'], +}; + +export const setupAuth = ({ + apiCall, + headers, + auth, +}: { + apiCall: SuperTest.Test; + headers: Record; + auth?: { user: User; space: string | null } | null; +}): SuperTest.Test => { + if (!Object.hasOwn(headers, 'Cookie') && auth != null) { + return apiCall.auth(auth.user.username, auth.user.password); + } + + return apiCall; +}; + +export const getSpaceUrlPrefix = (spaceId: string | undefined | null) => { + return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; +}; + +export const deleteAllCaseItems = async (es: Client) => { + await Promise.all([ + deleteCasesByESQuery(es), + deleteCasesUserActions(es), + deleteComments(es), + deleteConfiguration(es), + deleteMappings(es), + ]); +}; + +export const deleteCasesUserActions = async (es: Client): Promise => { + await es.deleteByQuery({ + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + q: 'type:cases-user-actions', + wait_for_completion: true, + refresh: true, + body: {}, + conflicts: 'proceed', + }); +}; + +export const deleteCasesByESQuery = async (es: Client): Promise => { + await es.deleteByQuery({ + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + q: 'type:cases', + wait_for_completion: true, + refresh: true, + body: {}, + conflicts: 'proceed', + }); +}; + +export const deleteComments = async (es: Client): Promise => { + await es.deleteByQuery({ + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + q: 'type:cases-comments', + wait_for_completion: true, + refresh: true, + body: {}, + conflicts: 'proceed', + }); +}; + +export const deleteConfiguration = async (es: Client): Promise => { + await es.deleteByQuery({ + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + q: 'type:cases-configure', + wait_for_completion: true, + refresh: true, + body: {}, + conflicts: 'proceed', + }); +}; + +export const deleteMappings = async (es: Client): Promise => { + await es.deleteByQuery({ + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + q: 'type:cases-connector-mappings', + wait_for_completion: true, + refresh: true, + body: {}, + conflicts: 'proceed', + }); +}; + +export const defaultUser = { email: null, full_name: null, username: 'elastic' }; +/** + * A null filled user will occur when the security plugin is disabled + */ +export const nullUser = { email: null, full_name: null, username: null }; + +export const postCaseReq: CasePostRequest = { + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Observability Issue', + tags: ['defacement'], + severity: CaseSeverity.LOW, + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + settings: { + syncAlerts: true, + }, + owner: 'observability', + assignees: [], +}; + +/** + * Return a request for creating a case. + */ +export const getPostCaseRequest = (req?: Partial): CasePostRequest => ({ + ...postCaseReq, + ...req, +}); + +export const postCaseResp = ( + id?: string | null, + req: CasePostRequest = postCaseReq +): Partial => ({ + ...req, + ...(id != null ? { id } : {}), + comments: [], + duration: null, + severity: req.severity ?? CaseSeverity.LOW, + totalAlerts: 0, + totalComment: 0, + closed_by: null, + created_by: defaultUser, + external_service: null, + status: CaseStatuses.open, + updated_by: null, + category: null, +}); + +const findCommon = { + page: 1, + per_page: 20, + total: 0, + count_open_cases: 0, + count_closed_cases: 0, + count_in_progress_cases: 0, +}; + +export const findCasesResp: CasesFindResponse = { + ...findCommon, + cases: [], +}; + +export const createCase = async ( + supertest: SuperTest.SuperTest, + params: CasePostRequest, + expectedHttpCode: number = 200, + auth: { user: User; space: string | null } | null = { user: superUser, space: null }, + headers: Record = {} +): Promise => { + const apiCall = supertest.post(`${CASES_URL}`); + + setupAuth({ apiCall, headers, auth }); + + const response = await apiCall + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo') + .set(headers) + .send(params) + .expect(expectedHttpCode); + + return response.body; +}; + +export const findCases = async ({ + supertest, + query = {}, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest; + query?: Record; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { + const { body: res } = await supertest + .get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/_find`) + .auth(auth.user.username, auth.user.password) + .query({ sortOrder: 'asc', ...query }) + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo') + .send() + .expect(expectedHttpCode); + + return res; +}; + +export const getCase = async ({ + supertest, + caseId, + includeComments = false, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest; + caseId: string; + includeComments?: boolean; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { + const { body: theCase } = await supertest + .get( + `${getSpaceUrlPrefix(auth?.space)}${CASES_URL}/${caseId}?includeComments=${includeComments}` + ) + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo') + .auth(auth.user.username, auth.user.password) + .expect(expectedHttpCode); + + return theCase; +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/cases/helpers/omit.ts b/x-pack/test_serverless/api_integration/test_suites/observability/cases/helpers/omit.ts new file mode 100644 index 0000000000000..b25506bfaebea --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/observability/cases/helpers/omit.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Case, Attachment } from '@kbn/cases-plugin/common/types/domain'; +import { omit } from 'lodash'; + +interface CommonSavedObjectAttributes { + id?: string | null; + created_at?: string | null; + updated_at?: string | null; + version?: string | null; + [key: string]: unknown; +} + +const savedObjectCommonAttributes = ['created_at', 'updated_at', 'version', 'id']; + +export const removeServerGeneratedPropertiesFromObject = ( + object: T, + keys: K[] +): Omit => { + return omit(object, keys); +}; +export const removeServerGeneratedPropertiesFromSavedObject = < + T extends CommonSavedObjectAttributes +>( + attributes: T, + keys: Array = [] +): Omit => { + return removeServerGeneratedPropertiesFromObject(attributes, [ + ...savedObjectCommonAttributes, + ...keys, + ]); +}; + +export const removeServerGeneratedPropertiesFromCase = (theCase: Case): Partial => { + return removeServerGeneratedPropertiesFromSavedObject(theCase, ['closed_at']); +}; + +export const removeServerGeneratedPropertiesFromComments = ( + comments: Attachment[] | undefined +): Array> | undefined => { + return comments?.map((comment) => { + return removeServerGeneratedPropertiesFromSavedObject(comment, []); + }); +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/cases/post_case.ts b/x-pack/test_serverless/api_integration/test_suites/observability/cases/post_case.ts new file mode 100644 index 0000000000000..2bb37149c007f --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/observability/cases/post_case.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { deleteCasesByESQuery, createCase, getPostCaseRequest } from './helpers/api'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + + describe('post_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should create a case', async () => { + expect( + await createCase( + supertest, + getPostCaseRequest({ + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + }), + 200 + ) + ); + }); + + it('should throw 403 when create a case with securitySolution as owner', async () => { + expect( + await createCase( + supertest, + getPostCaseRequest({ + owner: 'securitySolution', + }), + 403 + ) + ); + }); + + it('should throw 403 when create a case with cases as owner', async () => { + expect( + await createCase( + supertest, + getPostCaseRequest({ + owner: 'cases', + }), + 403 + ) + ); + }); + }); +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts index b235be792ed42..443c9366d751b 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts @@ -17,5 +17,8 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./threshold_rule/documents_count_fired')); loadTestFile(require.resolve('./threshold_rule/custom_eq_avg_bytes_fired')); loadTestFile(require.resolve('./threshold_rule/group_by_fired')); + loadTestFile(require.resolve('./cases/post_case')); + loadTestFile(require.resolve('./cases/find_cases')); + loadTestFile(require.resolve('./cases/get_case')); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/search/cases/find_cases.ts b/x-pack/test_serverless/api_integration/test_suites/search/cases/find_cases.ts new file mode 100644 index 0000000000000..b847833b30b46 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/search/cases/find_cases.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CASES_URL } from '@kbn/cases-plugin/common/constants'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + + describe('find_cases', () => { + it('403 when calling find cases API', async () => { + await supertest + .get(`${CASES_URL}/_find`) + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo') + .expect(403); + }); + }); +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/search/cases/post_case.ts b/x-pack/test_serverless/api_integration/test_suites/search/cases/post_case.ts new file mode 100644 index 0000000000000..4391fa29e0831 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/search/cases/post_case.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CASES_URL } from '@kbn/cases-plugin/common/constants'; +import { CaseSeverity } from '@kbn/cases-plugin/common/types/domain'; +import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + + describe('post_case', () => { + it('403 when trying to create case', async () => { + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo') + .send({ + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Observability Issue', + tags: ['defacement'], + severity: CaseSeverity.LOW, + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + settings: { + syncAlerts: true, + }, + owner: 'cases', + assignees: [], + }) + .expect(403); + }); + }); +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/search/index.ts b/x-pack/test_serverless/api_integration/test_suites/search/index.ts index 20c04f741b1ac..13ddf80d5a950 100644 --- a/x-pack/test_serverless/api_integration/test_suites/search/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/search/index.ts @@ -10,5 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('serverless search API', function () { loadTestFile(require.resolve('./snapshot_telemetry')); + loadTestFile(require.resolve('./cases/post_case')); + loadTestFile(require.resolve('./cases/find_cases')); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cases/find_cases.ts b/x-pack/test_serverless/api_integration/test_suites/security/cases/find_cases.ts new file mode 100644 index 0000000000000..07283119aea2f --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/security/cases/find_cases.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +import { + findCases, + createCase, + deleteAllCaseItems, + findCasesResp, + postCaseReq, +} from './helpers/api'; + +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('find_cases', () => { + describe('basic tests', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return empty response', async () => { + const cases = await findCases({ supertest }); + expect(cases).to.eql(findCasesResp); + }); + + it('should return cases', async () => { + const a = await createCase(supertest, postCaseReq); + const b = await createCase(supertest, postCaseReq); + const c = await createCase(supertest, postCaseReq); + + const cases = await findCases({ supertest }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 3, + cases: [a, b, c], + count_open_cases: 3, + }); + }); + + it('returns empty response when trying to find cases with owner as cases', async () => { + const cases = await findCases({ supertest, query: { owner: 'cases' } }); + expect(cases).to.eql(findCasesResp); + }); + + it('returns empty response when trying to find cases with owner as observability', async () => { + const cases = await findCases({ supertest, query: { owner: 'observability' } }); + expect(cases).to.eql(findCasesResp); + }); + }); + }); +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cases/get_case.ts b/x-pack/test_serverless/api_integration/test_suites/security/cases/get_case.ts new file mode 100644 index 0000000000000..c8dd9ec867514 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/security/cases/get_case.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +import { + getCase, + createCase, + deleteCasesByESQuery, + getPostCaseRequest, + postCaseResp, +} from './helpers/api'; +import { removeServerGeneratedPropertiesFromCase } from './helpers/omit'; + +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('get_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should return a case', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const theCase = await getCase({ supertest, caseId: postedCase.id, includeComments: true }); + + const data = removeServerGeneratedPropertiesFromCase(theCase); + expect(data).to.eql(postCaseResp()); + expect(data.comments?.length).to.eql(0); + }); + }); +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cases/helpers/api.ts b/x-pack/test_serverless/api_integration/test_suites/security/cases/helpers/api.ts new file mode 100644 index 0000000000000..afba9c46c67c2 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/security/cases/helpers/api.ts @@ -0,0 +1,247 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Client } from '@elastic/elasticsearch'; +import type SuperTest from 'supertest'; +import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server/src/saved_objects_index_pattern'; +import { CASES_URL } from '@kbn/cases-plugin/common'; +import { Case, CaseSeverity, CaseStatuses } from '@kbn/cases-plugin/common/types/domain'; +import type { CasePostRequest } from '@kbn/cases-plugin/common/types/api'; +import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain'; +import { CasesFindResponse } from '@kbn/cases-plugin/common/types/api'; + +export interface User { + username: string; + password: string; + description?: string; + roles: string[]; +} + +export const superUser: User = { + username: 'superuser', + password: 'superuser', + roles: ['superuser'], +}; + +export const setupAuth = ({ + apiCall, + headers, + auth, +}: { + apiCall: SuperTest.Test; + headers: Record; + auth?: { user: User; space: string | null } | null; +}): SuperTest.Test => { + if (!Object.hasOwn(headers, 'Cookie') && auth != null) { + return apiCall.auth(auth.user.username, auth.user.password); + } + + return apiCall; +}; + +export const getSpaceUrlPrefix = (spaceId: string | undefined | null) => { + return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; +}; + +export const deleteAllCaseItems = async (es: Client) => { + await Promise.all([ + deleteCasesByESQuery(es), + deleteCasesUserActions(es), + deleteComments(es), + deleteConfiguration(es), + deleteMappings(es), + ]); +}; + +export const deleteCasesUserActions = async (es: Client): Promise => { + await es.deleteByQuery({ + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + q: 'type:cases-user-actions', + wait_for_completion: true, + refresh: true, + body: {}, + conflicts: 'proceed', + }); +}; + +export const deleteCasesByESQuery = async (es: Client): Promise => { + await es.deleteByQuery({ + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + q: 'type:cases', + wait_for_completion: true, + refresh: true, + body: {}, + conflicts: 'proceed', + }); +}; + +export const deleteComments = async (es: Client): Promise => { + await es.deleteByQuery({ + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + q: 'type:cases-comments', + wait_for_completion: true, + refresh: true, + body: {}, + conflicts: 'proceed', + }); +}; + +export const deleteConfiguration = async (es: Client): Promise => { + await es.deleteByQuery({ + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + q: 'type:cases-configure', + wait_for_completion: true, + refresh: true, + body: {}, + conflicts: 'proceed', + }); +}; + +export const deleteMappings = async (es: Client): Promise => { + await es.deleteByQuery({ + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + q: 'type:cases-connector-mappings', + wait_for_completion: true, + refresh: true, + body: {}, + conflicts: 'proceed', + }); +}; + +export const defaultUser = { email: null, full_name: null, username: 'elastic' }; +/** + * A null filled user will occur when the security plugin is disabled + */ +export const nullUser = { email: null, full_name: null, username: null }; + +export const postCaseReq: CasePostRequest = { + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Observability Issue', + tags: ['defacement'], + severity: CaseSeverity.LOW, + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + settings: { + syncAlerts: true, + }, + owner: 'securitySolution', + assignees: [], +}; + +/** + * Return a request for creating a case. + */ +export const getPostCaseRequest = (req?: Partial): CasePostRequest => ({ + ...postCaseReq, + ...req, +}); + +export const postCaseResp = ( + id?: string | null, + req: CasePostRequest = postCaseReq +): Partial => ({ + ...req, + ...(id != null ? { id } : {}), + comments: [], + duration: null, + severity: req.severity ?? CaseSeverity.LOW, + totalAlerts: 0, + totalComment: 0, + closed_by: null, + created_by: defaultUser, + external_service: null, + status: CaseStatuses.open, + updated_by: null, + category: null, +}); + +const findCommon = { + page: 1, + per_page: 20, + total: 0, + count_open_cases: 0, + count_closed_cases: 0, + count_in_progress_cases: 0, +}; + +export const findCasesResp: CasesFindResponse = { + ...findCommon, + cases: [], +}; + +export const createCase = async ( + supertest: SuperTest.SuperTest, + params: CasePostRequest, + expectedHttpCode: number = 200, + auth: { user: User; space: string | null } | null = { user: superUser, space: null }, + headers: Record = {} +): Promise => { + const apiCall = supertest.post(`${CASES_URL}`); + + setupAuth({ apiCall, headers, auth }); + + const { body: theCase } = await apiCall + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo') + .set(headers) + .send(params) + .expect(expectedHttpCode); + + return theCase; +}; + +export const findCases = async ({ + supertest, + query = {}, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest; + query?: Record; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { + const { body: res } = await supertest + .get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/_find`) + .auth(auth.user.username, auth.user.password) + .query({ sortOrder: 'asc', ...query }) + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo') + .send() + .expect(expectedHttpCode); + + return res; +}; + +export const getCase = async ({ + supertest, + caseId, + includeComments = false, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest; + caseId: string; + includeComments?: boolean; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { + const { body: theCase } = await supertest + .get( + `${getSpaceUrlPrefix(auth?.space)}${CASES_URL}/${caseId}?includeComments=${includeComments}` + ) + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo') + .auth(auth.user.username, auth.user.password) + .expect(expectedHttpCode); + + return theCase; +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cases/helpers/omit.ts b/x-pack/test_serverless/api_integration/test_suites/security/cases/helpers/omit.ts new file mode 100644 index 0000000000000..b25506bfaebea --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/security/cases/helpers/omit.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Case, Attachment } from '@kbn/cases-plugin/common/types/domain'; +import { omit } from 'lodash'; + +interface CommonSavedObjectAttributes { + id?: string | null; + created_at?: string | null; + updated_at?: string | null; + version?: string | null; + [key: string]: unknown; +} + +const savedObjectCommonAttributes = ['created_at', 'updated_at', 'version', 'id']; + +export const removeServerGeneratedPropertiesFromObject = ( + object: T, + keys: K[] +): Omit => { + return omit(object, keys); +}; +export const removeServerGeneratedPropertiesFromSavedObject = < + T extends CommonSavedObjectAttributes +>( + attributes: T, + keys: Array = [] +): Omit => { + return removeServerGeneratedPropertiesFromObject(attributes, [ + ...savedObjectCommonAttributes, + ...keys, + ]); +}; + +export const removeServerGeneratedPropertiesFromCase = (theCase: Case): Partial => { + return removeServerGeneratedPropertiesFromSavedObject(theCase, ['closed_at']); +}; + +export const removeServerGeneratedPropertiesFromComments = ( + comments: Attachment[] | undefined +): Array> | undefined => { + return comments?.map((comment) => { + return removeServerGeneratedPropertiesFromSavedObject(comment, []); + }); +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cases/post_case.ts b/x-pack/test_serverless/api_integration/test_suites/security/cases/post_case.ts new file mode 100644 index 0000000000000..1d49545c3d53a --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/security/cases/post_case.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { deleteCasesByESQuery, createCase, getPostCaseRequest, postCaseResp } from './helpers/api'; +import { removeServerGeneratedPropertiesFromCase } from './helpers/omit'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + + describe('post_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should create a case', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + }) + ); + const data = removeServerGeneratedPropertiesFromCase(postedCase); + + expect(data).to.eql( + postCaseResp( + null, + getPostCaseRequest({ + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + }) + ) + ); + }); + + it('should throw 403 when trying to create a case with observability as owner', async () => { + expect( + await createCase( + supertest, + getPostCaseRequest({ + owner: 'observability', + }), + 403 + ) + ); + }); + + it('should throw 403 when trying to create a case with cases as owner', async () => { + expect( + await createCase( + supertest, + getPostCaseRequest({ + owner: 'cases', + }), + 403 + ) + ); + }); + }); +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/index.ts b/x-pack/test_serverless/api_integration/test_suites/security/index.ts index a57d4446e4b9b..294f4b32af5e6 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/index.ts @@ -11,5 +11,8 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('serverless security API', function () { loadTestFile(require.resolve('./fleet')); loadTestFile(require.resolve('./snapshot_telemetry')); + loadTestFile(require.resolve('./cases/post_case')); + loadTestFile(require.resolve('./cases/find_cases')); + loadTestFile(require.resolve('./cases/get_case')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/observability/navigation.ts b/x-pack/test_serverless/functional/test_suites/observability/navigation.ts index 6c90a2a410b61..2054e8eb011f6 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/navigation.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/navigation.ts @@ -86,5 +86,22 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await expect(await browser.getCurrentUrl()).contain('/app/discover#/p/log-explorer'); }); + + it('shows cases in sidebar navigation', async () => { + await svlCommonNavigation.expectExists(); + + await svlCommonNavigation.sidenav.expectLinkExists({ + deepLinkId: 'observability-overview:cases', + }); + }); + + it('navigates to cases app', async () => { + await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'observability-overview:cases' }); + + await svlCommonNavigation.sidenav.expectLinkActive({ + deepLinkId: 'observability-overview:cases', + }); + expect(await browser.getCurrentUrl()).contain('/app/observability/cases'); + }); }); } diff --git a/x-pack/test_serverless/functional/test_suites/search/navigation.ts b/x-pack/test_serverless/functional/test_suites/search/navigation.ts index edf4c41274403..ba19e7b756073 100644 --- a/x-pack/test_serverless/functional/test_suites/search/navigation.ts +++ b/x-pack/test_serverless/functional/test_suites/search/navigation.ts @@ -74,5 +74,18 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await expect(await browser.getCurrentUrl()).contain('/app/discover'); }); + + it('does not show cases in sidebar navigation', async () => { + await svlSearchLandingPage.assertSvlSearchSideNavExists(); + + expect(await testSubjects.missingOrFail('cases')); + }); + + it('does not navigate to cases app', async () => { + await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'discover' }); + + expect(await browser.getCurrentUrl()).not.contain('/app/management/cases'); + await testSubjects.missingOrFail('cases-all-title'); + }); }); } diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/navigation.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/navigation.ts index 1ea8ed53c0ca4..695336b7734fb 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/navigation.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/navigation.ts @@ -43,5 +43,19 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await expect(await browser.getCurrentUrl()).contain('app/security/dashboards'); }); + + it('shows cases in sidebar navigation', async () => { + await svlSecLandingPage.assertSvlSecSideNavExists(); + await svlCommonNavigation.expectExists(); + + expect(await testSubjects.existOrFail('solutionSideNavItemLink-cases')); + }); + + it('navigates to cases app', async () => { + await testSubjects.click('solutionSideNavItemLink-cases'); + + expect(await browser.getCurrentUrl()).contain('/app/security/cases'); + await testSubjects.existOrFail('cases-all-title'); + }); }); } diff --git a/x-pack/test_serverless/tsconfig.json b/x-pack/test_serverless/tsconfig.json index 8c431b2537986..eb69a37884039 100644 --- a/x-pack/test_serverless/tsconfig.json +++ b/x-pack/test_serverless/tsconfig.json @@ -46,5 +46,6 @@ "@kbn/test-subj-selector", "@kbn/core-http-common", "@kbn/data-views-plugin", + "@kbn/core-saved-objects-server", ] } diff --git a/yarn.lock b/yarn.lock index 952c808afb066..a7e1003917f3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,21 @@ # yarn lockfile v1 +"@actions/core@^1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.10.0.tgz#44551c3c71163949a2f06e94d9ca2157a0cfac4f" + integrity sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug== + dependencies: + "@actions/http-client" "^2.0.1" + uuid "^8.3.2" + +"@actions/http-client@^2.0.1": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.1.0.tgz#b6d8c3934727d6a50d10d19f00a711a964599a9f" + integrity sha512-BonhODnXr3amchh4qkmjPMUO8mFi/zLaaCeCAJZqch8iQqyDnVIkySjB38VHAC8IJ+bnlgfOqlhpyCUZHlQsqw== + dependencies: + tunnel "^0.0.6" + "@adobe/css-tools@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.0.1.tgz#b38b444ad3aa5fedbb15f2f746dcd934226a12dd" @@ -398,6 +413,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.2.tgz#dacafadfc6d7654c3051a66d6fe55b6cb2f2a0b3" integrity sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ== +"@babel/parser@^7.21.8": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.5.tgz#721fd042f3ce1896238cf1b341c77eb7dee7dbea" + integrity sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -1302,6 +1322,13 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@csstools/selector-specificity@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.1.tgz#b6b8d81780b9a9f6459f4bfe9226ac6aefaefe87" @@ -1395,6 +1422,14 @@ enabled "2.0.x" kuler "^2.0.0" +"@dependents/detective-less@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@dependents/detective-less/-/detective-less-4.1.0.tgz#4a979ee7a6a79eb33602862d6a1263e30f98002e" + integrity sha512-KrkT6qO5NxqNfy68sBl6CTSoJ4SNDIS5iQArkibhlbGU4LaDukZ3q2HIkh8aUKDio6o4itU4xDR7t82Y2eP1Bg== + dependencies: + gonzales-pe "^4.3.0" + node-source-walk "^6.0.1" + "@discoveryjs/json-ext@^0.5.0", "@discoveryjs/json-ext@^0.5.3": version "0.5.5" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz#9283c9ce5b289a3c4f61c12757469e59377f81f3" @@ -2792,7 +2827,7 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@3.1.0": +"@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== @@ -2815,6 +2850,14 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": version "0.3.17" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" @@ -5177,6 +5220,10 @@ version "0.0.0" uid "" +"@kbn/search-response-warnings@link:packages/kbn-search-response-warnings": + version "0.0.0" + uid "" + "@kbn/searchprofiler-plugin@link:x-pack/plugins/searchprofiler": version "0.0.0" uid "" @@ -8088,6 +8135,26 @@ mkdirp "^1.0.4" path-browserify "^1.0.1" +"@tsconfig/node10@^1.0.7": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" + integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== + +"@tsconfig/node12@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" + integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== + +"@tsconfig/node14@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" + integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== + +"@tsconfig/node16@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" + integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== + "@tsd/typescript@~4.6.3": version "4.6.3" resolved "https://registry.yarnpkg.com/@tsd/typescript/-/typescript-4.6.3.tgz#9b4c8198da7614fe1547436fbd5657cfe8327c1d" @@ -9892,6 +9959,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.54.0.tgz#7d519df01f50739254d89378e0dcac504cab2740" integrity sha512-nExy+fDCBEgqblasfeE3aQ3NuafBUxZxgxXcYfzYRZFHdVvk5q60KhCSkG0noHgHRo/xQ/BOzURLZAafFpTkmQ== +"@typescript-eslint/types@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== + "@typescript-eslint/typescript-estree@5.54.0", "@typescript-eslint/typescript-estree@^5.54.0": version "5.54.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.54.0.tgz#f6f3440cabee8a43a0b25fa498213ebb61fdfe99" @@ -9905,6 +9977,19 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@^5.59.5": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + "@typescript-eslint/utils@5.54.0", "@typescript-eslint/utils@^5.10.0": version "5.54.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.54.0.tgz#3db758aae078be7b54b8ea8ea4537ff6cd3fbc21" @@ -9927,6 +10012,14 @@ "@typescript-eslint/types" "5.54.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== + dependencies: + "@typescript-eslint/types" "5.62.0" + eslint-visitor-keys "^3.3.0" + "@wdio/logger@^8.6.6": version "8.6.6" resolved "https://registry.yarnpkg.com/@wdio/logger/-/logger-8.6.6.tgz#6f3844a2506730ae1e4151dca0ed0242b5b69b63" @@ -10314,10 +10407,10 @@ acorn-globals@^7.0.0: acorn "^8.1.0" acorn-walk "^8.0.2" -acorn-import-assertions@^1.7.6: - version "1.8.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" - integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== +acorn-import-assertions@^1.7.6, acorn-import-assertions@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== acorn-jsx@^5.3.1: version "5.3.2" @@ -10338,7 +10431,7 @@ acorn-walk@^7.0.0, acorn-walk@^7.2.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.2.0: +acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== @@ -10358,10 +10451,15 @@ acorn@^7.0.0, acorn@^7.4.0, acorn@^7.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.0.4, acorn@^8.1.0, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0: - version "8.8.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" - integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== +acorn@^8.0.4, acorn@^8.1.0, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8.2: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + +acorn@^8.4.1: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== address@^1.0.1: version "1.1.2" @@ -10670,6 +10768,11 @@ apidoc-markdown@^7.2.4: update-notifier "^5.1.0" yargs "^17.6.0" +app-module-path@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/app-module-path/-/app-module-path-2.2.0.tgz#641aa55dfb7d6a6f0a8141c4b9c0aa50b6c24dd5" + integrity sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ== + app-root-dir@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/app-root-dir/-/app-root-dir-1.0.2.tgz#38187ec2dea7577fff033ffcb12172692ff6e118" @@ -10742,6 +10845,16 @@ are-we-there-yet@^3.0.0: delegates "^1.0.0" readable-stream "^3.6.0" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +arg@^5.0.1, arg@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -10986,6 +11099,11 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= +ast-module-types@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ast-module-types/-/ast-module-types-5.0.0.tgz#32b2b05c56067ff38e95df66f11d6afd6c9ba16b" + integrity sha512-JvqziE0Wc0rXQfma0HZC/aY7URXHFuZV84fJRtP8u+lhp0JYCNd5wJzVXP45t0PH0Mej3ynlzvdyITYIu0G4LQ== + ast-transform@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/ast-transform/-/ast-transform-0.0.0.tgz#74944058887d8283e189d954600947bc98fe0062" @@ -12432,14 +12550,14 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^114.0.2: - version "114.0.2" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-114.0.2.tgz#1ddaa6738f2b60e6b832a39f791c8c54bf840837" - integrity sha512-v0qrXRBknbxqmtklG7RWOe3TJ/dLaHhtB0jVxE7BAdYERxUjEaNRyqBwoGgVfQDibHCB0swzvzsj158nnfPgZw== +chromedriver@^115.0.1: + version "115.0.1" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-115.0.1.tgz#76cbf35f16e0c1f5e29ab821fb3b8b06d22c3e40" + integrity sha512-faE6WvIhXfhnoZ3nAxUXYzeDCKy612oPwpkUp0mVkA7fZPg2JHSUiYOQhUYgzHQgGvDWD5Fy2+M2xV55GKHBVQ== dependencies: "@testim/chrome-version" "^1.1.3" axios "^1.4.0" - compare-versions "^5.0.3" + compare-versions "^6.0.0" extract-zip "^2.0.1" https-proxy-agent "^5.0.1" proxy-from-env "^1.1.0" @@ -12475,6 +12593,11 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== +cjs-module-lexer@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" + integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== + clamp@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/clamp/-/clamp-1.0.1.tgz#66a0e64011816e37196828fdc8c8c147312c8634" @@ -12846,10 +12969,10 @@ compare-versions@3.5.1: resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.5.1.tgz#26e1f5cf0d48a77eced5046b9f67b6b61075a393" integrity sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg== -compare-versions@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-5.0.3.tgz#a9b34fea217472650ef4a2651d905f42c28ebfd7" - integrity sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A== +compare-versions@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a" + integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg== component-emitter@^1.2.0, component-emitter@^1.2.1: version "1.2.1" @@ -12969,6 +13092,13 @@ console-log-level@^1.4.1: resolved "https://registry.yarnpkg.com/console-log-level/-/console-log-level-1.4.1.tgz#9c5a6bb9ef1ef65b05aba83028b0ff894cdf630a" integrity sha512-VZzbIORbP+PPcN/gg3DXClTLPLg5Slwd5fL2MIc+o1qZ4BXBvWyc6QxPk6T/Mkr6IVjRpoAGf32XxP3ZWMVRcQ== +console.table@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/console.table/-/console.table-0.10.0.tgz#0917025588875befd70cf2eff4bef2c6e2d75d04" + integrity sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g== + dependencies: + easy-table "1.1.0" + constants-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" @@ -13213,6 +13343,11 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cronstrue@^1.51.0: version "1.51.0" resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-1.51.0.tgz#7a63153d61d940344049037628da38a60784c8e2" @@ -14251,6 +14386,16 @@ dependency-check@^4.1.0: read-package-json "^2.0.10" resolve "^1.1.7" +dependency-tree@^10.0.9: + version "10.0.9" + resolved "https://registry.yarnpkg.com/dependency-tree/-/dependency-tree-10.0.9.tgz#0c6c0dbeb0c5ec2cf83bf755f30e9cb12e7b4ac7" + integrity sha512-dwc59FRIsht+HfnTVM0BCjJaEWxdq2YAvEDy4/Hn6CwS3CBWMtFnL3aZGAkQn3XCYxk/YcTDE4jX2Q7bFTwCjA== + dependencies: + commander "^10.0.1" + filing-cabinet "^4.1.6" + precinct "^11.0.5" + typescript "^5.0.4" + deprecation@^2.0.0, deprecation@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" @@ -14321,6 +14466,71 @@ detect-port@^1.3.0: address "^1.0.1" debug "^2.6.0" +detective-amd@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/detective-amd/-/detective-amd-5.0.2.tgz#579900f301c160efe037a6377ec7e937434b2793" + integrity sha512-XFd/VEQ76HSpym80zxM68ieB77unNuoMwopU2TFT/ErUk5n4KvUTwW4beafAVUugrjV48l4BmmR0rh2MglBaiA== + dependencies: + ast-module-types "^5.0.0" + escodegen "^2.0.0" + get-amd-module-type "^5.0.1" + node-source-walk "^6.0.1" + +detective-cjs@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/detective-cjs/-/detective-cjs-5.0.1.tgz#836ad51c6de4863efc7c419ec243694f760ff8b2" + integrity sha512-6nTvAZtpomyz/2pmEmGX1sXNjaqgMplhQkskq2MLrar0ZAIkHMrDhLXkRiK2mvbu9wSWr0V5/IfiTrZqAQMrmQ== + dependencies: + ast-module-types "^5.0.0" + node-source-walk "^6.0.0" + +detective-es6@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/detective-es6/-/detective-es6-4.0.1.tgz#38d5d49a6d966e992ef8f2d9bffcfe861a58a88a" + integrity sha512-k3Z5tB4LQ8UVHkuMrFOlvb3GgFWdJ9NqAa2YLUU/jTaWJIm+JJnEh4PsMc+6dfT223Y8ACKOaC0qcj7diIhBKw== + dependencies: + node-source-walk "^6.0.1" + +detective-postcss@^6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/detective-postcss/-/detective-postcss-6.1.3.tgz#51a2d4419327ad85d0af071c7054c79fafca7e73" + integrity sha512-7BRVvE5pPEvk2ukUWNQ+H2XOq43xENWbH0LcdCE14mwgTBEAMoAx+Fc1rdp76SmyZ4Sp48HlV7VedUnP6GA1Tw== + dependencies: + is-url "^1.2.4" + postcss "^8.4.23" + postcss-values-parser "^6.0.2" + +detective-sass@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/detective-sass/-/detective-sass-5.0.3.tgz#63e54bc9b32f4bdbd9d5002308f9592a3d3a508f" + integrity sha512-YsYT2WuA8YIafp2RVF5CEfGhhyIVdPzlwQgxSjK+TUm3JoHP+Tcorbk3SfG0cNZ7D7+cYWa0ZBcvOaR0O8+LlA== + dependencies: + gonzales-pe "^4.3.0" + node-source-walk "^6.0.1" + +detective-scss@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/detective-scss/-/detective-scss-4.0.3.tgz#79758baa0158f72bfc4481eb7e21cc3b5f1ea6eb" + integrity sha512-VYI6cHcD0fLokwqqPFFtDQhhSnlFWvU614J42eY6G0s8c+MBhi9QAWycLwIOGxlmD8I/XvGSOUV1kIDhJ70ZPg== + dependencies: + gonzales-pe "^4.3.0" + node-source-walk "^6.0.1" + +detective-stylus@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detective-stylus/-/detective-stylus-4.0.0.tgz#ce97b6499becdc291de7b3c11df8c352c1eee46e" + integrity sha512-TfPotjhszKLgFBzBhTOxNHDsutIxx9GTWjrL5Wh7Qx/ydxKhwUrlSFeLIn+ZaHPF+h0siVBkAQSuy6CADyTxgQ== + +detective-typescript@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/detective-typescript/-/detective-typescript-11.1.0.tgz#2deea5364cae1f0d9d3688bc596e662b049438cc" + integrity sha512-Mq8egjnW2NSCkzEb/Az15/JnBI/Ryyl6Po0Y+0mABTFvOS6DAyUGRZqz1nyhu4QJmWWe0zaGs/ITIBeWkvCkGw== + dependencies: + "@typescript-eslint/typescript-estree" "^5.59.5" + ast-module-types "^5.0.0" + node-source-walk "^6.0.1" + typescript "^5.0.4" + detective@^5.0.2: version "5.2.0" resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b" @@ -14671,6 +14881,13 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +easy-table@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/easy-table/-/easy-table-1.1.0.tgz#86f9ab4c102f0371b7297b92a651d5824bc8cb73" + integrity sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA== + optionalDependencies: + wcwidth ">=1.0.1" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -14763,10 +14980,10 @@ elastic-apm-node@3.46.0: traverse "^0.6.6" unicode-byte-truncate "^1.0.0" -elastic-apm-node@^3.49.0: - version "3.49.0" - resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.49.0.tgz#89ae052fbd81787ef012fe0f304756d9249584ab" - integrity sha512-6EyysTNdJqBxd13ZyPHaew1IDdiQKvG29K/rvbYZPyNRL4T7Asi8yoVBs3/mfVWOjheLqoxeyPq2EL1lSO/dtg== +elastic-apm-node@^3.49.1: + version "3.49.1" + resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.49.1.tgz#c000936a1b7f062e4dd502cd3617ebe97d4d9786" + integrity sha512-k1kQ/exFqodZOoZSRJ3Csbdo7dtRs/uORBlRTyV2takYa1OIN7o9dvZwd8+eEPOUz4qaeRyVY8X9X2krk9GO/g== dependencies: "@elastic/ecs-pino-format" "^1.2.0" "@opentelemetry/api" "^1.4.1" @@ -14787,7 +15004,7 @@ elastic-apm-node@^3.49.0: fast-safe-stringify "^2.0.7" fast-stream-to-buffer "^1.0.0" http-headers "^3.0.2" - import-in-the-middle "1.3.5" + import-in-the-middle "1.4.2" is-native "^1.0.1" lru-cache "^6.0.0" measured-reporting "^1.51.1" @@ -14931,6 +15148,14 @@ enhanced-resolve@^5.10.0: graceful-fs "^4.2.4" tapable "^2.2.0" +enhanced-resolve@^5.14.1: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + enquirer@^2.3.5, enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" @@ -16234,6 +16459,24 @@ filelist@^1.0.1: dependencies: minimatch "^5.0.1" +filing-cabinet@^4.1.6: + version "4.1.6" + resolved "https://registry.yarnpkg.com/filing-cabinet/-/filing-cabinet-4.1.6.tgz#8d6d12cf3a84365bbd94e1cbf07d71c113420dd2" + integrity sha512-C+HZbuQTER36sKzGtUhrAPAoK6+/PrrUhYDBQEh3kBRdsyEhkLbp1ML8S0+6e6gCUrUlid+XmubxJrhvL2g/Zw== + dependencies: + app-module-path "^2.2.0" + commander "^10.0.1" + enhanced-resolve "^5.14.1" + is-relative-path "^1.0.2" + module-definition "^5.0.1" + module-lookup-amd "^8.0.5" + resolve "^1.22.3" + resolve-dependency-path "^3.0.2" + sass-lookup "^5.0.1" + stylus-lookup "^5.0.1" + tsconfig-paths "^4.2.0" + typescript "^5.0.4" + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -16282,11 +16525,41 @@ find-cache-dir@^3.2.0, find-cache-dir@^3.3.1: make-dir "^3.0.2" pkg-dir "^4.1.0" +find-cypress-specs@^1.35.1: + version "1.35.1" + resolved "https://registry.yarnpkg.com/find-cypress-specs/-/find-cypress-specs-1.35.1.tgz#89f633de14ab46c2afc6fee992a470596526f721" + integrity sha512-ngLPf/U/I8jAS6vn5ljClETa6seG+fmr3oXw6BcYX3xVIk7D8jNljHUIJCTDvnK90XOI1cflYGuNFDezhRBNvQ== + dependencies: + "@actions/core" "^1.10.0" + arg "^5.0.1" + console.table "^0.10.0" + debug "^4.3.3" + find-test-names "1.28.13" + globby "^11.1.0" + minimatch "^3.0.4" + pluralize "^8.0.0" + require-and-forget "^1.0.1" + shelljs "^0.8.5" + spec-change "^1.7.1" + ts-node "^10.9.1" + find-root@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== +find-test-names@1.28.13: + version "1.28.13" + resolved "https://registry.yarnpkg.com/find-test-names/-/find-test-names-1.28.13.tgz#871d5585d1f618ed772ffe544ea475ab4657ca83" + integrity sha512-hVatCLbiZmvBwqYNGTkVNbeJwK/8pvkXKQGji+23GzW8fVFHcEaRID77eQYItLKGwa1Tmu0AK2LjcUtuid57FQ== + dependencies: + "@babel/parser" "^7.21.2" + "@babel/plugin-syntax-jsx" "^7.18.6" + acorn-walk "^8.2.0" + debug "^4.3.3" + globby "^11.0.4" + simple-bin-help "^1.8.0" + find-test-names@^1.19.0: version "1.28.6" resolved "https://registry.yarnpkg.com/find-test-names/-/find-test-names-1.28.6.tgz#2840916b42815ce1286bfbfad04cf08f066f727e" @@ -16786,6 +17059,14 @@ geojson-vt@^3.2.1: resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-3.2.1.tgz#f8adb614d2c1d3f6ee7c4265cad4bbf3ad60c8b7" integrity sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg== +get-amd-module-type@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/get-amd-module-type/-/get-amd-module-type-5.0.1.tgz#bef38ea3674e1aa1bda9c59c8b0da598582f73f2" + integrity sha512-jb65zDeHyDjFR1loOVk0HQGM5WNwoGB8aLWy3LKCieMKol0/ProHkhO2X1JxojuN10vbz1qNn09MJ7tNp7qMzw== + dependencies: + ast-module-types "^5.0.0" + node-source-walk "^6.0.1" + get-assigned-identifiers@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz#6dbf411de648cbaf8d9169ebb0d2d576191e2ff1" @@ -16811,6 +17092,11 @@ get-nonce@^1.0.0: resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -16990,7 +17276,7 @@ glob@^10.2.2: minipass "^5.0.0 || ^6.0.2" path-scurry "^1.7.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0, glob@~7.2.0: +glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0, glob@^7.2.3, glob@~7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -17166,6 +17452,13 @@ globule@^1.0.0: lodash "~4.17.10" minimatch "~3.0.2" +gonzales-pe@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3" + integrity sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ== + dependencies: + minimist "^1.2.5" + google-protobuf@^3.6.1: version "3.19.4" resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.19.4.tgz#8d32c3e34be9250956f28c0fb90955d13f311888" @@ -18033,11 +18326,14 @@ import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" -import-in-the-middle@1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.3.5.tgz#78384fbcfc7c08faf2b1f61cb94e7dd25651df9c" - integrity sha512-yzHlBqi1EBFrkieAnSt8eTgO5oLSl+YJ7qaOpUH/PMqQOMZoQ/RmDlwnTLQrwYto+gHYjRG+i/IbsB1eDx32NQ== +import-in-the-middle@1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz#2a266676e3495e72c04bbaa5ec14756ba168391b" + integrity sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw== dependencies: + acorn "^8.8.2" + acorn-import-assertions "^1.9.0" + cjs-module-lexer "^1.2.2" module-details-from-path "^1.0.3" import-lazy@^2.1.0: @@ -18195,6 +18491,11 @@ internmap@^1.0.0: resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + interpret@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" @@ -18385,6 +18686,13 @@ is-ci@^3.0.0: dependencies: ci-info "^3.2.0" +is-core-module@^2.12.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" + integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== + dependencies: + has "^1.0.3" + is-core-module@^2.6.0, is-core-module@^2.9.0: version "2.10.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" @@ -18597,6 +18905,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + is-obj@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" @@ -18702,11 +19015,21 @@ is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.2, is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + is-regexp@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d" integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA== +is-relative-path@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-relative-path/-/is-relative-path-1.0.2.tgz#091b46a0d67c1ed0fe85f1f8cfdde006bb251d46" + integrity sha512-i1h+y50g+0hRbBD+dbnInl3JlJ702aar58snAeX+MxBAPvzXGej7sYoPMhlnykabt0ZzCJNBEyzMlekuQZN7fA== + is-relative@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" @@ -18779,7 +19102,12 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== -is-url@^1.2.2: +is-url-superb@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-url-superb/-/is-url-superb-4.0.0.tgz#b54d1d2499bb16792748ac967aa3ecb41a33a8c2" + integrity sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA== + +is-url@^1.2.2, is-url@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== @@ -20152,6 +20480,11 @@ lazy-ass@1.6.0, lazy-ass@^1.6.0: resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM= +lazy-ass@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-2.0.3.tgz#1e8451729f2bebdff1218bb18921566a08f81b36" + integrity sha512-/O3/DoQmI1XAhklDvF1dAjFf/epE8u3lzOZegQfLZ8G7Ud5bTRSZiFOpukHCu6jODrCA4gtIdwUCC7htxcDACA== + lazy-cache@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264" @@ -20779,6 +21112,11 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: dependencies: semver "^6.0.0" +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + make-fetch-happen@^10.0.4: version "10.2.1" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz#f5e3835c5e9817b617f2770870d9492d28678164" @@ -21703,11 +22041,29 @@ mock-fs@^5.1.2: resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.2.tgz#6fa486e06d00f8793a8d2228de980eff93ce6db7" integrity sha512-YkjQkdLulFrz0vD4BfNQdQRVmgycXTV7ykuHMlyv+C8WCHazpkiQRDthwa02kSyo8wKnY9wRptHfQLgmf0eR+A== +module-definition@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/module-definition/-/module-definition-5.0.1.tgz#62d1194e5d5ea6176b7dc7730f818f466aefa32f" + integrity sha512-kvw3B4G19IXk+BOXnYq/D/VeO9qfHaapMeuS7w7sNUqmGaA6hywdFHMi+VWeR9wUScXM7XjoryTffCZ5B0/8IA== + dependencies: + ast-module-types "^5.0.0" + node-source-walk "^6.0.1" + module-details-from-path@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" integrity sha1-EUyUlnPiqKNenTV4hSeqN7Z52is= +module-lookup-amd@^8.0.5: + version "8.0.5" + resolved "https://registry.yarnpkg.com/module-lookup-amd/-/module-lookup-amd-8.0.5.tgz#aaeea41979105b49339380ca3f7d573db78c32a5" + integrity sha512-vc3rYLjDo5Frjox8NZpiyLXsNWJ5BWshztc/5KSOMzpg9k5cHH652YsJ7VKKmtM4SvaxuE9RkrYGhiSjH3Ehow== + dependencies: + commander "^10.0.1" + glob "^7.2.3" + requirejs "^2.3.6" + requirejs-config-file "^4.0.0" + moment-duration-format@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/moment-duration-format/-/moment-duration-format-2.3.2.tgz#5fa2b19b941b8d277122ff3f87a12895ec0d6212" @@ -21906,6 +22262,11 @@ nanoid@^3.3.1, nanoid@^3.3.4: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +nanoid@^3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + nanomatch@^1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2" @@ -22225,6 +22586,13 @@ node-sass@^8.0.0: stdout-stream "^1.4.0" "true-case-path" "^2.2.1" +node-source-walk@^6.0.0, node-source-walk@^6.0.1, node-source-walk@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/node-source-walk/-/node-source-walk-6.0.2.tgz#ba81bc4bc0f6f05559b084bea10be84c3f87f211" + integrity sha512-jn9vOIK/nfqoFCcpK89/VCVaLg1IHE6UVfDOzvqmANaJ/rWCTEdH8RZ1V278nv2jr36BJdyQXIAavBLXpzdlag== + dependencies: + "@babel/parser" "^7.21.8" + nodemailer@^6.6.2: version "6.6.2" resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.6.2.tgz#e184c9ed5bee245a3e0bcabc7255866385757114" @@ -23408,6 +23776,11 @@ pluralize@3.1.0: resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-3.1.0.tgz#84213d0a12356069daa84060c559242633161368" integrity sha1-hCE9ChI1YGnaqEBgxVkkJjMWE2g= +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + png-js@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.0.0.tgz#e5484f1e8156996e383aceebb3789fd75df1874d" @@ -23748,6 +24121,15 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== +postcss-values-parser@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz#636edc5b86c953896f1bb0d7a7a6615df00fb76f" + integrity sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw== + dependencies: + color-name "^1.1.4" + is-url-superb "^4.0.0" + quote-unquote "^1.0.0" + postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.36, postcss@^7.0.5, postcss@^7.0.6: version "7.0.39" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" @@ -23765,6 +24147,15 @@ postcss@^8.4.14: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.23: + version "8.4.25" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.25.tgz#4a133f5e379eda7f61e906c3b1aaa9b81292726f" + integrity sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + potpack@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/potpack/-/potpack-2.0.0.tgz#61f4dd2dc4b3d5e996e3698c0ec9426d0e169104" @@ -23795,6 +24186,24 @@ prebuild-install@^7.1.1: tar-fs "^2.0.0" tunnel-agent "^0.6.0" +precinct@^11.0.5: + version "11.0.5" + resolved "https://registry.yarnpkg.com/precinct/-/precinct-11.0.5.tgz#3e15b3486670806f18addb54b8533e23596399ff" + integrity sha512-oHSWLC8cL/0znFhvln26D14KfCQFFn4KOLSw6hmLhd+LQ2SKt9Ljm89but76Pc7flM9Ty1TnXyrA2u16MfRV3w== + dependencies: + "@dependents/detective-less" "^4.1.0" + commander "^10.0.1" + detective-amd "^5.0.2" + detective-cjs "^5.0.1" + detective-es6 "^4.0.1" + detective-postcss "^6.1.3" + detective-sass "^5.0.3" + detective-scss "^4.0.3" + detective-stylus "^4.0.0" + detective-typescript "^11.1.0" + module-definition "^5.0.1" + node-source-walk "^6.0.2" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -24285,6 +24694,11 @@ quote-stream@^1.0.1: minimist "^1.1.3" through2 "^2.0.0" +quote-unquote@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/quote-unquote/-/quote-unquote-1.0.0.tgz#67a9a77148effeaf81a4d428404a710baaac8a0b" + integrity sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg== + raf-schd@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" @@ -25095,6 +25509,13 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= + dependencies: + resolve "^1.1.6" + rechoir@^0.7.0: version "0.7.1" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" @@ -25556,6 +25977,13 @@ request-progress@^3.0.0: dependencies: throttleit "^1.0.0" +require-and-forget@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-and-forget/-/require-and-forget-1.0.1.tgz#b535a1b8f0f0dd6a48ab05b0ab15d26135d61142" + integrity sha512-Sea861D/seGo3cptxc857a34Df0oEijXit8Q3IDodiwZMzVmyXrRI9EgQQa3hjkhoEjNzCBvv0t/0fMgebmWLg== + dependencies: + debug "4.3.4" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -25585,6 +26013,19 @@ requireindex@~1.2.0: resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef" integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww== +requirejs-config-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/requirejs-config-file/-/requirejs-config-file-4.0.0.tgz#4244da5dd1f59874038cc1091d078d620abb6ebc" + integrity sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw== + dependencies: + esprima "^4.0.0" + stringify-object "^3.2.1" + +requirejs@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9" + integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg== + requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -25612,6 +26053,11 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" +resolve-dependency-path@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/resolve-dependency-path/-/resolve-dependency-path-3.0.2.tgz#012816717bcbe8b846835da11af9d2beb5acef50" + integrity sha512-Tz7zfjhLfsvR39ADOSk9us4421J/1ztVBo4rWUkF38hgHK5m0OCZ3NxFVpqHRkjctnwVa15igEUHFJp8MCS7vA== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -25656,7 +26102,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.1.5, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1, resolve@^1.3.2, resolve@^1.9.0: +resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1, resolve@^1.3.2, resolve@^1.9.0: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -25665,6 +26111,15 @@ resolve@^1.1.5, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12. path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.3: + version "1.22.3" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.3.tgz#4b4055349ffb962600972da1fdc33c46a4eb3283" + integrity sha512-P8ur/gp/AmbEzjr729bZnLjXK5Z+4P0zhIJgBgzqRih7hL7BOukHGtSTA3ACMY467GRFz3duQsi0bDZdR7DKdw== + dependencies: + is-core-module "^2.12.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^2.0.0-next.4: version "2.0.0-next.4" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660" @@ -25963,6 +26418,13 @@ sass-loader@^10.4.1: schema-utils "^3.0.0" semver "^7.3.2" +sass-lookup@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/sass-lookup/-/sass-lookup-5.0.1.tgz#1f01d7ff21e09d8c9dcf8d05b3fca28f2f96e6ed" + integrity sha512-t0X5PaizPc2H4+rCwszAqHZRtr4bugo4pgiCvrBFvIX0XFxnr29g77LJcpyj9A0DcKf7gXMLcgvRjsonYI6x4g== + dependencies: + commander "^10.0.1" + sax@>=0.6.0, sax@^1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -26313,6 +26775,15 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shelljs@^0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + side-channel@^1.0.2, side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -26796,6 +27267,17 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" +spec-change@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/spec-change/-/spec-change-1.7.1.tgz#3c56185c887a15482f1fbb3362916fc97c8fdb9f" + integrity sha512-bZmtSmS5w6M6Snae+AGp+y89MZ7QG2SZW1v3Au83+YWcZzCu0YtH2hXruJWXg6VdYUpQ3n+m9bRrWmwLaPkFjQ== + dependencies: + arg "^5.0.2" + debug "^4.3.4" + dependency-tree "^10.0.9" + globby "^11.1.0" + lazy-ass "^2.0.3" + specificity@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.1.tgz#aab5e645012db08ba182e151165738d00887b019" @@ -27199,6 +27681,15 @@ stringify-entities@^3.0.0, stringify-entities@^3.0.1: is-decimal "^1.0.2" is-hexadecimal "^1.0.0" +stringify-object@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -27401,6 +27892,13 @@ stylis@4.2.0: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== +stylus-lookup@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/stylus-lookup/-/stylus-lookup-5.0.1.tgz#3c4d116c3b1e8e1a8169c0d9cd20e608595560f4" + integrity sha512-tLtJEd5AGvnVy4f9UHQMw4bkJJtaAcmo54N+ovQBjDY3DuWyK9Eltxzr5+KG0q4ew6v2EHyuWWNnHeiw/Eo7rQ== + dependencies: + commander "^10.0.1" + success-symbol@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/success-symbol/-/success-symbol-0.1.0.tgz#24022e486f3bf1cdca094283b769c472d3b72897" @@ -28121,6 +28619,25 @@ ts-morph@^13.0.2: "@ts-morph/common" "~0.12.2" code-block-writer "^11.0.0" +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + ts-pnp@^1.1.6: version "1.2.0" resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" @@ -28136,6 +28653,15 @@ tsconfig-paths@^3.11.0: minimist "^1.2.0" strip-bom "^3.0.0" +tsconfig-paths@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + tsd@^0.20.0: version "0.20.0" resolved "https://registry.yarnpkg.com/tsd/-/tsd-0.20.0.tgz#0346321ee3c506545486227e488e753109164248" @@ -28187,7 +28713,7 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -tunnel@0.0.6: +tunnel@0.0.6, tunnel@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== @@ -28329,7 +28855,7 @@ typescript-tuple@^2.2.1: dependencies: typescript-compare "^0.0.2" -typescript@4.6.3, typescript@^3.3.3333, typescript@^4.6.3, typescript@^4.8.4: +typescript@4.6.3, typescript@^3.3.3333, typescript@^4.6.3, typescript@^4.8.4, typescript@^5.0.4: version "4.6.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c" integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== @@ -28909,6 +29435,11 @@ uuid@^8.3.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -29503,7 +30034,7 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" -wcwidth@^1.0.1: +wcwidth@>=1.0.1, wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= @@ -30314,6 +30845,11 @@ yazl@^2.5.1: dependencies: buffer-crc32 "~0.2.3" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"